mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-18 01:29:09 +00:00
Compare commits
18 Commits
v1.6.0-dev
...
v1.7.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
263bf0bf36 | ||
|
|
e0ed371adb | ||
|
|
87934fb3d1 | ||
|
|
19b7ed5f06 | ||
|
|
c027e37d9a | ||
|
|
2251a9c727 | ||
|
|
02a8ba16cd | ||
|
|
565090a951 | ||
|
|
943aad1564 | ||
|
|
9fb4762c7b | ||
|
|
be92047d65 | ||
|
|
0940e843ac | ||
|
|
a880bc9445 | ||
|
|
6c8eb02cbb | ||
|
|
08b048845f | ||
|
|
77e477e07f | ||
|
|
5ba6034b2f | ||
|
|
95dafdf839 |
85
CHANGELOG.md
85
CHANGELOG.md
@@ -1,6 +1,20 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
## [1.6.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0...v1.6.0-develop.1) (2023-05-04)
|
## [1.7.0-develop.2](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0-develop.1...v1.7.0-develop.2) (2023-05-10)
|
||||||
|
|
||||||
|
## [1.7.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.6.0...v1.7.0-develop.1) (2023-05-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* function config file ([02a8ba1](https://github.com/chenasraf/simple-scaffold/commit/02a8ba16cd6ee31806532845cb5ddbe0f5abf7de))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* use path.normalize ([565090a](https://github.com/chenasraf/simple-scaffold/commit/565090a951e13dd222f2f802df717e7cb6ca0a73))
|
||||||
|
|
||||||
|
## [1.6.0](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0...v1.6.0) (2023-05-05)
|
||||||
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
@@ -12,58 +26,57 @@
|
|||||||
|
|
||||||
* move dependency to dev dependency ([d916d88](https://github.com/chenasraf/simple-scaffold/commit/d916d88384054e6c6b40e6299073f1d1acb4d29d))
|
* move dependency to dev dependency ([d916d88](https://github.com/chenasraf/simple-scaffold/commit/d916d88384054e6c6b40e6299073f1d1acb4d29d))
|
||||||
|
|
||||||
## [1.5.0-develop.3](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0-develop.2...v1.5.0-develop.3) (2023-05-04)
|
## Change Log
|
||||||
|
|
||||||
|
# [1.5.0](https://github.com/chenasraf/simple-scaffold/compare/v1.6.0-develop.1...v1.5.0) (2023-05-04)
|
||||||
|
|
||||||
|
## [1.6.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0...v1.6.0-develop.1) (2023-05-04)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* node.js function for remote configs ([7e9022f](https://github.com/chenasraf/simple-scaffold/commit/7e9022f4331c8a1351642042c0215f9160b2768a))
|
- node.js function for remote configs
|
||||||
|
([ce5adbe](https://github.com/chenasraf/simple-scaffold/commit/ce5adbe0f898a86db6046d7f66d83dfcaa519ad2))
|
||||||
## [1.5.0-develop.2](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0-develop.1...v1.5.0-develop.2) (2023-05-03)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* move dependency to dev dependency ([408a940](https://github.com/chenasraf/simple-scaffold/commit/408a94085366bb4e39391fcfcfa7df78b06a480f))
|
- move dependency to dev dependency
|
||||||
|
([d916d88](https://github.com/chenasraf/simple-scaffold/commit/d916d88384054e6c6b40e6299073f1d1acb4d29d))
|
||||||
|
|
||||||
## [1.5.0](https://github.com/chenasraf/simple-scaffold/compare/v1.4.0...v1.5.0) (2023-05-02)
|
## [1.5.0](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0-develop.1...v1.5.0) (2023-05-02)
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* add github remote templates ([f961c13](https://github.com/chenasraf/simple-scaffold/commit/f961c13da15320b42540773ed958cdc3f97e4502))
|
|
||||||
* support for remote template configs ([05487f4](https://github.com/chenasraf/simple-scaffold/commit/05487f4d1e3b05f1d695242bb54427ee2fbdf247))
|
|
||||||
|
|
||||||
## [1.5.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.4.0...v1.5.0-develop.1) (2023-05-02)
|
## [1.5.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.4.0...v1.5.0-develop.1) (2023-05-02)
|
||||||
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* add github remote templates ([f961c13](https://github.com/chenasraf/simple-scaffold/commit/f961c13da15320b42540773ed958cdc3f97e4502))
|
- add github remote templates
|
||||||
* support for remote template configs ([05487f4](https://github.com/chenasraf/simple-scaffold/commit/05487f4d1e3b05f1d695242bb54427ee2fbdf247))
|
([f961c13](https://github.com/chenasraf/simple-scaffold/commit/f961c13da15320b42540773ed958cdc3f97e4502))
|
||||||
|
- support for remote template configs
|
||||||
|
([05487f4](https://github.com/chenasraf/simple-scaffold/commit/05487f4d1e3b05f1d695242bb54427ee2fbdf247))
|
||||||
|
|
||||||
## [1.4.0](https://github.com/chenasraf/simple-scaffold/compare/v1.3.2...v1.4.0) (2023-04-28)
|
## [1.4.0](https://github.com/chenasraf/simple-scaffold/compare/v1.3.2...v1.4.0) (2023-04-28)
|
||||||
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* add `--key` | `-k` to config loader ([6c5ba0b](https://github.com/chenasraf/simple-scaffold/commit/6c5ba0bc916fb1d59240d2eaa1abedc74527a974))
|
- add `--key` | `-k` to config loader
|
||||||
|
([6c5ba0b](https://github.com/chenasraf/simple-scaffold/commit/6c5ba0bc916fb1d59240d2eaa1abedc74527a974))
|
||||||
|
|
||||||
## [1.3.2](https://github.com/chenasraf/simple-scaffold/compare/v1.3.1...v1.3.2) (2023-04-28)
|
## [1.3.2](https://github.com/chenasraf/simple-scaffold/compare/v1.3.1...v1.3.2) (2023-04-28)
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* release build ([2c23fa9](https://github.com/chenasraf/simple-scaffold/commit/2c23fa9dbb310cd0a31f09606798f96b95d66779))
|
- release build
|
||||||
* release build asset ([0bef2df](https://github.com/chenasraf/simple-scaffold/commit/0bef2df5f3aa800ad5f1094c0996108db9acce51))
|
([2c23fa9](https://github.com/chenasraf/simple-scaffold/commit/2c23fa9dbb310cd0a31f09606798f96b95d66779))
|
||||||
|
- release build asset
|
||||||
|
([0bef2df](https://github.com/chenasraf/simple-scaffold/commit/0bef2df5f3aa800ad5f1094c0996108db9acce51))
|
||||||
|
|
||||||
## [1.3.1](https://github.com/chenasraf/simple-scaffold/compare/v1.3.0...v1.3.1) (2023-04-28)
|
## [1.3.1](https://github.com/chenasraf/simple-scaffold/compare/v1.3.0...v1.3.1) (2023-04-28)
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* docs ([6e19a86](https://github.com/chenasraf/simple-scaffold/commit/6e19a86190dd924058a48448aa6463569ef1125f))
|
- docs
|
||||||
* remove old peer-dep ([c7e2ef8](https://github.com/chenasraf/simple-scaffold/commit/c7e2ef862cb658feb1071ac120b185d8b34d6dd3))
|
([6e19a86](https://github.com/chenasraf/simple-scaffold/commit/6e19a86190dd924058a48448aa6463569ef1125f))
|
||||||
|
- remove old peer-dep
|
||||||
|
([c7e2ef8](https://github.com/chenasraf/simple-scaffold/commit/c7e2ef862cb658feb1071ac120b185d8b34d6dd3))
|
||||||
|
|
||||||
## [1.3.0](https://github.com/chenasraf/simple-scaffold/compare/v1.2.0...v1.3.0) (2023-04-25)
|
## [1.3.0](https://github.com/chenasraf/simple-scaffold/compare/v1.2.0...v1.3.0) (2023-04-25)
|
||||||
|
|
||||||
@@ -90,28 +103,14 @@
|
|||||||
|
|
||||||
- ci node version
|
- ci node version
|
||||||
([767d34c](https://github.com/chenasraf/simple-scaffold/commit/767d34c684516d4cea865b25e87c27c779bb79ce))
|
([767d34c](https://github.com/chenasraf/simple-scaffold/commit/767d34c684516d4cea865b25e87c27c779bb79ce))
|
||||||
- semantic-release build dir
|
|
||||||
([f7956dd](https://github.com/chenasraf/simple-scaffold/commit/f7956ddc786018905c48ccf1f21a3bb4657c3d75))
|
|
||||||
- support quote wrapping in append-data
|
|
||||||
([4fecca8](https://github.com/chenasraf/simple-scaffold/commit/4fecca848347312d45d704f82f2bcb3822da9b06))
|
|
||||||
|
|
||||||
## [1.1.4](https://github.com/chenasraf/simple-scaffold/compare/v1.1.3...v1.1.4) (2023-03-13)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- github action node version
|
- github action node version
|
||||||
([7c19c53](https://github.com/chenasraf/simple-scaffold/commit/7c19c533376dc6904231e5cc51c7a4b2658c66e0))
|
([7c19c53](https://github.com/chenasraf/simple-scaffold/commit/7c19c533376dc6904231e5cc51c7a4b2658c66e0))
|
||||||
- github action node version
|
- github action node version
|
||||||
([94fec76](https://github.com/chenasraf/simple-scaffold/commit/94fec766165f7540c578dbf2d0aeeb6ea3969ad8))
|
([94fec76](https://github.com/chenasraf/simple-scaffold/commit/94fec766165f7540c578dbf2d0aeeb6ea3969ad8))
|
||||||
|
- semantic-release build dir
|
||||||
### Misc
|
([f7956dd](https://github.com/chenasraf/simple-scaffold/commit/f7956ddc786018905c48ccf1f21a3bb4657c3d75))
|
||||||
|
- support quote wrapping in append-data
|
||||||
- update typedoc version
|
([4fecca8](https://github.com/chenasraf/simple-scaffold/commit/4fecca848347312d45d704f82f2bcb3822da9b06))
|
||||||
([c334396](https://github.com/chenasraf/simple-scaffold/commit/c334396d74414cbe0aba305c66dfad7fdeb88669))
|
|
||||||
- update dependencies
|
|
||||||
([20400bd](https://github.com/chenasraf/simple-scaffold/commit/20400bd81dd43d457427675286c9964a8bc0d5f6))
|
|
||||||
- bump version number
|
|
||||||
([8e432bf](https://github.com/chenasraf/simple-scaffold/commit/8e432bfb0b410dc0655c3924031bea2648a42ad0))
|
|
||||||
|
|
||||||
## [1.1.3](https://github.com/chenasraf/simple-scaffold/compare/v1.1.2...v1.1.3) (2023-03-11)
|
## [1.1.3](https://github.com/chenasraf/simple-scaffold/compare/v1.1.2...v1.1.3) (2023-03-11)
|
||||||
|
|
||||||
|
|||||||
64
README.md
64
README.md
@@ -40,10 +40,12 @@ The fastest way to get started is to use `npx` to immediately start a scaffold p
|
|||||||
Prepare any templates you want to use - for example, in the directory `templates/component`; and use
|
Prepare any templates you want to use - for example, in the directory `templates/component`; and use
|
||||||
that in the CLI args. Here is a simple example file:
|
that in the CLI args. Here is a simple example file:
|
||||||
|
|
||||||
|
Simple Scaffold will maintain any file and directory structure you try to generate.
|
||||||
|
|
||||||
`templates/component/{{ pascalName name }}.tsx`
|
`templates/component/{{ pascalName name }}.tsx`
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// Created: {{ now | 'yyyy-MM-dd' }}
|
// Created: {{ now 'yyyy-MM-dd' }}
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export default {{pascalCase name}}: React.FC = (props) => {
|
export default {{pascalCase name}}: React.FC = (props) => {
|
||||||
@@ -57,7 +59,7 @@ To generate the template output, run:
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
# generate single component
|
# generate single component
|
||||||
npx simple-scaffold@latest \
|
$ npx simple-scaffold@latest \
|
||||||
-t templates/component -o src/components PageWrapper
|
-t templates/component -o src/components PageWrapper
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -74,27 +76,73 @@ export default PageWrapper: React.FC = (props) => {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Remote Templates
|
### Configuration Files
|
||||||
|
|
||||||
|
You can also use a config file to more easily maintain all your scaffold definitions.
|
||||||
|
|
||||||
|
`scaffold.config.js`
|
||||||
|
|
||||||
|
```js
|
||||||
|
module.exports = {
|
||||||
|
// use "default" to avoid needing to specify key
|
||||||
|
// in this case the key is "component"
|
||||||
|
component: {
|
||||||
|
templates: ["templates/component"],
|
||||||
|
output: "src/components",
|
||||||
|
data: {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then call your scaffold like this:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ npx simple-scaffold@latest -c scaffold.config.js PageWrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
This will allow you to avoid needing to remember which configs are needed or to store them in a
|
||||||
|
1-liner in `packqge.json` which can get pretty long and messy, which is harder to maintain.
|
||||||
|
|
||||||
|
Also, this allows you to define more complex scaffolds with logic without having to use the Node.js
|
||||||
|
API directly. (Of course you always have the option to still do so if you wish)
|
||||||
|
|
||||||
|
See more at the [CLI documentation](https://chenasraf.github.io/simple-scaffold/pages/cli.html) and
|
||||||
|
[Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/configuration_files.html).
|
||||||
|
|
||||||
|
### Remote Configurations
|
||||||
|
|
||||||
Another quick way to start is to re-use someone else's (or your own) work using a template
|
Another quick way to start is to re-use someone else's (or your own) work using a template
|
||||||
repository.
|
repository.
|
||||||
|
|
||||||
|
A remote config can be loaded in one of these ways:
|
||||||
|
|
||||||
|
- If it's on GitHub, you can use `-gh user/repository_name`
|
||||||
|
- If it's on another git server (such as GitLab), you can use
|
||||||
|
`-c https://example.com/user/repository_name.git`
|
||||||
|
|
||||||
|
Configurations can hold multiple scaffold groups. Each group can be accessed using its key by
|
||||||
|
supplying the `--key` or `-k` argument, or by appending a hash and then the key name, like so:
|
||||||
|
`-gh user/repository_name#key_name` - this also works for the `-c` flag.
|
||||||
|
|
||||||
Here is an example for loading the example component templates in this very repository:
|
Here is an example for loading the example component templates in this very repository:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npx simple-scaffold@latest \
|
$ npx simple-scaffold@latest \
|
||||||
-gh chenasraf/simple-scaffold#examples/test-input/scaffold.config.js:component \
|
-gh chenasraf/simple-scaffold#scaffold.config.js:component \
|
||||||
PageWrapper
|
PageWrapper
|
||||||
|
|
||||||
# equivalent to:
|
# equivalent to:
|
||||||
npx simple-scaffold@latest \
|
$ npx simple-scaffold@latest \
|
||||||
-c https://github.com/chenasraf/simple-scaffold.git#examples/test-input/scaffold.config.js:component \
|
-c https://github.com/chenasraf/simple-scaffold.git#scaffold.config.js:component \
|
||||||
PageWrapper
|
PageWrapper
|
||||||
```
|
```
|
||||||
|
|
||||||
When template name (`:component`) is omitted, `default` is used.
|
When template name (`:component`) is omitted, `default` is used.
|
||||||
|
|
||||||
See more at the [CLI documentation](https://chenasraf.github.io/simple-scaffold/pages/cli.html)
|
See more at the [CLI documentation](https://chenasraf.github.io/simple-scaffold/pages/cli.html) and
|
||||||
|
[Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/configuration_files.html).
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "simple-scaffold",
|
"name": "simple-scaffold",
|
||||||
"version": "1.5.0",
|
"version": "1.7.0-develop.2",
|
||||||
"description": "Generate any file structure - from single components to entire app boilerplates, with a single command.",
|
"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": "https://github.com/chenasraf/simple-scaffold.git",
|
"repository": "https://github.com/chenasraf/simple-scaffold.git",
|
||||||
"author": "Chen Asraf <inbox@casraf.com>",
|
"author": "Chen Asraf <contact@casraf.dev>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"bin": "cmd.js",
|
"bin": "cmd.js",
|
||||||
|
|||||||
4764
pnpm-lock.yaml
generated
Normal file
4764
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,13 @@
|
|||||||
const releaseRules = [
|
const releaseRules = [
|
||||||
{ type: "feat", section: "Features", release: "minor" },
|
{ type: "feat", section: "Features", release: "minor" },
|
||||||
{ type: "docs", section: "Build", release: false },
|
{ type: "revert", section: "Features", release: "minor" },
|
||||||
{ type: "fix", section: "Bug Fixes", release: "patch" },
|
{ type: "fix", section: "Bug Fixes", release: "patch" },
|
||||||
|
{ type: "chore", section: "Misc", release: "patch" },
|
||||||
{ type: "refactor", section: "Misc", release: "patch" },
|
{ type: "refactor", section: "Misc", release: "patch" },
|
||||||
{ type: "perf", section: "Misc", release: "patch" },
|
{ type: "perf", section: "Misc", release: "patch" },
|
||||||
{ type: "build", section: "Build", release: "patch" },
|
{ type: "build", section: "Build", release: "patch" },
|
||||||
{ type: "chore", section: "Misc", release: "patch" },
|
{ type: "docs", section: "Build", release: false },
|
||||||
{ type: "test", section: "Tests", release: "patch" },
|
{ type: "test", section: "Tests", release: false },
|
||||||
]
|
]
|
||||||
|
|
||||||
/** @type {import('semantic-release').Options} */
|
/** @type {import('semantic-release').Options} */
|
||||||
@@ -22,9 +23,9 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
analyzeCommits: {
|
analyzeCommits: {
|
||||||
path: "semantic-release-conventional-commits",
|
path: "semantic-release-conventional-commits",
|
||||||
majorTypes: ["major", "breaking"],
|
majorTypes: releaseRules.filter((x) => x.release === "major").map((x) => x.type),
|
||||||
minorTypes: ["minor", "feat", "feature"],
|
minorTypes: releaseRules.filter((x) => x.release === "minor").map((x) => x.type),
|
||||||
patchTypes: ["patch", "fix", "bugfix", "refactor", "perf", "revert"],
|
patchTypes: releaseRules.filter((x) => x.release === "patch").map((x) => x.type),
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
[
|
[
|
||||||
@@ -42,7 +43,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
preset: "conventionalcommits",
|
preset: "conventionalcommits",
|
||||||
parserOpts: {
|
parserOpts: {
|
||||||
noteKeywords: ["breaking"],
|
noteKeywords: ["breaking", "major"],
|
||||||
types: releaseRules,
|
types: releaseRules,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -57,6 +58,13 @@ module.exports = {
|
|||||||
[
|
[
|
||||||
"@semantic-release/npm",
|
"@semantic-release/npm",
|
||||||
{
|
{
|
||||||
|
npmPublish: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/npm",
|
||||||
|
{
|
||||||
|
npmPublish: true,
|
||||||
pkgRoot: "dist",
|
pkgRoot: "dist",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { LogLevel, ScaffoldCmdConfig } from "./types"
|
|||||||
import { Scaffold } from "./scaffold"
|
import { Scaffold } from "./scaffold"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { parseAppendData, parseConfig } from "./utils"
|
import { parseAppendData, parseConfig } from "./config"
|
||||||
|
|
||||||
export async function parseCliArgs(args = process.argv.slice(2)) {
|
export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||||
const pkg = JSON.parse((await fs.readFile(path.join(__dirname, "package.json"))).toString())
|
const pkg = JSON.parse((await fs.readFile(path.join(__dirname, "package.json"))).toString())
|
||||||
|
|||||||
168
src/config.ts
Normal file
168
src/config.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import path from "path"
|
||||||
|
import {
|
||||||
|
AsyncResolver,
|
||||||
|
ConfigLoadConfig,
|
||||||
|
FileResponse,
|
||||||
|
FileResponseHandler,
|
||||||
|
LogConfig,
|
||||||
|
LogLevel,
|
||||||
|
Resolver,
|
||||||
|
ScaffoldCmdConfig,
|
||||||
|
ScaffoldConfig,
|
||||||
|
ScaffoldConfigFile,
|
||||||
|
ScaffoldConfigMap,
|
||||||
|
} from "./types"
|
||||||
|
import { OptionsBase } from "massarg/types"
|
||||||
|
import { spawn } from "node:child_process"
|
||||||
|
import os from "node:os"
|
||||||
|
import { handlebarsParse } from "./parser"
|
||||||
|
import { log } from "./logger"
|
||||||
|
import { resolve } from "./utils"
|
||||||
|
|
||||||
|
export function getOptionValueForFile<T>(
|
||||||
|
config: ScaffoldConfig,
|
||||||
|
filePath: string,
|
||||||
|
fn: FileResponse<T>,
|
||||||
|
defaultValue?: T,
|
||||||
|
): T {
|
||||||
|
if (typeof fn !== "function") {
|
||||||
|
return defaultValue ?? (fn as T)
|
||||||
|
}
|
||||||
|
return (fn as FileResponseHandler<T>)(
|
||||||
|
filePath,
|
||||||
|
path.dirname(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
||||||
|
path.basename(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAppendData(value: string, options: ScaffoldCmdConfig & OptionsBase): unknown {
|
||||||
|
const data = options.data ?? {}
|
||||||
|
const [key, val] = value.split(/\:?=/)
|
||||||
|
// raw
|
||||||
|
if (value.includes(":=") && !val.includes(":=")) {
|
||||||
|
return { ...data, [key]: JSON.parse(val) }
|
||||||
|
}
|
||||||
|
return { ...data, [key]: isWrappedWithQuotes(val) ? val.substring(1, val.length - 1) : val }
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWrappedWithQuotes(string: string): boolean {
|
||||||
|
return (string.startsWith('"') && string.endsWith('"')) || (string.startsWith("'") && string.endsWith("'"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export async function parseConfig(config: ScaffoldCmdConfig & OptionsBase): Promise<ScaffoldConfig> {
|
||||||
|
let c: ScaffoldConfig = config
|
||||||
|
if (config.github) {
|
||||||
|
log(config, LogLevel.Info, `Loading config from github ${config.github}`)
|
||||||
|
const gitUrl = new URL(`https://github.com/${config.github}`)
|
||||||
|
if (!gitUrl.pathname.endsWith(".git")) {
|
||||||
|
gitUrl.pathname += ".git"
|
||||||
|
}
|
||||||
|
config.config = gitUrl.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.config) {
|
||||||
|
const isUrl = config.config.includes("://")
|
||||||
|
|
||||||
|
const hasColonToken = (!isUrl && config.config.includes(":")) || (isUrl && count(config.config, ":") > 1)
|
||||||
|
const colonIndex = config.config.lastIndexOf(":")
|
||||||
|
const [configFile, templateKey = "default"] = hasColonToken
|
||||||
|
? [config.config.substring(0, colonIndex), config.config.substring(colonIndex + 1)]
|
||||||
|
: [config.config, undefined]
|
||||||
|
const key = (config.key ?? templateKey) || "default"
|
||||||
|
log(config, LogLevel.Info, `Loading config from ${configFile} with key ${key}`)
|
||||||
|
const configPromise = await getConfig({ config: configFile, quiet: config.quiet, verbose: config.verbose })
|
||||||
|
const configImport = await resolve(configPromise, config)
|
||||||
|
|
||||||
|
if (!configImport[key]) {
|
||||||
|
throw new Error(`Template "${key}" not found in ${configFile}`)
|
||||||
|
}
|
||||||
|
c = {
|
||||||
|
...config,
|
||||||
|
...configImport[key],
|
||||||
|
data: {
|
||||||
|
...configImport[key].data,
|
||||||
|
...config.data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.data = { ...c.data, ...config.appendData }
|
||||||
|
delete config.appendData
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapNoopResolver<T, R = T>(value: Resolver<T, R>): Resolver<T, R> {
|
||||||
|
if (typeof value === "function") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return (_) => value
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export async function getConfig(config: ConfigLoadConfig): Promise<ScaffoldConfigFile> {
|
||||||
|
const { config: configFile, ...logConfig } = config as Required<typeof config>
|
||||||
|
const url = new URL(configFile)
|
||||||
|
|
||||||
|
if (url.protocol === "file:") {
|
||||||
|
log(logConfig, LogLevel.Info, `Loading config from file ${configFile}`)
|
||||||
|
const absolutePath = path.resolve(process.cwd(), configFile)
|
||||||
|
return wrapNoopResolver(import(absolutePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHttp = url.protocol === "http:" || url.protocol === "https:"
|
||||||
|
const isGit = url.protocol === "git:" || (isHttp && url.pathname.endsWith(".git"))
|
||||||
|
|
||||||
|
if (isHttp || isGit) {
|
||||||
|
if (isGit) {
|
||||||
|
return getGitConfig(url, logConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported protocol ${url.protocol}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapNoopResolver(import(path.resolve(process.cwd(), configFile)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGitConfig(
|
||||||
|
url: URL,
|
||||||
|
logConfig: LogConfig,
|
||||||
|
): Promise<AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>> {
|
||||||
|
const repoUrl = `${url.protocol}//${url.host}${url.pathname}`
|
||||||
|
|
||||||
|
log(logConfig, LogLevel.Info, `Cloning git repo ${repoUrl}`)
|
||||||
|
|
||||||
|
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const clone = spawn("git", ["clone", "--depth", "1", repoUrl, tmpPath])
|
||||||
|
|
||||||
|
clone.on("error", reject)
|
||||||
|
clone.on("close", async (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
log(logConfig, LogLevel.Info, `Loading config from git repo: ${repoUrl}`)
|
||||||
|
const hashPath = url.hash?.replace("#", "") || "scaffold.config.js"
|
||||||
|
const absolutePath = path.resolve(tmpPath, hashPath)
|
||||||
|
const loadedConfig = (await import(absolutePath)).default as ScaffoldConfigMap
|
||||||
|
log(logConfig, LogLevel.Info, `Loaded config from git`)
|
||||||
|
log(logConfig, LogLevel.Debug, `Raw config:`, loadedConfig)
|
||||||
|
const fixedConfig: ScaffoldConfigMap = Object.fromEntries(
|
||||||
|
Object.entries(loadedConfig).map(([k, v]) => [
|
||||||
|
k,
|
||||||
|
// use absolute paths for template as config is necessarily in another directory
|
||||||
|
{ ...v, templates: v.templates.map((t) => path.resolve(tmpPath, t)) },
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
resolve(wrapNoopResolver(fixedConfig))
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Git clone failed with code ${code}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function count(string: string, substring: string): number {
|
||||||
|
return string.split(substring).length - 1
|
||||||
|
}
|
||||||
207
src/file.ts
Normal file
207
src/file.ts
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import path from "path"
|
||||||
|
import { F_OK } from "constants"
|
||||||
|
import { LogLevel, ScaffoldConfig } from "./types"
|
||||||
|
import { promises as fsPromises } from "fs"
|
||||||
|
const { stat, access, mkdir } = fsPromises
|
||||||
|
import { glob, hasMagic } from "glob"
|
||||||
|
import { log } from "./logger"
|
||||||
|
import { getOptionValueForFile } from "./config"
|
||||||
|
import { handlebarsParse } from "./parser"
|
||||||
|
import { handleErr } from "./utils"
|
||||||
|
|
||||||
|
const { readFile, writeFile } = fsPromises
|
||||||
|
|
||||||
|
export async function createDirIfNotExists(dir: string, config: ScaffoldConfig): Promise<void> {
|
||||||
|
const parentDir = path.dirname(dir)
|
||||||
|
|
||||||
|
if (!(await pathExists(parentDir))) {
|
||||||
|
await createDirIfNotExists(parentDir, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await pathExists(dir))) {
|
||||||
|
try {
|
||||||
|
log(config, LogLevel.Debug, `Creating dir ${dir}`)
|
||||||
|
await mkdir(dir)
|
||||||
|
return
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.code !== "EEXIST") {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 async function isDir(path: string): Promise<boolean> {
|
||||||
|
const tplStat = await stat(path)
|
||||||
|
return tplStat.isDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeGlob(template: string): string {
|
||||||
|
return template.replace(/\*/g, "").replace(/(\/\/|\\\\)/g, path.sep)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeRelativePath(str: string): string {
|
||||||
|
return str.startsWith(path.sep) ? str.slice(1) : str
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBasePath(relPath: string): string {
|
||||||
|
return path
|
||||||
|
.resolve(process.cwd(), relPath)
|
||||||
|
.replace(process.cwd() + path.sep, "")
|
||||||
|
.replace(process.cwd(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFileList(_config: ScaffoldConfig, template: string): Promise<string[]> {
|
||||||
|
return (
|
||||||
|
await glob(template, {
|
||||||
|
dot: true,
|
||||||
|
nodir: true,
|
||||||
|
// debug: config.verbose === LogLevel.Debug,
|
||||||
|
})
|
||||||
|
).map(path.normalize)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GlobInfo {
|
||||||
|
nonGlobTemplate: string
|
||||||
|
origTemplate: string
|
||||||
|
isDirOrGlob: boolean
|
||||||
|
isGlob: boolean
|
||||||
|
template: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTemplateGlobInfo(config: ScaffoldConfig, template: string): Promise<GlobInfo> {
|
||||||
|
const isGlob = hasMagic(template)
|
||||||
|
log(config, LogLevel.Debug, "before isDir", "isGlob:", isGlob, template)
|
||||||
|
let _template = template
|
||||||
|
let nonGlobTemplate = isGlob ? removeGlob(template) : template
|
||||||
|
nonGlobTemplate = path.normalize(nonGlobTemplate)
|
||||||
|
const isDirOrGlob = isGlob ? true : await isDir(template)
|
||||||
|
log(config, LogLevel.Debug, "after isDir", isDirOrGlob)
|
||||||
|
const _shouldAddGlob = !isGlob && isDirOrGlob
|
||||||
|
const origTemplate = template
|
||||||
|
if (_shouldAddGlob) {
|
||||||
|
_template = path.join(template, "**", "*")
|
||||||
|
}
|
||||||
|
return { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template: _template }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OutputFileInfo {
|
||||||
|
inputPath: string
|
||||||
|
outputPathOpt: string
|
||||||
|
outputDir: string
|
||||||
|
outputPath: string
|
||||||
|
exists: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTemplateFileInfo(
|
||||||
|
config: ScaffoldConfig,
|
||||||
|
{ templatePath, basePath }: { templatePath: string; basePath: string },
|
||||||
|
): Promise<OutputFileInfo> {
|
||||||
|
const inputPath = path.resolve(process.cwd(), templatePath)
|
||||||
|
const outputPathOpt = getOptionValueForFile(config, inputPath, config.output)
|
||||||
|
const outputDir = getOutputDir(config, outputPathOpt, basePath)
|
||||||
|
const outputPath = handlebarsParse(config, path.join(outputDir, path.basename(inputPath)), {
|
||||||
|
isPath: true,
|
||||||
|
}).toString()
|
||||||
|
const exists = await pathExists(outputPath)
|
||||||
|
return { inputPath, outputPathOpt, outputDir, outputPath, exists }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function copyFileTransformed(
|
||||||
|
config: ScaffoldConfig,
|
||||||
|
{
|
||||||
|
exists,
|
||||||
|
overwrite,
|
||||||
|
outputPath,
|
||||||
|
inputPath,
|
||||||
|
}: {
|
||||||
|
exists: boolean
|
||||||
|
overwrite: boolean
|
||||||
|
outputPath: string
|
||||||
|
inputPath: string
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
if (!exists || overwrite) {
|
||||||
|
if (exists && overwrite) {
|
||||||
|
log(config, LogLevel.Info, `File ${outputPath} exists, overwriting`)
|
||||||
|
}
|
||||||
|
const templateBuffer = await readFile(inputPath)
|
||||||
|
const unprocessedOutputContents = handlebarsParse(config, templateBuffer)
|
||||||
|
const finalOutputContents =
|
||||||
|
(await config.beforeWrite?.(unprocessedOutputContents, templateBuffer, outputPath)) ?? unprocessedOutputContents
|
||||||
|
|
||||||
|
if (!config.dryRun) {
|
||||||
|
await writeFile(outputPath, finalOutputContents)
|
||||||
|
log(config, LogLevel.Info, "Done.")
|
||||||
|
} else {
|
||||||
|
log(config, LogLevel.Info, "Dry Run. Output should be:")
|
||||||
|
log(config, LogLevel.Info, finalOutputContents)
|
||||||
|
}
|
||||||
|
} else if (exists) {
|
||||||
|
log(config, LogLevel.Info, `File ${outputPath} already exists, skipping`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOutputDir(config: ScaffoldConfig, outputPathOpt: string, basePath: string): string {
|
||||||
|
return path.resolve(
|
||||||
|
process.cwd(),
|
||||||
|
...([
|
||||||
|
outputPathOpt,
|
||||||
|
basePath,
|
||||||
|
config.createSubFolder
|
||||||
|
? config.subFolderNameHelper
|
||||||
|
? handlebarsParse(config, `{{ ${config.subFolderNameHelper} name }}`).toString()
|
||||||
|
: config.name
|
||||||
|
: undefined,
|
||||||
|
].filter(Boolean) as string[]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleTemplateFile(
|
||||||
|
config: ScaffoldConfig,
|
||||||
|
{ templatePath, basePath }: { templatePath: string; basePath: string },
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(config, {
|
||||||
|
templatePath,
|
||||||
|
basePath,
|
||||||
|
})
|
||||||
|
const overwrite = getOptionValueForFile(config, inputPath, config.overwrite ?? false)
|
||||||
|
|
||||||
|
log(
|
||||||
|
config,
|
||||||
|
LogLevel.Debug,
|
||||||
|
`\nParsing ${templatePath}`,
|
||||||
|
`\nBase path: ${basePath}`,
|
||||||
|
`\nFull input path: ${inputPath}`,
|
||||||
|
`\nOutput Path Opt: ${outputPathOpt}`,
|
||||||
|
`\nFull output dir: ${outputDir}`,
|
||||||
|
`\nFull output path: ${outputPath}`,
|
||||||
|
`\n`,
|
||||||
|
)
|
||||||
|
|
||||||
|
await createDirIfNotExists(path.dirname(outputPath), config)
|
||||||
|
|
||||||
|
log(config, LogLevel.Info, `Writing to ${outputPath}`)
|
||||||
|
await copyFileTransformed(config, { exists, overwrite, outputPath, inputPath })
|
||||||
|
resolve()
|
||||||
|
} catch (e: any) {
|
||||||
|
handleErr(e)
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
87
src/logger.ts
Normal file
87
src/logger.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { LogConfig, LogLevel, ScaffoldConfig } from "./types"
|
||||||
|
import chalk from "chalk"
|
||||||
|
|
||||||
|
export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
|
||||||
|
if (config.quiet || config.verbose === LogLevel.None || level < (config.verbose ?? LogLevel.Info)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelColor: Record<LogLevel, keyof typeof chalk> = {
|
||||||
|
[LogLevel.None]: "reset",
|
||||||
|
[LogLevel.Debug]: "blue",
|
||||||
|
[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 function logInputFile(
|
||||||
|
config: ScaffoldConfig,
|
||||||
|
{
|
||||||
|
origTemplate,
|
||||||
|
relPath,
|
||||||
|
template,
|
||||||
|
inputFilePath,
|
||||||
|
nonGlobTemplate,
|
||||||
|
basePath,
|
||||||
|
isDirOrGlob,
|
||||||
|
isGlob,
|
||||||
|
}: {
|
||||||
|
origTemplate: string
|
||||||
|
relPath: string
|
||||||
|
template: string
|
||||||
|
inputFilePath: string
|
||||||
|
nonGlobTemplate: string
|
||||||
|
basePath: string
|
||||||
|
isDirOrGlob: boolean
|
||||||
|
isGlob: boolean
|
||||||
|
},
|
||||||
|
): void {
|
||||||
|
log(
|
||||||
|
config,
|
||||||
|
LogLevel.Debug,
|
||||||
|
`\nprocess.cwd(): ${process.cwd()}`,
|
||||||
|
`\norigTemplate: ${origTemplate}`,
|
||||||
|
`\nrelPath: ${relPath}`,
|
||||||
|
`\ntemplate: ${template}`,
|
||||||
|
`\ninputFilePath: ${inputFilePath}`,
|
||||||
|
`\nnonGlobTemplate: ${nonGlobTemplate}`,
|
||||||
|
`\nbasePath: ${basePath}`,
|
||||||
|
`\nisDirOrGlob: ${isDirOrGlob}`,
|
||||||
|
`\nisGlob: ${isGlob}`,
|
||||||
|
`\n`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logInitStep(config: ScaffoldConfig): void {
|
||||||
|
log(config, LogLevel.Debug, "Full config:", {
|
||||||
|
name: config.name,
|
||||||
|
templates: config.templates,
|
||||||
|
output: config.output,
|
||||||
|
createSubFolder: config.createSubFolder,
|
||||||
|
data: config.data,
|
||||||
|
overwrite: config.overwrite,
|
||||||
|
quiet: config.quiet,
|
||||||
|
subFolderNameHelper: config.subFolderNameHelper,
|
||||||
|
helpers: Object.keys(config.helpers ?? {}),
|
||||||
|
verbose: `${config.verbose} (${Object.keys(LogLevel).find(
|
||||||
|
(k) => (LogLevel[k as any] as unknown as number) === config.verbose!,
|
||||||
|
)})`,
|
||||||
|
dryRun: config.dryRun,
|
||||||
|
beforeWrite: config.beforeWrite,
|
||||||
|
} as Record<keyof ScaffoldConfig, unknown>)
|
||||||
|
log(config, LogLevel.Info, "Data:", config.data)
|
||||||
|
}
|
||||||
108
src/parser.ts
Normal file
108
src/parser.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import path from "path"
|
||||||
|
import { DefaultHelpers, 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 dtAdd from "date-fns/add"
|
||||||
|
import dtFormat from "date-fns/format"
|
||||||
|
import dtParseISO from "date-fns/parseISO"
|
||||||
|
import { log } from "./logger"
|
||||||
|
|
||||||
|
const dateFns = {
|
||||||
|
add: dtAdd,
|
||||||
|
format: dtFormat,
|
||||||
|
parseISO: dtParseISO,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultHelpers: Record<DefaultHelpers, Helper> = {
|
||||||
|
camelCase,
|
||||||
|
snakeCase,
|
||||||
|
startCase,
|
||||||
|
kebabCase,
|
||||||
|
hyphenCase: kebabCase,
|
||||||
|
pascalCase,
|
||||||
|
lowerCase: (text) => text.toLowerCase(),
|
||||||
|
upperCase: (text) => text.toUpperCase(),
|
||||||
|
now: nowHelper,
|
||||||
|
date: dateHelper,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function _dateHelper(date: Date, formatString: string): string
|
||||||
|
export function _dateHelper(
|
||||||
|
date: Date,
|
||||||
|
formatString: string,
|
||||||
|
durationDifference: number,
|
||||||
|
durationType: keyof Duration,
|
||||||
|
): string
|
||||||
|
export function _dateHelper(
|
||||||
|
date: Date,
|
||||||
|
formatString: string,
|
||||||
|
durationDifference?: number,
|
||||||
|
durationType?: keyof Duration,
|
||||||
|
): string {
|
||||||
|
if (durationType && durationDifference !== undefined) {
|
||||||
|
return dateFns.format(dateFns.add(date, { [durationType]: durationDifference }), formatString)
|
||||||
|
}
|
||||||
|
return dateFns.format(date, formatString)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nowHelper(formatString: string): string
|
||||||
|
export function nowHelper(formatString: string, durationDifference: number, durationType: keyof Duration): string
|
||||||
|
export function nowHelper(formatString: string, durationDifference?: number, durationType?: keyof Duration): string {
|
||||||
|
return _dateHelper(new Date(), formatString, durationDifference!, durationType!)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dateHelper(date: string, formatString: string): string
|
||||||
|
export function dateHelper(
|
||||||
|
date: string,
|
||||||
|
formatString: string,
|
||||||
|
durationDifference: number,
|
||||||
|
durationType: keyof Duration,
|
||||||
|
): string
|
||||||
|
|
||||||
|
export function dateHelper(
|
||||||
|
date: string,
|
||||||
|
formatString: string,
|
||||||
|
durationDifference?: number,
|
||||||
|
durationType?: keyof Duration,
|
||||||
|
): string {
|
||||||
|
return _dateHelper(dateFns.parseISO(date), formatString, durationDifference!, durationType!)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pascalCase(s: string): string {
|
||||||
|
return startCase(s).replace(/\s+/g, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerHelpers(config: ScaffoldConfig): void {
|
||||||
|
const _helpers = { ...defaultHelpers, ...config.helpers }
|
||||||
|
for (const helperName in _helpers) {
|
||||||
|
log(config, LogLevel.Debug, `Registering helper: ${helperName}`)
|
||||||
|
Handlebars.registerHelper(helperName, _helpers[helperName as keyof typeof _helpers])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handlebarsParse(
|
||||||
|
config: ScaffoldConfig,
|
||||||
|
templateBuffer: Buffer | string,
|
||||||
|
{ isPath = false }: { isPath?: boolean } = {},
|
||||||
|
): Buffer {
|
||||||
|
const { data } = config
|
||||||
|
try {
|
||||||
|
let str = templateBuffer.toString()
|
||||||
|
if (isPath) {
|
||||||
|
str = str.replace(/\\/g, "/")
|
||||||
|
}
|
||||||
|
const parser = Handlebars.compile(str, { noEscape: true })
|
||||||
|
let outputContents = parser(data)
|
||||||
|
if (isPath && path.sep !== "/") {
|
||||||
|
outputContents = outputContents.replace(/\//g, "\\")
|
||||||
|
}
|
||||||
|
return Buffer.from(outputContents)
|
||||||
|
} catch (e) {
|
||||||
|
log(config, LogLevel.Debug, e)
|
||||||
|
log(config, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
|
||||||
|
return Buffer.from(templateBuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/scaffold.ts
Executable file → Normal file
64
src/scaffold.ts
Executable file → Normal file
@@ -5,28 +5,24 @@
|
|||||||
* See [readme](README.md)
|
* See [readme](README.md)
|
||||||
*/
|
*/
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { handleErr, resolve } from "./utils"
|
||||||
import {
|
import {
|
||||||
createDirIfNotExists,
|
createDirIfNotExists,
|
||||||
getOptionValueForFile,
|
|
||||||
handleErr,
|
|
||||||
log,
|
|
||||||
pascalCase,
|
|
||||||
isDir,
|
isDir,
|
||||||
removeGlob,
|
removeGlob,
|
||||||
makeRelativePath,
|
makeRelativePath,
|
||||||
registerHelpers,
|
|
||||||
getTemplateGlobInfo,
|
getTemplateGlobInfo,
|
||||||
getFileList,
|
getFileList,
|
||||||
getBasePath,
|
getBasePath,
|
||||||
copyFileTransformed,
|
copyFileTransformed,
|
||||||
getTemplateFileInfo,
|
getTemplateFileInfo,
|
||||||
logInitStep,
|
handleTemplateFile,
|
||||||
logInputFile,
|
} from "./file"
|
||||||
parseConfig,
|
import { LogLevel, MinimalConfig, Resolver, ScaffoldCmdConfig, ScaffoldConfig } from "./types"
|
||||||
} from "./utils"
|
|
||||||
import { LogLevel, ScaffoldCmdConfig, ScaffoldConfig } from "./types"
|
|
||||||
import { OptionsBase } from "massarg/types"
|
import { OptionsBase } from "massarg/types"
|
||||||
|
import { pascalCase, registerHelpers } from "./parser"
|
||||||
|
import { log, logInitStep, logInputFile } from "./logger"
|
||||||
|
import { getOptionValueForFile, parseConfig } from "./config"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a scaffold using given `options`.
|
* Create a scaffold using given `options`.
|
||||||
@@ -117,9 +113,12 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
|
|||||||
* @return {Promise<void>} A promise that resolves when the scaffold is complete
|
* @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,
|
pathOrUrl: string,
|
||||||
config: Pick<ScaffoldCmdConfig, "name" | "key">,
|
/** Information needed before loading the config */
|
||||||
overrides?: Partial<Omit<ScaffoldConfig, "name">>,
|
config: MinimalConfig,
|
||||||
|
/** Any overrides to the loaded config */
|
||||||
|
overrides?: Resolver<ScaffoldCmdConfig, Partial<Omit<ScaffoldConfig, "name">>>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const _cmdConfig: ScaffoldCmdConfig & OptionsBase = {
|
const _cmdConfig: ScaffoldCmdConfig & OptionsBase = {
|
||||||
dryRun: false,
|
dryRun: false,
|
||||||
@@ -134,44 +133,9 @@ Scaffold.fromConfig = async function (
|
|||||||
config: pathOrUrl,
|
config: pathOrUrl,
|
||||||
...config,
|
...config,
|
||||||
}
|
}
|
||||||
|
const _overrides = resolve(overrides, _cmdConfig)
|
||||||
const _config = await parseConfig(_cmdConfig)
|
const _config = await parseConfig(_cmdConfig)
|
||||||
return Scaffold({ ..._config, ...overrides })
|
return Scaffold({ ..._config, ..._overrides })
|
||||||
}
|
|
||||||
|
|
||||||
async function handleTemplateFile(
|
|
||||||
config: ScaffoldConfig,
|
|
||||||
{ templatePath, basePath }: { templatePath: string; basePath: string },
|
|
||||||
): Promise<void> {
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
try {
|
|
||||||
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(config, {
|
|
||||||
templatePath,
|
|
||||||
basePath,
|
|
||||||
})
|
|
||||||
const overwrite = getOptionValueForFile(config, inputPath, config.overwrite ?? false)
|
|
||||||
|
|
||||||
log(
|
|
||||||
config,
|
|
||||||
LogLevel.Debug,
|
|
||||||
`\nParsing ${templatePath}`,
|
|
||||||
`\nBase path: ${basePath}`,
|
|
||||||
`\nFull input path: ${inputPath}`,
|
|
||||||
`\nOutput Path Opt: ${outputPathOpt}`,
|
|
||||||
`\nFull output dir: ${outputDir}`,
|
|
||||||
`\nFull output path: ${outputPath}`,
|
|
||||||
`\n`,
|
|
||||||
)
|
|
||||||
|
|
||||||
await createDirIfNotExists(path.dirname(outputPath), config)
|
|
||||||
|
|
||||||
log(config, LogLevel.Info, `Writing to ${outputPath}`)
|
|
||||||
await copyFileTransformed(config, { exists, overwrite, outputPath, inputPath })
|
|
||||||
resolve()
|
|
||||||
} catch (e: any) {
|
|
||||||
handleErr(e)
|
|
||||||
reject(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Scaffold
|
export default Scaffold
|
||||||
|
|||||||
32
src/types.ts
32
src/types.ts
@@ -57,7 +57,7 @@ export interface ScaffoldConfig {
|
|||||||
* Enable to override output files, even if they already exist.
|
* Enable to override output files, even if they already exist.
|
||||||
*
|
*
|
||||||
* You may supply a function to this option, which can take the arguments `(fullPath, baseDir, baseName)` and returns
|
* 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.
|
* a boolean for each file.
|
||||||
*
|
*
|
||||||
* May also be a {@link FileResponseHandler} which returns a boolean value per file.
|
* May also be a {@link FileResponseHandler} which returns a boolean value per file.
|
||||||
*
|
*
|
||||||
@@ -325,6 +325,10 @@ export type FileResponse<T> = T | FileResponseHandler<T>
|
|||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export interface ScaffoldCmdConfig {
|
export interface ScaffoldCmdConfig {
|
||||||
|
/**
|
||||||
|
* Name to be passed to the generated files. `{{name}}` and `{{Name}}` inside contents and file names will be replaced
|
||||||
|
* accordingly.
|
||||||
|
*/
|
||||||
name: string
|
name: string
|
||||||
templates: string[]
|
templates: string[]
|
||||||
output: string
|
output: string
|
||||||
@@ -336,6 +340,7 @@ export interface ScaffoldCmdConfig {
|
|||||||
verbose: LogLevel
|
verbose: LogLevel
|
||||||
dryRun: boolean
|
dryRun: boolean
|
||||||
config?: string
|
config?: string
|
||||||
|
/** The key to use for the file which contains the template configurations. */
|
||||||
key?: string
|
key?: string
|
||||||
github?: string
|
github?: string
|
||||||
}
|
}
|
||||||
@@ -351,4 +356,27 @@ export interface ScaffoldCmdConfig {
|
|||||||
*
|
*
|
||||||
* @see {@link ScaffoldConfig}
|
* @see {@link ScaffoldConfig}
|
||||||
*/
|
*/
|
||||||
export type ScaffoldConfigFile = Record<string, ScaffoldConfig>
|
export type ScaffoldConfigMap = Record<string, ScaffoldConfig>
|
||||||
|
|
||||||
|
/** The scaffold config file is either:
|
||||||
|
* - A {@link ScaffoldConfigMap} object
|
||||||
|
* - A function that returns a {@link ScaffoldConfigMap} object
|
||||||
|
* - A promise that resolves to a {@link ScaffoldConfigMap} object
|
||||||
|
* - A function that returns a promise that resolves to a {@link ScaffoldConfigMap} object
|
||||||
|
*/
|
||||||
|
export type ScaffoldConfigFile = AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export type Resolver<T, R = T> = R | ((value: T) => R)
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export type AsyncResolver<T, R = T> = Resolver<T, Promise<R> | R>
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export type LogConfig = Pick<ScaffoldConfig, "quiet" | "verbose">
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export type ConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config">
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export type MinimalConfig = Pick<ScaffoldCmdConfig, "name" | "key">
|
||||||
|
|||||||
505
src/utils.ts
505
src/utils.ts
@@ -1,508 +1,9 @@
|
|||||||
import path from "path"
|
import { Resolver } from "./types"
|
||||||
import { F_OK } from "constants"
|
|
||||||
import {
|
|
||||||
DefaultHelpers,
|
|
||||||
FileResponse,
|
|
||||||
FileResponseHandler,
|
|
||||||
Helper,
|
|
||||||
LogLevel,
|
|
||||||
ScaffoldCmdConfig,
|
|
||||||
ScaffoldConfig,
|
|
||||||
ScaffoldConfigFile,
|
|
||||||
} from "./types"
|
|
||||||
import camelCase from "lodash/camelCase"
|
|
||||||
import snakeCase from "lodash/snakeCase"
|
|
||||||
import kebabCase from "lodash/kebabCase"
|
|
||||||
import startCase from "lodash/startCase"
|
|
||||||
import Handlebars from "handlebars"
|
|
||||||
import { promises as fsPromises } from "fs"
|
|
||||||
import chalk from "chalk"
|
|
||||||
const { stat, access, mkdir } = fsPromises
|
|
||||||
import dtAdd from "date-fns/add"
|
|
||||||
import dtFormat from "date-fns/format"
|
|
||||||
import dtParseISO from "date-fns/parseISO"
|
|
||||||
import { glob, hasMagic } from "glob"
|
|
||||||
import { OptionsBase } from "massarg/types"
|
|
||||||
import { spawn } from "node:child_process"
|
|
||||||
import os from "node:os"
|
|
||||||
|
|
||||||
const dateFns = {
|
|
||||||
add: dtAdd,
|
|
||||||
format: dtFormat,
|
|
||||||
parseISO: dtParseISO,
|
|
||||||
}
|
|
||||||
|
|
||||||
const { readFile, writeFile } = fsPromises
|
|
||||||
|
|
||||||
export const defaultHelpers: Record<DefaultHelpers, Helper> = {
|
|
||||||
camelCase,
|
|
||||||
snakeCase,
|
|
||||||
startCase,
|
|
||||||
kebabCase,
|
|
||||||
hyphenCase: kebabCase,
|
|
||||||
pascalCase,
|
|
||||||
lowerCase: (text) => text.toLowerCase(),
|
|
||||||
upperCase: (text) => text.toUpperCase(),
|
|
||||||
now: nowHelper,
|
|
||||||
date: dateHelper,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function _dateHelper(date: Date, formatString: string): string
|
|
||||||
export function _dateHelper(
|
|
||||||
date: Date,
|
|
||||||
formatString: string,
|
|
||||||
durationDifference: number,
|
|
||||||
durationType: keyof Duration,
|
|
||||||
): string
|
|
||||||
export function _dateHelper(
|
|
||||||
date: Date,
|
|
||||||
formatString: string,
|
|
||||||
durationDifference?: number,
|
|
||||||
durationType?: keyof Duration,
|
|
||||||
): string {
|
|
||||||
if (durationType && durationDifference !== undefined) {
|
|
||||||
return dateFns.format(dateFns.add(date, { [durationType]: durationDifference }), formatString)
|
|
||||||
}
|
|
||||||
return dateFns.format(date, formatString)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function nowHelper(formatString: string): string
|
|
||||||
export function nowHelper(formatString: string, durationDifference: number, durationType: keyof Duration): string
|
|
||||||
export function nowHelper(formatString: string, durationDifference?: number, durationType?: keyof Duration): string {
|
|
||||||
return _dateHelper(new Date(), formatString, durationDifference!, durationType!)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dateHelper(date: string, formatString: string): string
|
|
||||||
export function dateHelper(
|
|
||||||
date: string,
|
|
||||||
formatString: string,
|
|
||||||
durationDifference: number,
|
|
||||||
durationType: keyof Duration,
|
|
||||||
): string
|
|
||||||
|
|
||||||
export function dateHelper(
|
|
||||||
date: string,
|
|
||||||
formatString: string,
|
|
||||||
durationDifference?: number,
|
|
||||||
durationType?: keyof Duration,
|
|
||||||
): string {
|
|
||||||
return _dateHelper(dateFns.parseISO(date), formatString, durationDifference!, durationType!)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerHelpers(config: ScaffoldConfig): void {
|
|
||||||
const _helpers = { ...defaultHelpers, ...config.helpers }
|
|
||||||
for (const helperName in _helpers) {
|
|
||||||
log(config, LogLevel.Debug, `Registering helper: ${helperName}`)
|
|
||||||
Handlebars.registerHelper(helperName, _helpers[helperName as keyof typeof _helpers])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleErr(err: NodeJS.ErrnoException | null): void {
|
export function handleErr(err: NodeJS.ErrnoException | null): void {
|
||||||
if (err) throw err
|
if (err) throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
export function resolve<T, R = T>(resolver: Resolver<T, R>, arg: T): R {
|
||||||
export type LogConfig = Pick<ScaffoldConfig, "quiet" | "verbose">
|
return typeof resolver === "function" ? (resolver as (value: T) => R)(arg) : (resolver as R)
|
||||||
|
|
||||||
export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
|
|
||||||
if (config.quiet || config.verbose === LogLevel.None || level < (config.verbose ?? LogLevel.Info)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const levelColor: Record<LogLevel, keyof typeof chalk> = {
|
|
||||||
[LogLevel.None]: "reset",
|
|
||||||
[LogLevel.Debug]: "blue",
|
|
||||||
[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, config: ScaffoldConfig): Promise<void> {
|
|
||||||
const parentDir = path.dirname(dir)
|
|
||||||
|
|
||||||
if (!(await pathExists(parentDir))) {
|
|
||||||
await createDirIfNotExists(parentDir, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await pathExists(dir))) {
|
|
||||||
try {
|
|
||||||
log(config, LogLevel.Debug, `Creating dir ${dir}`)
|
|
||||||
await mkdir(dir)
|
|
||||||
return
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e.code !== "EEXIST") {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOptionValueForFile<T>(
|
|
||||||
config: ScaffoldConfig,
|
|
||||||
filePath: string,
|
|
||||||
fn: FileResponse<T>,
|
|
||||||
defaultValue?: T,
|
|
||||||
): T {
|
|
||||||
if (typeof fn !== "function") {
|
|
||||||
return defaultValue ?? (fn as T)
|
|
||||||
}
|
|
||||||
return (fn as FileResponseHandler<T>)(
|
|
||||||
filePath,
|
|
||||||
path.dirname(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
|
||||||
path.basename(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handlebarsParse(
|
|
||||||
config: ScaffoldConfig,
|
|
||||||
templateBuffer: Buffer | string,
|
|
||||||
{ isPath = false }: { isPath?: boolean } = {},
|
|
||||||
): Buffer {
|
|
||||||
const { data } = config
|
|
||||||
try {
|
|
||||||
let str = templateBuffer.toString()
|
|
||||||
if (isPath) {
|
|
||||||
str = str.replace(/\\/g, "/")
|
|
||||||
}
|
|
||||||
const parser = Handlebars.compile(str, { noEscape: true })
|
|
||||||
let outputContents = parser(data)
|
|
||||||
if (isPath && path.sep !== "/") {
|
|
||||||
outputContents = outputContents.replace(/\//g, "\\")
|
|
||||||
}
|
|
||||||
return Buffer.from(outputContents)
|
|
||||||
} catch (e) {
|
|
||||||
log(config, LogLevel.Debug, e)
|
|
||||||
log(config, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
|
|
||||||
return Buffer.from(templateBuffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function pathExists(filePath: string): Promise<boolean> {
|
|
||||||
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): string {
|
|
||||||
return template.replace(/\*/g, "").replace(/(\/\/|\\\\)/g, path.sep)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeRelativePath(str: string): string {
|
|
||||||
return str.startsWith(path.sep) ? str.slice(1) : str
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBasePath(relPath: string): string {
|
|
||||||
return path
|
|
||||||
.resolve(process.cwd(), relPath)
|
|
||||||
.replace(process.cwd() + path.sep, "")
|
|
||||||
.replace(process.cwd(), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getFileList(config: ScaffoldConfig, template: string): Promise<string[]> {
|
|
||||||
return (
|
|
||||||
await glob(template, {
|
|
||||||
dot: true,
|
|
||||||
nodir: true,
|
|
||||||
// debug: config.verbose === LogLevel.Debug,
|
|
||||||
})
|
|
||||||
).map((f) => f.replace(/\//g, path.sep))
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GlobInfo {
|
|
||||||
nonGlobTemplate: string
|
|
||||||
origTemplate: string
|
|
||||||
isDirOrGlob: boolean
|
|
||||||
isGlob: boolean
|
|
||||||
template: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getTemplateGlobInfo(config: ScaffoldConfig, template: string): Promise<GlobInfo> {
|
|
||||||
const isGlob = hasMagic(template)
|
|
||||||
log(config, LogLevel.Debug, "before isDir", "isGlob:", isGlob, template)
|
|
||||||
let _template = template
|
|
||||||
let nonGlobTemplate = isGlob ? removeGlob(template) : template
|
|
||||||
nonGlobTemplate = path.normalize(nonGlobTemplate)
|
|
||||||
const isDirOrGlob = isGlob ? true : await isDir(template)
|
|
||||||
log(config, LogLevel.Debug, "after isDir", isDirOrGlob)
|
|
||||||
const _shouldAddGlob = !isGlob && isDirOrGlob
|
|
||||||
const origTemplate = template
|
|
||||||
if (_shouldAddGlob) {
|
|
||||||
_template = path.join(template, "**", "*")
|
|
||||||
}
|
|
||||||
return { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template: _template }
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OutputFileInfo {
|
|
||||||
inputPath: string
|
|
||||||
outputPathOpt: string
|
|
||||||
outputDir: string
|
|
||||||
outputPath: string
|
|
||||||
exists: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getTemplateFileInfo(
|
|
||||||
config: ScaffoldConfig,
|
|
||||||
{ templatePath, basePath }: { templatePath: string; basePath: string },
|
|
||||||
): Promise<OutputFileInfo> {
|
|
||||||
const inputPath = path.resolve(process.cwd(), templatePath)
|
|
||||||
const outputPathOpt = getOptionValueForFile(config, inputPath, config.output)
|
|
||||||
const outputDir = getOutputDir(config, outputPathOpt, basePath)
|
|
||||||
const outputPath = handlebarsParse(config, path.join(outputDir, path.basename(inputPath)), {
|
|
||||||
isPath: true,
|
|
||||||
}).toString()
|
|
||||||
const exists = await pathExists(outputPath)
|
|
||||||
return { inputPath, outputPathOpt, outputDir, outputPath, exists }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function copyFileTransformed(
|
|
||||||
config: ScaffoldConfig,
|
|
||||||
{
|
|
||||||
exists,
|
|
||||||
overwrite,
|
|
||||||
outputPath,
|
|
||||||
inputPath,
|
|
||||||
}: {
|
|
||||||
exists: boolean
|
|
||||||
overwrite: boolean
|
|
||||||
outputPath: string
|
|
||||||
inputPath: string
|
|
||||||
},
|
|
||||||
): Promise<void> {
|
|
||||||
if (!exists || overwrite) {
|
|
||||||
if (exists && overwrite) {
|
|
||||||
log(config, LogLevel.Info, `File ${outputPath} exists, overwriting`)
|
|
||||||
}
|
|
||||||
const templateBuffer = await readFile(inputPath)
|
|
||||||
const unprocessedOutputContents = handlebarsParse(config, templateBuffer)
|
|
||||||
const finalOutputContents =
|
|
||||||
(await config.beforeWrite?.(unprocessedOutputContents, templateBuffer, outputPath)) ?? unprocessedOutputContents
|
|
||||||
|
|
||||||
if (!config.dryRun) {
|
|
||||||
await writeFile(outputPath, finalOutputContents)
|
|
||||||
log(config, LogLevel.Info, "Done.")
|
|
||||||
} else {
|
|
||||||
log(config, LogLevel.Info, "Content output:")
|
|
||||||
log(config, LogLevel.Info, finalOutputContents)
|
|
||||||
}
|
|
||||||
} else if (exists) {
|
|
||||||
log(config, LogLevel.Info, `File ${outputPath} already exists, skipping`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOutputDir(config: ScaffoldConfig, outputPathOpt: string, basePath: string): string {
|
|
||||||
return path.resolve(
|
|
||||||
process.cwd(),
|
|
||||||
...([
|
|
||||||
outputPathOpt,
|
|
||||||
basePath,
|
|
||||||
config.createSubFolder
|
|
||||||
? config.subFolderNameHelper
|
|
||||||
? handlebarsParse(config, `{{ ${config.subFolderNameHelper} name }}`).toString()
|
|
||||||
: config.name
|
|
||||||
: undefined,
|
|
||||||
].filter(Boolean) as string[]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logInputFile(
|
|
||||||
config: ScaffoldConfig,
|
|
||||||
{
|
|
||||||
origTemplate,
|
|
||||||
relPath,
|
|
||||||
template,
|
|
||||||
inputFilePath,
|
|
||||||
nonGlobTemplate,
|
|
||||||
basePath,
|
|
||||||
isDirOrGlob,
|
|
||||||
isGlob,
|
|
||||||
}: {
|
|
||||||
origTemplate: string
|
|
||||||
relPath: string
|
|
||||||
template: string
|
|
||||||
inputFilePath: string
|
|
||||||
nonGlobTemplate: string
|
|
||||||
basePath: string
|
|
||||||
isDirOrGlob: boolean
|
|
||||||
isGlob: boolean
|
|
||||||
},
|
|
||||||
): void {
|
|
||||||
log(
|
|
||||||
config,
|
|
||||||
LogLevel.Debug,
|
|
||||||
`\nprocess.cwd(): ${process.cwd()}`,
|
|
||||||
`\norigTemplate: ${origTemplate}`,
|
|
||||||
`\nrelPath: ${relPath}`,
|
|
||||||
`\ntemplate: ${template}`,
|
|
||||||
`\ninputFilePath: ${inputFilePath}`,
|
|
||||||
`\nnonGlobTemplate: ${nonGlobTemplate}`,
|
|
||||||
`\nbasePath: ${basePath}`,
|
|
||||||
`\nisDirOrGlob: ${isDirOrGlob}`,
|
|
||||||
`\nisGlob: ${isGlob}`,
|
|
||||||
`\n`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logInitStep(config: ScaffoldConfig): void {
|
|
||||||
log(config, LogLevel.Debug, "Full config:", {
|
|
||||||
name: config.name,
|
|
||||||
templates: config.templates,
|
|
||||||
output: config.output,
|
|
||||||
createSubFolder: config.createSubFolder,
|
|
||||||
data: config.data,
|
|
||||||
overwrite: config.overwrite,
|
|
||||||
quiet: config.quiet,
|
|
||||||
subFolderNameHelper: config.subFolderNameHelper,
|
|
||||||
helpers: Object.keys(config.helpers ?? {}),
|
|
||||||
verbose: `${config.verbose} (${Object.keys(LogLevel).find(
|
|
||||||
(k) => (LogLevel[k as any] as unknown as number) === config.verbose!,
|
|
||||||
)})`,
|
|
||||||
dryRun: config.dryRun,
|
|
||||||
beforeWrite: config.beforeWrite,
|
|
||||||
} as Record<keyof ScaffoldConfig, unknown>)
|
|
||||||
log(config, LogLevel.Info, "Data:", config.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseAppendData(value: string, options: ScaffoldCmdConfig & OptionsBase): unknown {
|
|
||||||
const data = options.data ?? {}
|
|
||||||
const [key, val] = value.split(/\:?=/)
|
|
||||||
// raw
|
|
||||||
if (value.includes(":=") && !val.includes(":=")) {
|
|
||||||
return { ...data, [key]: JSON.parse(val) }
|
|
||||||
}
|
|
||||||
return { ...data, [key]: isWrappedWithQuotes(val) ? val.substring(1, val.length - 1) : val }
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWrappedWithQuotes(string: string): boolean {
|
|
||||||
return (string.startsWith('"') && string.endsWith('"')) || (string.startsWith("'") && string.endsWith("'"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export async function parseConfig(config: ScaffoldCmdConfig & OptionsBase): Promise<ScaffoldConfig> {
|
|
||||||
let c: ScaffoldConfig = config
|
|
||||||
if (config.github) {
|
|
||||||
log(config, LogLevel.Info, `Loading config from github ${config.github}`)
|
|
||||||
const gitUrl = new URL(`https://github.com/${config.github}`)
|
|
||||||
if (!gitUrl.pathname.endsWith(".git")) {
|
|
||||||
gitUrl.pathname += ".git"
|
|
||||||
}
|
|
||||||
config.config = gitUrl.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.config) {
|
|
||||||
const isUrl = config.config.includes("://")
|
|
||||||
|
|
||||||
const hasColonToken = (!isUrl && config.config.includes(":")) || (isUrl && count(config.config, ":") > 1)
|
|
||||||
const colonIndex = config.config.lastIndexOf(":")
|
|
||||||
const [configFile, templateKey = "default"] = hasColonToken
|
|
||||||
? [config.config.substring(0, colonIndex), config.config.substring(colonIndex + 1)]
|
|
||||||
: [config.config, undefined]
|
|
||||||
const key = (config.key ?? templateKey) || "default"
|
|
||||||
log(config, LogLevel.Info, `Loading config from ${configFile} with key ${key}`)
|
|
||||||
const configImport = await getConfig({ config: configFile, quiet: config.quiet, verbose: config.verbose })
|
|
||||||
if (!configImport[key]) {
|
|
||||||
throw new Error(`Template "${key}" not found in ${configFile}`)
|
|
||||||
}
|
|
||||||
c = {
|
|
||||||
...config,
|
|
||||||
...configImport[key],
|
|
||||||
data: {
|
|
||||||
...configImport[key].data,
|
|
||||||
...config.data,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.data = { ...c.data, ...config.appendData }
|
|
||||||
delete config.appendData
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export async function getConfig(
|
|
||||||
config: Pick<ScaffoldCmdConfig, "quiet" | "verbose" | "config">,
|
|
||||||
): Promise<ScaffoldConfigFile> {
|
|
||||||
const { config: configFile, ...logConfig } = config as Required<typeof config>
|
|
||||||
const url = new URL(configFile)
|
|
||||||
|
|
||||||
if (url.protocol === "file:") {
|
|
||||||
log(logConfig, LogLevel.Info, `Loading config from file ${configFile}`)
|
|
||||||
const absolutePath = path.resolve(process.cwd(), configFile)
|
|
||||||
return import(absolutePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isHttp = url.protocol === "http:" || url.protocol === "https:"
|
|
||||||
const isGit = url.protocol === "git:" || (isHttp && url.pathname.endsWith(".git"))
|
|
||||||
|
|
||||||
if (isHttp || isGit) {
|
|
||||||
if (isGit) {
|
|
||||||
const repoUrl = `${url.protocol}//${url.host}${url.pathname}`
|
|
||||||
log(logConfig, LogLevel.Info, `Cloning git repo ${repoUrl}`)
|
|
||||||
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const clone = spawn("git", ["clone", "--depth", "1", repoUrl, tmpPath])
|
|
||||||
|
|
||||||
clone.on("error", reject)
|
|
||||||
clone.on("close", async (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
log(logConfig, LogLevel.Info, `Loading config from git repo: ${repoUrl}`)
|
|
||||||
const hashPath = url.hash?.replace("#", "") || "scaffold.config.js"
|
|
||||||
const absolutePath = path.resolve(tmpPath, hashPath)
|
|
||||||
const loadedConfig = (await import(absolutePath)).default as ScaffoldConfigFile
|
|
||||||
log(logConfig, LogLevel.Info, `Loaded config from git`)
|
|
||||||
log(logConfig, LogLevel.Debug, `Raw config:`, loadedConfig)
|
|
||||||
const fixedConfig: ScaffoldConfigFile = Object.fromEntries(
|
|
||||||
Object.entries(loadedConfig).map(([k, v]) => [
|
|
||||||
k,
|
|
||||||
// use absolute paths for template as config is necessarily in another directory
|
|
||||||
{ ...v, templates: v.templates.map((t) => path.resolve(tmpPath, t)) },
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
|
|
||||||
resolve(fixedConfig)
|
|
||||||
} else {
|
|
||||||
reject(new Error(`Git clone failed with code ${code}`))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unsupported protocol ${url.protocol}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return import(path.resolve(process.cwd(), configFile))
|
|
||||||
}
|
|
||||||
|
|
||||||
function count(string: string, substring: string): number {
|
|
||||||
return string.split(substring).length - 1
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import FileSystem from "mock-fs/lib/filesystem"
|
|||||||
import Scaffold from "../src/scaffold"
|
import Scaffold from "../src/scaffold"
|
||||||
import { readdirSync, readFileSync } from "fs"
|
import { readdirSync, readFileSync } from "fs"
|
||||||
import { Console } from "console"
|
import { Console } from "console"
|
||||||
import { defaultHelpers } from "../src/utils"
|
import { defaultHelpers } from "../src/parser"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import * as dateFns from "date-fns"
|
import * as dateFns from "date-fns"
|
||||||
import crypto from "crypto"
|
import crypto from "crypto"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { dateHelper, handlebarsParse, nowHelper, parseAppendData } from "../src/utils"
|
|
||||||
import { ScaffoldCmdConfig, ScaffoldConfig } from "../src/types"
|
import { ScaffoldCmdConfig, ScaffoldConfig } from "../src/types"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import * as dateFns from "date-fns"
|
import * as dateFns from "date-fns"
|
||||||
import { OptionsBase } from "massarg/types"
|
import { OptionsBase } from "massarg/types"
|
||||||
|
import { dateHelper, handlebarsParse, nowHelper } from "../src/parser"
|
||||||
|
import { parseAppendData } from "../src/config"
|
||||||
|
|
||||||
const blankConf: ScaffoldConfig = {
|
const blankConf: ScaffoldConfig = {
|
||||||
verbose: 0,
|
verbose: 0,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2019",
|
"target": "ES2022",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"lib": ["ES2019"],
|
"lib": ["ES2022"],
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user