mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-18 01:29:09 +00:00
Compare commits
26 Commits
v2.0.0-pre
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
561477f07b | ||
|
|
bfcc59c20a | ||
|
|
ba5332d550 | ||
|
|
f9bc0419f3 | ||
|
|
381c55835f | ||
|
|
efdb7dc00b | ||
|
|
385829aa27 | ||
|
|
33c357bccc | ||
|
|
b74b781a5b | ||
|
|
57410c8d74 | ||
|
|
f81cfd8ae1 | ||
|
|
414494734d | ||
|
|
89b588f64e | ||
|
|
cf22e2e62f | ||
|
|
246c139061 | ||
|
|
711d8f0333 | ||
|
|
8b22e96329 | ||
|
|
53c0842ab8 | ||
|
|
48631325c1 | ||
|
|
045ad0118a | ||
|
|
b4dca7a954 | ||
|
|
7c42808f63 | ||
|
|
fd42013e8b | ||
|
|
961a72fcdc | ||
|
|
d6d99cfdf2 | ||
|
|
ea4ecabe02 |
10
.github/workflows/alpha.yml
vendored
10
.github/workflows/alpha.yml
vendored
@@ -13,8 +13,12 @@ jobs:
|
||||
with:
|
||||
node-version: "12.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn pack --filename=release.tgz
|
||||
- run: yarn test
|
||||
- run: yarn build
|
||||
- run: cd ./dist && yarn pack --filename=../package.tgz
|
||||
if: "!contains(github.event.head_commit.message, '[skip publish]')"
|
||||
- uses: Klemensas/action-autotag@stable
|
||||
if: "!contains(github.event.head_commit.message, '[skip publish]')"
|
||||
id: update_tag
|
||||
with:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -26,7 +30,7 @@ jobs:
|
||||
package: ./dist/package.json
|
||||
token: "${{ secrets.NPM_TOKEN }}"
|
||||
- name: Create Release
|
||||
if: "steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')"
|
||||
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
|
||||
uses: actions/create-release@v1
|
||||
id: create_release
|
||||
env:
|
||||
@@ -35,7 +39,7 @@ jobs:
|
||||
tag_name: ${{ steps.update_tag.outputs.tagname }}
|
||||
release_name: Release ${{ steps.update_tag.outputs.tagname }}
|
||||
- name: Upload Release Asset
|
||||
if: "steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')"
|
||||
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
|
||||
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
@@ -13,8 +13,12 @@ jobs:
|
||||
with:
|
||||
node-version: "12.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn pack --filename=release.tgz
|
||||
- run: yarn test
|
||||
- run: yarn build
|
||||
- run: cd ./dist && yarn pack --filename=../package.tgz
|
||||
if: "!contains(github.event.head_commit.message, '[skip publish]')"
|
||||
- uses: Klemensas/action-autotag@stable
|
||||
if: "!contains(github.event.head_commit.message, '[skip publish]')"
|
||||
id: update_tag
|
||||
with:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -22,10 +26,10 @@ jobs:
|
||||
- name: Publish on NPM
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
package: dist/package.json
|
||||
package: ./dist/package.json
|
||||
token: "${{ secrets.NPM_TOKEN }}"
|
||||
- name: Create Release
|
||||
if: steps.update_tag.outputs.tagname
|
||||
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
|
||||
uses: actions/create-release@v1
|
||||
id: create_release
|
||||
env:
|
||||
@@ -34,7 +38,7 @@ jobs:
|
||||
tag_name: ${{ steps.update_tag.outputs.tagname }}
|
||||
release_name: Release ${{ steps.update_tag.outputs.tagname }}
|
||||
- name: Upload Release Asset
|
||||
if: steps.update_tag.outputs.tagname
|
||||
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -59,3 +59,5 @@ typings/
|
||||
|
||||
examples/test-output/**/*
|
||||
dist/
|
||||
.DS_Store
|
||||
tmp/
|
||||
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -2,6 +2,11 @@
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
"npm.packageManager": "yarn",
|
||||
"cSpell.words": [
|
||||
"massarg"
|
||||
"massarg",
|
||||
"nodir",
|
||||
"nobrace",
|
||||
"noext",
|
||||
"nocomment",
|
||||
"nonegate"
|
||||
]
|
||||
}
|
||||
|
||||
118
README.md
118
README.md
@@ -2,6 +2,16 @@
|
||||
|
||||
Simple Scaffold allows you to create your structured files based on templates.
|
||||
|
||||
Simply organize your commonly-created files in their original structure, and replace any variable
|
||||
values (such as component or app name) inside the paths or contents of the files with tokens to be
|
||||
populated upon scaffolding.
|
||||
|
||||
Then, run Simple Scaffold and it will generate your files for you in the desired structure,
|
||||
with file names and contents that contain your dynamic information.
|
||||
|
||||
It's a simple way to easily create reusable components, common class files to start writing from,
|
||||
or even entire app structures.
|
||||
|
||||
## Install
|
||||
|
||||
You can either use it as a command line tool or import into your own code and run from there.
|
||||
@@ -22,6 +32,8 @@ npx simple-scaffold <...args>
|
||||
```plaintext
|
||||
Usage: simple-scaffold [options]
|
||||
|
||||
Create structured files based on templates.
|
||||
|
||||
Options:
|
||||
|
||||
--help|-h Display help information
|
||||
@@ -35,7 +47,8 @@ Options:
|
||||
|
||||
--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.
|
||||
pattern for multiple file matching easily. (default:
|
||||
)
|
||||
|
||||
--overwrite|-w Enable to override output files, even if they already exist.
|
||||
(default: false)
|
||||
@@ -46,19 +59,27 @@ Options:
|
||||
--create-sub-folder|-s Create subfolder with the input name (default:
|
||||
false)
|
||||
|
||||
--quiet|-q Suppress output logs (default:
|
||||
false)
|
||||
--quiet|-q Suppress output logs (Same as --verbose 0)
|
||||
(default: false)
|
||||
|
||||
--dry-run|-dr Don't emit actual files. This is good for testing your
|
||||
scaffolds and making sure they don't fail, without having to write
|
||||
actual files. (default: false)
|
||||
--verbose|-v Determine amount of logs to display. The values are: 0
|
||||
(none) | 1 (debug) | 2 (info) | 3 (warn) | 4 (error). The
|
||||
provided level will display messages of the same level or higher.
|
||||
(default: 2)
|
||||
|
||||
--dry-run|-dr Don't emit files. This is good for testing your scaffolds and
|
||||
making sure they don't fail, without having to write actual file
|
||||
contents or create directories. (default:
|
||||
false)
|
||||
```
|
||||
|
||||
You can also add this as a script in your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
"scripts": {
|
||||
...
|
||||
"scaffold": "yarn simple-scaffold --templates scaffolds/component/**/* --output src/components --data '{\"myProp\": \"propName\", \"myVal\": \"123\"}'"
|
||||
}
|
||||
}
|
||||
@@ -70,10 +91,10 @@ You can also build the scaffold yourself, if you want to create more complex arg
|
||||
Simply pass a config object to the constructor, and invoke `run()` when you are ready to start.
|
||||
The config takes similar arguments to the command line:
|
||||
|
||||
```javascript
|
||||
const SimpleScaffold = require("simple-scaffold").default
|
||||
```typescript
|
||||
import Scaffold from "simple-scaffold"
|
||||
|
||||
const scaffold = new SimpleScaffold({
|
||||
const scaffold = SimpleScaffold({
|
||||
name: "component",
|
||||
templates: [path.join(__dirname, "scaffolds", "component")],
|
||||
output: path.join(__dirname, "src", "components"),
|
||||
@@ -81,16 +102,16 @@ const scaffold = new SimpleScaffold({
|
||||
locals: {
|
||||
property: "value",
|
||||
},
|
||||
}).run()
|
||||
})
|
||||
```
|
||||
|
||||
The exception in the config is that `output`, when used in Node directly, may also be passed a
|
||||
function for each input file to output into a dynamic path:
|
||||
|
||||
```javascript
|
||||
```typescript
|
||||
config.output = (fullPath, baseDir, baseName) => {
|
||||
console.log({ fullPath, baseDir, baseName })
|
||||
return [baseDir, baseName].join(path.sep)
|
||||
return path.resolve(baseDir, baseName)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -118,23 +139,25 @@ Your `data` will be pre-populated with the following:
|
||||
> Any `data` you add in the config will be available for use with their names wrapped in
|
||||
> `{{` and `}}`.
|
||||
|
||||
#### Helpers
|
||||
|
||||
Simple-Scaffold provides some built-in text transformation filters usable by handleBars.
|
||||
|
||||
For example, you may use `{{ name | snakeCase }}` inside a template file or filename, and it will
|
||||
For example, you may use `{{ snakeCase name }}` inside a template file or filename, and it will
|
||||
replace `My Name` with `my_name` when producing the final value.
|
||||
|
||||
Here are the built-in helpers available for use:
|
||||
|
||||
```plaintext
|
||||
{{ name | camelCase }} => myName
|
||||
{{ name | snakeCase }} => my_name
|
||||
{{ name | startCase }} => My Name
|
||||
{{ name | kebabCase }} => my-name
|
||||
{{ name | hyphenCase }} => my-name
|
||||
{{ name | pascalCase }} => MyName
|
||||
```
|
||||
| Helper name | Example code | Example output |
|
||||
| ----------- | ----------------------- | -------------- |
|
||||
| camelCase | `{{ camelCase name }}` | myName |
|
||||
| snakeCase | `{{ snakeCase name }}` | my_name |
|
||||
| startCase | `{{ startCase name }}` | My Name |
|
||||
| kebabCase | `{{ kebabCase name }}` | my-name |
|
||||
| hyphenCase | `{{ hyphenCase name }}` | my-name |
|
||||
| pascalCase | `{{ pascalCase name }}` | MyName |
|
||||
|
||||
**Note:** These helpers are available for any data property, not exclusive to `name`.
|
||||
> These helpers are available for any data property, not exclusive to `name`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -144,7 +167,8 @@ Here are the built-in helpers available for use:
|
||||
simple-scaffold MyComponent \
|
||||
-t project/scaffold/**/* \
|
||||
-o src/components \
|
||||
-d '{"className":"myClassName"}'
|
||||
-d '{"className": "myClassName"}'
|
||||
MyComponent
|
||||
```
|
||||
|
||||
### Example Scaffold Input
|
||||
@@ -160,15 +184,15 @@ simple-scaffold MyComponent \
|
||||
- ...
|
||||
```
|
||||
|
||||
#### Contents of `project/scaffold/{{Name}}.js`
|
||||
#### Contents of `project/scaffold/{{Name}}.jsx`
|
||||
|
||||
```js
|
||||
const React = require('react')
|
||||
|
||||
module.exports = class {{Name}} extends React.Component {
|
||||
render() {
|
||||
module.exports = function {{Name}}(props) {
|
||||
return (
|
||||
<div className="{{className}}">{{Name}} Component</div>
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -185,7 +209,7 @@ module.exports = class {{Name}} extends React.Component {
|
||||
- ...
|
||||
```
|
||||
|
||||
With `createSubfolder = false`:
|
||||
With `createSubFolder = false`:
|
||||
|
||||
```plaintext
|
||||
- project
|
||||
@@ -195,14 +219,42 @@ With `createSubfolder = false`:
|
||||
- ...
|
||||
```
|
||||
|
||||
#### Contents of `project/scaffold/MyComponent/MyComponent.js`
|
||||
#### Contents of `project/scaffold/MyComponent/MyComponent.jsx`
|
||||
|
||||
```js
|
||||
const React = require("react")
|
||||
|
||||
module.exports = class MyComponent extends React.Component {
|
||||
render() {
|
||||
<div className="my-component">MyComponent Component</div>
|
||||
}
|
||||
module.exports = function MyComponent(props) {
|
||||
return (
|
||||
<div className="myClassName">MyComponent Component</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
I welcome any issues or pull requests on GitHub. If you find a bug, or would like a new feature,
|
||||
don't hesitate to open an appropriate issue and I will do my best to reply promptly.
|
||||
|
||||
If you are a developer and want to contribute code, here are some starting tips:
|
||||
|
||||
1. Fork this repository
|
||||
2. Run `yarn install`
|
||||
3. Run `yarn dev` to start file watch mode
|
||||
4. Make any changes you would like
|
||||
5. Create tests for your changes
|
||||
6. Update the relevant documentation (readme, code comments, type comments)
|
||||
7. Create a PR on upstream
|
||||
|
||||
Some tips on getting around the code:
|
||||
|
||||
- Use `yarn dev` for development - it runs TypeScript compile in watch mode, allowing you to make
|
||||
changes and immediately be able to try them using `yarn cmd`.
|
||||
- Use `yarn build` to build the output
|
||||
- Use `yarn test` to run tests
|
||||
- Use `yarn cmd` to use the CLI feature of Simple Scaffold from within the root directory,
|
||||
enabling you to test different behaviors. See `yarn cmd -h` for more information.
|
||||
|
||||
> This requires an updated build, and does not trigger one itself. Either use `yarn dev` or
|
||||
> `yarn build` before running this, or use `yarn build-cmd` instead, which triggers a build right
|
||||
> before running the command with the rest of the given arguments.
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
{
|
||||
"name": "simple-scaffold",
|
||||
"version": "1.0.0-alpha.1",
|
||||
"version": "1.0.0-alpha.12",
|
||||
"description": "Create files based on templates",
|
||||
"repository": "https://github.com/chenasraf/simple-scaffold.git",
|
||||
"author": "Chen Asraf <inbox@casraf.com>",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"bin": "cmd.js",
|
||||
"types": "index.d.ts",
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist/",
|
||||
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./dist/package.json",
|
||||
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./dist/ && cp ./README.md ./dist/",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/scaffold.js",
|
||||
"test": "jest --verbose",
|
||||
@@ -20,10 +19,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"args": "^5.0.1",
|
||||
"chalk": "^4.1.2",
|
||||
"glob": "^7.1.3",
|
||||
"handlebars": "^4.7.7",
|
||||
"lodash": "^4.17.21",
|
||||
"massarg": "^0.1.2",
|
||||
"massarg": "^1.0.4",
|
||||
"util.promisify": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
71
src/cmd.ts
71
src/cmd.ts
@@ -1,69 +1,2 @@
|
||||
import Scaffold from "./scaffold"
|
||||
import massarg from "massarg"
|
||||
import { ScaffoldCmdConfig } from "./types"
|
||||
|
||||
massarg<ScaffoldCmdConfig & { help: boolean; extras: string[] }>()
|
||||
.main(Scaffold)
|
||||
.option({
|
||||
name: "name",
|
||||
aliases: ["n"],
|
||||
isDefault: true,
|
||||
description:
|
||||
"Name to be passed to the generated files. {{name}} and {{Name}} inside contents and file names will be replaced accordingly.",
|
||||
})
|
||||
.option({
|
||||
name: "output",
|
||||
aliases: ["o"],
|
||||
description:
|
||||
"Path to output to. If --create-sub-folder is enabled, the subfolder will be created inside this path.",
|
||||
})
|
||||
.option({
|
||||
name: "templates",
|
||||
aliases: ["t"],
|
||||
description:
|
||||
"Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path, " +
|
||||
"or a glob pattern for multiple file matching easily.",
|
||||
defaultValue: [],
|
||||
array: true,
|
||||
})
|
||||
.option({
|
||||
aliases: ["w"],
|
||||
name: "overwrite",
|
||||
description: "Enable to override output files, even if they already exist.",
|
||||
defaultValue: false,
|
||||
boolean: true,
|
||||
})
|
||||
.option({
|
||||
aliases: ["d"],
|
||||
name: "data",
|
||||
description: "Add custom data to the templates. By default, only your app name is included.",
|
||||
parse: (v) => JSON.parse(v),
|
||||
})
|
||||
.option({
|
||||
aliases: ["s"],
|
||||
name: "create-sub-folder",
|
||||
description: "Create subfolder with the input name",
|
||||
defaultValue: false,
|
||||
boolean: true,
|
||||
})
|
||||
.option({ aliases: ["q"], name: "quiet", description: "Suppress output logs", defaultValue: false, boolean: true })
|
||||
.option({
|
||||
aliases: ["dr"],
|
||||
name: "dry-run",
|
||||
description:
|
||||
"Don't emit files. This is good for testing your scaffolds and making sure they " +
|
||||
"don't fail, without having to write actual file contents or create directories.",
|
||||
defaultValue: false,
|
||||
boolean: true,
|
||||
})
|
||||
// .example({
|
||||
// input: `yarn cmd -t examples/test-input/Component -o examples/test-output -d '{"property":"myProp","value":"10"}'`,
|
||||
// description: "Usage",
|
||||
// output: "",
|
||||
// })
|
||||
.help({
|
||||
binName: "simple-scaffold",
|
||||
useGlobalColumns: true,
|
||||
usageExample: "[options]",
|
||||
})
|
||||
.parse()
|
||||
import { parseCliArgs } from "./cmd_util"
|
||||
parseCliArgs()
|
||||
|
||||
91
src/cmd_util.ts
Normal file
91
src/cmd_util.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import massarg from "massarg"
|
||||
import chalk from "chalk"
|
||||
import { LogLevel, ScaffoldCmdConfig } from "./types"
|
||||
import { Scaffold } from "./scaffold"
|
||||
|
||||
export function parseCliArgs(args = process.argv.slice(2)) {
|
||||
return (
|
||||
massarg<ScaffoldCmdConfig & { help: boolean; extras: string[] }>()
|
||||
.main(Scaffold)
|
||||
.option({
|
||||
name: "name",
|
||||
aliases: ["n"],
|
||||
description:
|
||||
"Name to be passed to the generated files. {{name}} and {{Name}} inside contents and file names will be replaced accordingly.",
|
||||
isDefault: true,
|
||||
required: true,
|
||||
})
|
||||
.option({
|
||||
name: "output",
|
||||
aliases: ["o"],
|
||||
description:
|
||||
"Path to output to. If --create-sub-folder is enabled, the subfolder will be created inside this path.",
|
||||
required: true,
|
||||
})
|
||||
.option({
|
||||
name: "templates",
|
||||
aliases: ["t"],
|
||||
array: true,
|
||||
description:
|
||||
"Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path, " +
|
||||
"or a glob pattern for multiple file matching easily.",
|
||||
required: true,
|
||||
})
|
||||
.option({
|
||||
name: "overwrite",
|
||||
aliases: ["w"],
|
||||
boolean: true,
|
||||
defaultValue: false,
|
||||
description: "Enable to override output files, even if they already exist.",
|
||||
})
|
||||
.option({
|
||||
name: "data",
|
||||
aliases: ["d"],
|
||||
description: "Add custom data to the templates. By default, only your app name is included.",
|
||||
parse: (v) => JSON.parse(v),
|
||||
})
|
||||
.option({
|
||||
name: "create-sub-folder",
|
||||
aliases: ["s"],
|
||||
boolean: true,
|
||||
defaultValue: false,
|
||||
description: "Create subfolder with the input name",
|
||||
})
|
||||
.option({
|
||||
name: "quiet",
|
||||
aliases: ["q"],
|
||||
boolean: true,
|
||||
defaultValue: false,
|
||||
description: "Suppress output logs (Same as --verbose 0)",
|
||||
})
|
||||
.option({
|
||||
name: "verbose",
|
||||
aliases: ["v"],
|
||||
defaultValue: LogLevel.Info,
|
||||
description: `Determine amount of logs to display. The values are: ${chalk.bold`0 (none) | 1 (debug) | 2 (info) | 3 (warn) | 4 (error)`}. The provided level will display messages of the same level or higher.`,
|
||||
parse: Number,
|
||||
})
|
||||
.option({
|
||||
name: "dry-run",
|
||||
aliases: ["dr"],
|
||||
boolean: true,
|
||||
defaultValue: false,
|
||||
description:
|
||||
"Don't emit files. This is good for testing your scaffolds and making sure they " +
|
||||
"don't fail, without having to write actual file contents or create directories.",
|
||||
})
|
||||
// .example({
|
||||
// input: `yarn cmd -t examples/test-input/Component -o examples/test-output -d '{"property":"myProp","value":"10"}'`,
|
||||
// description: "Usage",
|
||||
// output: "",
|
||||
// })
|
||||
.help({
|
||||
binName: "simple-scaffold",
|
||||
useGlobalColumns: true,
|
||||
usageExample: "[options]",
|
||||
header: "Create structured files based on templates.",
|
||||
footer: `Copyright © Chen Asraf 2021\nNPM: ${chalk.underline`https://npmjs.com/package/massarg`}\nGitHub: ${chalk.underline`https://github.com/chenasraf/massarg`}`,
|
||||
})
|
||||
.parse(args)
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./scaffold"
|
||||
export * from "./types"
|
||||
import Scaffold from "./scaffold"
|
||||
export default Scaffold
|
||||
|
||||
103
src/scaffold.ts
103
src/scaffold.ts
@@ -13,14 +13,16 @@ import {
|
||||
pathExists,
|
||||
pascalCase,
|
||||
isDir,
|
||||
removeGlob,
|
||||
makeRelativePath,
|
||||
} from "./utils"
|
||||
import { ScaffoldConfig } from "./types"
|
||||
import { LogLevel, ScaffoldConfig } from "./types"
|
||||
|
||||
export async function Scaffold(config: ScaffoldConfig) {
|
||||
const options = { ...config }
|
||||
try {
|
||||
const options = { ...config }
|
||||
const data = { name: options.name, Name: pascalCase(options.name), ...options.data }
|
||||
log(options, "Config:", {
|
||||
log(options, LogLevel.Debug, "Full config:", {
|
||||
name: options.name,
|
||||
templates: options.templates,
|
||||
output: options.output,
|
||||
@@ -28,21 +30,63 @@ export async function Scaffold(config: ScaffoldConfig) {
|
||||
data: options.data,
|
||||
overwrite: options.overwrite,
|
||||
quiet: options.quiet,
|
||||
verbose: `${options.verbose} (${Object.keys(LogLevel).find(
|
||||
(k) => (LogLevel[k as any] as unknown as number) === options.verbose!
|
||||
)})`,
|
||||
})
|
||||
log(options, "Data:", data)
|
||||
log(options, LogLevel.Info, "Data:", data)
|
||||
for (let template of config.templates) {
|
||||
try {
|
||||
const _isDir = await isDir(template)
|
||||
const basePath = path
|
||||
.resolve(process.cwd(), _isDir ? template : path.dirname(template.replace("*", "").replace("//", "/")))
|
||||
.replace(process.cwd(), ".")
|
||||
if (_isDir) {
|
||||
const _isGlob = template.includes("*")
|
||||
if (!_isGlob && !(await pathExists(template))) {
|
||||
const err: NodeJS.ErrnoException = new Error(`ENOENT, no such file or directory ${template}`)
|
||||
err.code = "ENOENT"
|
||||
err.path = "non-existing-input"
|
||||
err.errno = -2
|
||||
throw err
|
||||
}
|
||||
const _nonGlobTemplate = _isGlob ? removeGlob(template) : template
|
||||
log(options, LogLevel.Debug, "before isDir", "isGlob:", _isGlob, template)
|
||||
const _isDir = _isGlob ? true : await isDir(template)
|
||||
log(options, LogLevel.Debug, "after isDir", _isDir)
|
||||
const _shouldAddGlob = !_isGlob && _isDir
|
||||
const origTemplate = template
|
||||
if (_shouldAddGlob) {
|
||||
template = template + "/**/*"
|
||||
}
|
||||
const files = await promisify(glob)(template, { dot: true, debug: false })
|
||||
for (const templatePath of files) {
|
||||
if (!(await isDir(templatePath))) {
|
||||
await handleTemplateFile(templatePath, basePath, options, data)
|
||||
log(options, LogLevel.Debug, "before glob")
|
||||
const files = await promisify(glob)(template, {
|
||||
dot: true,
|
||||
debug: false,
|
||||
nodir: options.verbose === LogLevel.Debug,
|
||||
nobrace: true,
|
||||
noext: true,
|
||||
nocomment: true,
|
||||
nonegate: true,
|
||||
})
|
||||
log(options, LogLevel.Debug, "after glob")
|
||||
for (const inputFilePath of files) {
|
||||
if (!(await isDir(inputFilePath))) {
|
||||
const relPath = makeRelativePath(path.dirname(removeGlob(inputFilePath).replace(_nonGlobTemplate, "")))
|
||||
const basePath = path
|
||||
.resolve(process.cwd(), relPath)
|
||||
.replace(process.cwd() + "/", "")
|
||||
.replace(process.cwd(), "")
|
||||
log(
|
||||
options,
|
||||
LogLevel.Debug,
|
||||
`\nprocess.cwd(): ${process.cwd()}`,
|
||||
`\norigTemplate: ${origTemplate}`,
|
||||
`\nrelPath: ${relPath}`,
|
||||
`\ntemplate: ${template}`,
|
||||
`\ninputFilePath: ${inputFilePath}`,
|
||||
`\nnonGlobTemplate: ${_nonGlobTemplate}`,
|
||||
`\nbasePath: ${basePath}`,
|
||||
`\nisDir: ${_isDir}`,
|
||||
`\nisGlob: ${_isGlob}`,
|
||||
`\n`
|
||||
)
|
||||
await handleTemplateFile(inputFilePath, basePath, options, data)
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -50,7 +94,7 @@ export async function Scaffold(config: ScaffoldConfig) {
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
log(options, LogLevel.Error, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -63,35 +107,46 @@ async function handleTemplateFile(
|
||||
): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
log(options, `Parsing ${templatePath}`)
|
||||
const inputPath = path.join(process.cwd(), templatePath)
|
||||
const inputPath = path.resolve(process.cwd(), templatePath)
|
||||
const outputPathOpt = getOptionValueForFile(inputPath, data, options.output)
|
||||
const outputDir = path.resolve(
|
||||
process.cwd(),
|
||||
...([outputPathOpt, options.createSubFolder ? options.name : undefined].filter(Boolean) as string[])
|
||||
...([outputPathOpt, basePath, options.createSubFolder ? options.name : undefined].filter(Boolean) as string[])
|
||||
)
|
||||
const outputPath = handlebarsParse(path.join(outputDir, path.basename(inputPath)), data)
|
||||
log(
|
||||
options,
|
||||
LogLevel.Debug,
|
||||
`\nParsing ${templatePath}`,
|
||||
`\nBase path: ${basePath}`,
|
||||
`\nFull input path: ${inputPath}`,
|
||||
`\nOutput Path Opt: ${outputPathOpt}`,
|
||||
`\nFull output dir: ${outputDir}`,
|
||||
`\nFull output path: ${outputPath}`,
|
||||
`\n`
|
||||
)
|
||||
const outputPath = path.join(outputDir, handlebarsParse(path.basename(inputPath), data))
|
||||
const overwrite = getOptionValueForFile(inputPath, data, options.overwrite ?? false)
|
||||
const exists = await pathExists(outputPath)
|
||||
|
||||
await createDirIfNotExists(outputDir, options)
|
||||
await createDirIfNotExists(path.dirname(outputPath), options)
|
||||
|
||||
log(options, `Writing to ${outputPath}`)
|
||||
log(options, LogLevel.Info, `Writing to ${outputPath}`)
|
||||
if (!exists || overwrite) {
|
||||
if (exists && overwrite) {
|
||||
log(options, `File ${outputPath} exists, overwriting`)
|
||||
log(options, LogLevel.Info, `File ${outputPath} exists, overwriting`)
|
||||
}
|
||||
const templateBuffer = await readFile(inputPath)
|
||||
const outputContents = handlebarsParse(templateBuffer, data)
|
||||
|
||||
if (!options.dryRun) {
|
||||
await writeFile(outputPath, outputContents)
|
||||
log(options, LogLevel.Info, "Done.")
|
||||
} else {
|
||||
log(options, "Content output:")
|
||||
log(options, outputContents)
|
||||
log(options, LogLevel.Info, "Content output:")
|
||||
log(options, LogLevel.Info, outputContents)
|
||||
}
|
||||
} else if (exists) {
|
||||
log(options, `File ${outputPath} already exists, skipping`)
|
||||
log(options, LogLevel.Info, `File ${outputPath} already exists, skipping`)
|
||||
}
|
||||
resolve()
|
||||
} catch (e: any) {
|
||||
|
||||
24
src/types.d.ts
vendored
24
src/types.d.ts
vendored
@@ -1,24 +0,0 @@
|
||||
export type FileResponseFn<T> = (fullPath: string, basedir: string, basename: string) => T
|
||||
|
||||
export type FileResponse<T> = T | FileResponseFn<T>
|
||||
|
||||
export interface ScaffoldConfig {
|
||||
name: string
|
||||
templates: string[]
|
||||
output: FileResponse<string>
|
||||
createSubFolder?: boolean
|
||||
data?: Record<string, string>
|
||||
overwrite?: FileResponse<boolean>
|
||||
quiet?: boolean
|
||||
dryRun?: boolean
|
||||
}
|
||||
export interface ScaffoldCmdConfig {
|
||||
name: string
|
||||
templates: string[]
|
||||
output: string
|
||||
createSubFolder: boolean
|
||||
data?: Record<string, string>
|
||||
overwrite: boolean
|
||||
quiet: boolean
|
||||
dryRun: boolean
|
||||
}
|
||||
75
src/types.ts
Normal file
75
src/types.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
export enum LogLevel {
|
||||
None = 0,
|
||||
Debug = 1,
|
||||
Info = 2,
|
||||
Warning = 3,
|
||||
Error = 4,
|
||||
}
|
||||
|
||||
export type FileResponseFn<T> = (fullPath: string, basedir: string, basename: string) => T
|
||||
|
||||
export type FileResponse<T> = T | FileResponseFn<T>
|
||||
|
||||
export interface ScaffoldConfig {
|
||||
/**
|
||||
* Name to be passed to the generated files. `{{name}}` and `{{Name}}` inside contents and file names will be replaced
|
||||
* accordingly.
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path,
|
||||
* or a glob pattern for multiple file matching easily. (default: current working directory)
|
||||
*/
|
||||
templates: string[]
|
||||
|
||||
/** Path to output to. If `createSubFolder` is `true`, the subfolder will be created inside this path. */
|
||||
output: FileResponse<string>
|
||||
|
||||
/**
|
||||
* Create subfolder with the input name (default: `false`)
|
||||
*/
|
||||
createSubFolder?: boolean
|
||||
|
||||
/**
|
||||
* Add custom data to the templates. By default, only your app name is included as `{{name}}` and `{{Name}}`.
|
||||
*/
|
||||
data?: Record<string, string>
|
||||
|
||||
/**
|
||||
* Enable to override output files, even if they already exist. (default: `false`)
|
||||
*
|
||||
* You may supply a function to this option, which can take the arguments `(fullPath, baseDir, baseName)` and returns
|
||||
* a string, to return a dynamic path for each file.
|
||||
*/
|
||||
overwrite?: FileResponse<boolean>
|
||||
|
||||
/** Suppress output logs (Same as `verbose: 0` or `verbose: LogLevel.None`) */
|
||||
quiet?: boolean
|
||||
|
||||
/**
|
||||
* Determine amount of logs to display.
|
||||
*
|
||||
* The values are: `0 (none) | 1 (debug) | 2 (info) | 3 (warn) | 4 (error)`. The provided level will display messages
|
||||
* of the same level or higher. (default: `2 (info)`)
|
||||
* @see LogLevel
|
||||
*/
|
||||
verbose?: LogLevel
|
||||
|
||||
/**
|
||||
* Don't emit files. This is good for testing your scaffolds and making sure they don't fail, without having to write
|
||||
* actual file contents or create directories. (default: `false`)
|
||||
*/
|
||||
dryRun?: boolean
|
||||
}
|
||||
export interface ScaffoldCmdConfig {
|
||||
name: string
|
||||
templates: string[]
|
||||
output: string
|
||||
createSubFolder: boolean
|
||||
data?: Record<string, string>
|
||||
overwrite: boolean
|
||||
quiet: boolean
|
||||
verbose: LogLevel
|
||||
dryRun: boolean
|
||||
}
|
||||
36
src/utils.ts
36
src/utils.ts
@@ -1,12 +1,13 @@
|
||||
import path from "path"
|
||||
import { F_OK } from "constants"
|
||||
import { FileResponse, FileResponseFn, ScaffoldConfig } from "./types"
|
||||
import { FileResponse, FileResponseFn, LogLevel, ScaffoldConfig } from "./types"
|
||||
import camelCase from "lodash/camelCase"
|
||||
import snakeCase from "lodash/snakeCase"
|
||||
import kebabCase from "lodash/kebabCase"
|
||||
import startCase from "lodash/startCase"
|
||||
import Handlebars from "handlebars"
|
||||
import { promises as fsPromises } from "fs"
|
||||
import chalk from "chalk"
|
||||
const { stat, access, mkdir } = fsPromises
|
||||
|
||||
const helpers = {
|
||||
@@ -26,11 +27,29 @@ export function handleErr(err: NodeJS.ErrnoException | null) {
|
||||
if (err) throw err
|
||||
}
|
||||
|
||||
export function log(options: ScaffoldConfig, ...obj: any[]) {
|
||||
if (options.quiet) {
|
||||
export function log(options: ScaffoldConfig, level: LogLevel, ...obj: any[]) {
|
||||
if (options.quiet || options.verbose === LogLevel.None || level <= (options.verbose ?? LogLevel.Info)) {
|
||||
return
|
||||
}
|
||||
console["log"](...obj)
|
||||
const levelColor: Record<LogLevel, keyof typeof chalk> = {
|
||||
[LogLevel.None]: "reset",
|
||||
[LogLevel.Debug]: "blue",
|
||||
[LogLevel.Info]: "dim",
|
||||
[LogLevel.Warning]: "yellow",
|
||||
[LogLevel.Error]: "red",
|
||||
}
|
||||
const chalkFn: any = chalk[levelColor[level]]
|
||||
const key: "log" | "warn" | "error" = level === LogLevel.Error ? "error" : level === LogLevel.Warning ? "warn" : "log"
|
||||
const logFn: any = console[key]
|
||||
logFn(
|
||||
...obj.map((i) =>
|
||||
i instanceof Error
|
||||
? chalkFn(i, JSON.stringify(i, undefined, 1), i.stack)
|
||||
: typeof i === "object"
|
||||
? chalkFn(JSON.stringify(i, undefined, 1))
|
||||
: chalkFn(i)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export async function createDirIfNotExists(dir: string, options: ScaffoldConfig): Promise<void> {
|
||||
@@ -42,6 +61,7 @@ export async function createDirIfNotExists(dir: string, options: ScaffoldConfig)
|
||||
|
||||
if (!(await pathExists(dir))) {
|
||||
try {
|
||||
log(options, LogLevel.Debug, `Creating dir ${dir}`)
|
||||
await mkdir(dir)
|
||||
return
|
||||
} catch (e: any) {
|
||||
@@ -95,3 +115,11 @@ export async function isDir(path: string): Promise<boolean> {
|
||||
const tplStat = await stat(path)
|
||||
return tplStat.isDirectory()
|
||||
}
|
||||
|
||||
export function removeGlob(template: string) {
|
||||
return template.replace(/\*/g, "").replace(/\/\//g, "/")
|
||||
}
|
||||
|
||||
export function makeRelativePath(str: string): string {
|
||||
return str.startsWith("/") ? str.slice(1) : str
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import mockFs from "mock-fs"
|
||||
import FileSystem from "mock-fs/lib/filesystem"
|
||||
import Scaffold from "../src/scaffold"
|
||||
import { readdirSync, readFileSync } from "fs"
|
||||
import { Console } from "console"
|
||||
|
||||
const fileStructNormal = {
|
||||
input: {
|
||||
@@ -19,21 +20,32 @@ const fileStructWithData = {
|
||||
|
||||
const fileStructNested = {
|
||||
input: {
|
||||
"{{name}}-1.text": "This should be in root",
|
||||
"{{name}}-1.txt": "This should be in root",
|
||||
"{{Name}}": {
|
||||
"{{name}}-2.txt": "Hello, my value is {{value}}",
|
||||
moreNesting: {
|
||||
"{{name}}-3.txt": "Hi! My value is actually NOT {{value}}!",
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {},
|
||||
}
|
||||
|
||||
// let logsTemp: any = []
|
||||
// let logMock: any
|
||||
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunction): jest.EmptyFunction {
|
||||
return () => {
|
||||
beforeEach(() => {
|
||||
// console.log("Mocking:", fileStruct)
|
||||
console = new Console(process.stdout, process.stderr)
|
||||
|
||||
mockFs(fileStruct)
|
||||
// logMock = jest.spyOn(console, 'log').mockImplementation((...args) => {
|
||||
// logsTemp.push(args)
|
||||
// })
|
||||
})
|
||||
testFn()
|
||||
afterEach(() => {
|
||||
// console.log("Restoring mock")
|
||||
mockFs.restore()
|
||||
})
|
||||
}
|
||||
@@ -48,9 +60,8 @@ describe("Scaffold", () => {
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const data = readFileSync(process.cwd() + "/output/app_name.txt")
|
||||
expect(data.toString()).toBe("Hello, my app is app_name")
|
||||
})
|
||||
@@ -61,7 +72,7 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
createSubFolder: true,
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const data = readFileSync(process.cwd() + "/output/app_name/app_name.txt")
|
||||
@@ -79,7 +90,7 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
await Scaffold({
|
||||
@@ -87,7 +98,7 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
data: { value: "2" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const data = readFileSync(process.cwd() + "/output/app_name.txt")
|
||||
@@ -100,7 +111,7 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
await Scaffold({
|
||||
@@ -109,7 +120,7 @@ describe("Scaffold", () => {
|
||||
templates: ["input"],
|
||||
data: { value: "2" },
|
||||
overwrite: true,
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const data = readFileSync(process.cwd() + "/output/app_name.txt")
|
||||
@@ -121,9 +132,13 @@ describe("Scaffold", () => {
|
||||
describe(
|
||||
"errors",
|
||||
withMock(fileStructNormal, () => {
|
||||
let consoleMock: jest.SpyInstance
|
||||
let consoleMock1: jest.SpyInstance
|
||||
beforeAll(() => {
|
||||
consoleMock = jest.spyOn(console, "error").mockImplementation(() => void 0)
|
||||
consoleMock1 = jest.spyOn(console, "error").mockImplementation(() => void 0)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
consoleMock1.mockRestore()
|
||||
})
|
||||
|
||||
test("should throw for bad input", async () => {
|
||||
@@ -133,16 +148,12 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["non-existing-input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
).rejects.toThrow()
|
||||
|
||||
expect(() => readFileSync(process.cwd() + "/output/app_name.txt")).toThrow()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
@@ -155,7 +166,7 @@ describe("Scaffold", () => {
|
||||
output: (fullPath, basedir, basename) => `custom-output/${basename.split(".")[0]}`,
|
||||
templates: ["input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
const data = readFileSync(process.cwd() + "/custom-output/app_name/app_name.txt")
|
||||
expect(data.toString()).toBe("Hello, my app is app_name")
|
||||
@@ -169,14 +180,25 @@ describe("Scaffold", () => {
|
||||
test("should maintain input structure on output", async () => {
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: "./",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const dir = readdirSync(process.cwd())
|
||||
const rootDir = readdirSync(process.cwd() + "/output")
|
||||
const dir = readdirSync(process.cwd() + "/output/AppName")
|
||||
const nestedDir = readdirSync(process.cwd() + "/output/AppName/moreNesting")
|
||||
expect(rootDir).toHaveProperty("length")
|
||||
expect(dir).toHaveProperty("length")
|
||||
expect(nestedDir).toHaveProperty("length")
|
||||
|
||||
const rootFile = readFileSync(process.cwd() + "/output/app_name-1.txt")
|
||||
const oneDeepFile = readFileSync(process.cwd() + "/output/AppName/app_name-2.txt")
|
||||
const twoDeepFile = readFileSync(process.cwd() + "/output/AppName/moreNesting/app_name-3.txt")
|
||||
expect(rootFile.toString()).toEqual("This should be in root")
|
||||
expect(oneDeepFile.toString()).toEqual("Hello, my value is 1")
|
||||
expect(twoDeepFile.toString()).toEqual("Hi! My value is actually NOT 1!")
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"sourceMap": true
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
},
|
||||
"include": [
|
||||
"src/index.ts",
|
||||
"src/cmd.ts"
|
||||
"src/cmd.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"tests/*"
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@@ -940,6 +940,14 @@ chalk@^4.0.0, chalk@^4.1.1:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
dependencies:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
char-regex@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
|
||||
@@ -2153,10 +2161,10 @@ makeerror@1.0.x:
|
||||
dependencies:
|
||||
tmpl "1.0.x"
|
||||
|
||||
massarg@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/massarg/-/massarg-0.1.2.tgz#f298741318172be14f3d2b701329fbe10eff41bb"
|
||||
integrity sha512-gpFIjsvOoqyQnrqNDytQXPljOGlX5lvJFGYzAIqjxDqiSZwHOvz+/YfjtzrFvokfYsk0uZbE/XOH4LVRiu/1cg==
|
||||
massarg@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/massarg/-/massarg-1.0.4.tgz#8756193bee9bc7331b1f48a8840e86acf808f980"
|
||||
integrity sha512-SakImNzZP8SN6tlcGBoC62z+12yv/To+h7KP8XapnOux5Mjiwprn4KhNHsjh2rbrKCAPqZQWYziAitSzGTkjMw==
|
||||
dependencies:
|
||||
chalk "^4.1.1"
|
||||
lodash "^4.17.21"
|
||||
|
||||
Reference in New Issue
Block a user