Compare commits

...

29 Commits

Author SHA1 Message Date
Chen Asraf
027f5576ba fix copyright 2021-12-06 00:03:51 +02:00
Chen Asraf
5d630e7e62 added custom helpers 2021-12-06 00:00:18 +02:00
Chen Asraf
7073add836 update deps + add MIGRATION.md 2021-12-05 23:28:23 +02:00
Chen Asraf
561477f07b update deps + update cmd requirements 2021-12-05 00:49:10 +02:00
Chen Asraf
bfcc59c20a update readme [skip ci] 2021-12-03 01:28:40 +02:00
Chen Asraf
ba5332d550 update readme [skip ci] 2021-12-03 01:11:18 +02:00
Chen Asraf
f9bc0419f3 update docs [skip publish] 2021-12-03 01:02:14 +02:00
Chen Asraf
381c55835f fix basename in some cases 2021-12-03 00:39:03 +02:00
Chen Asraf
efdb7dc00b run tests on ci [skip publish] 2021-12-02 11:48:57 +02:00
Chen Asraf
385829aa27 fix errors, fix nested output 2021-12-02 11:46:08 +02:00
Chen Asraf
33c357bccc remove types from package.json 2021-11-28 15:50:27 +02:00
Chen Asraf
b74b781a5b fix yarn.lock 2021-11-28 15:45:51 +02:00
Chen Asraf
57410c8d74 fix log level 0 2021-11-28 15:34:59 +02:00
Chen Asraf
f81cfd8ae1 add export for cmd_util 2021-11-28 15:28:48 +02:00
Chen Asraf
414494734d bump version number 2021-11-28 14:24:01 +02:00
Chen Asraf
89b588f64e update workflows [skip publish] 2021-11-28 14:09:25 +02:00
Chen Asraf
cf22e2e62f fixes + add log level [skip publish] 2021-11-28 14:07:25 +02:00
Chen Asraf
246c139061 fix readme [skip publish] 2021-11-18 15:25:43 +02:00
Chen Asraf
711d8f0333 try fix release upload 2021-11-18 14:22:23 +02:00
Chen Asraf
8b22e96329 try new release version 2021-11-18 14:19:50 +02:00
Chen Asraf
53c0842ab8 fixed release tarball file location 2021-11-18 14:18:18 +02:00
Chen Asraf
48631325c1 fixed release tarball file location 2021-11-18 14:16:58 +02:00
Chen Asraf
045ad0118a fix build output files 2021-11-18 14:09:21 +02:00
Chen Asraf
b4dca7a954 add build step 2021-11-18 13:57:57 +02:00
Chen Asraf
7c42808f63 try to fix workflow 2021-11-18 13:56:24 +02:00
Chen Asraf
fd42013e8b publish: debug mode off, try to fix workflow 2021-11-18 13:55:30 +02:00
Chen Asraf
961a72fcdc publish: debug mode on 2021-11-18 13:51:29 +02:00
Chen Asraf
d6d99cfdf2 try fix workflow 2021-11-18 13:45:37 +02:00
Chen Asraf
ea4ecabe02 Merge pull request #12 from chenasraf/v1.0
v1.0
2021-11-18 13:44:09 +02:00
17 changed files with 606 additions and 202 deletions

View File

@@ -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:

View File

@@ -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
View File

@@ -59,3 +59,5 @@ typings/
examples/test-output/**/*
dist/
.DS_Store
tmp/

View File

@@ -2,6 +2,11 @@
"typescript.tsdk": "./node_modules/typescript/lib",
"npm.packageManager": "yarn",
"cSpell.words": [
"massarg"
"massarg",
"nodir",
"nobrace",
"noext",
"nocomment",
"nonegate"
]
}

20
MIGRATION.md Normal file
View File

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

154
README.md
View File

@@ -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,18 +102,30 @@ const scaffold = new SimpleScaffold({
locals: {
property: "value",
},
}).run()
helpers: {
twice: (text) => [text, text].join(" ")
}
})
```
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:
### Additional Node.js options
```javascript
config.output = (fullPath, baseDir, baseName) => {
console.log({ fullPath, baseDir, baseName })
return [baseDir, baseName].join(path.sep)
}
```
In addition to all the options available in the command line, there are some JS-specific options
available:
1. When `output` is used in Node directly, it may also be passed a function for each input file to
output into a dynamic path:
```typescript
config.output = (fullPath, baseDir, baseName) => {
console.log({ fullPath, baseDir, baseName })
return path.resolve(baseDir, baseName)
}
```
2. You may add custom `helpers` to your scaffolds. Helpers are simple `(string) => string` functions
that transform your `data` variables into other values. See [Helpers](#helpers) for the list of
default helpers, or add your own to be loaded into the template parser.
## Preparing files
@@ -118,23 +151,37 @@ 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 |
| upperCase | `{{ upperCase name }}` | MYNAME |
| lowerCase | `{{ lowerCase 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`.
You may also add your own custom helpers using the `helpers` options when using the JS API (rather
than the CLI). The `helpers` option takes an object whose keys are helper names, and values are
the transformation functions. For example, `upperCase` is implemented like so:
```typescript
config.helpers = {
upperCase: (text) => text.toUpperCase(),
}
```
## Examples
@@ -144,7 +191,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 +208,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 +233,7 @@ module.exports = class {{Name}} extends React.Component {
- ...
```
With `createSubfolder = false`:
With `createSubFolder = false`:
```plaintext
- project
@@ -195,14 +243,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.

View File

@@ -1,16 +1,15 @@
{
"name": "simple-scaffold",
"version": "1.0.0-alpha.1",
"version": "1.0.0-alpha.14",
"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.5",
"util.promisify": "^1.1.1"
},
"devDependencies": {

View File

@@ -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()

95
src/cmd_util.ts Normal file
View File

@@ -0,0 +1,95 @@
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`,
`NPM: ${chalk.underline`https://npmjs.com/package/simple-scaffold`}`,
`GitHub: ${chalk.underline`https://github.com/chenasraf/simple-scaffold`}`,
].join("\n"),
})
.parse(args)
)
}

View File

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

View File

@@ -13,14 +13,17 @@ import {
pathExists,
pascalCase,
isDir,
removeGlob,
makeRelativePath,
registerHelpers,
} 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 +31,65 @@ export async function Scaffold(config: ScaffoldConfig) {
data: options.data,
overwrite: options.overwrite,
quiet: options.quiet,
helpers: Object.keys(options.helpers ?? {}),
verbose: `${options.verbose} (${Object.keys(LogLevel).find(
(k) => (LogLevel[k as any] as unknown as number) === options.verbose!
)})`,
})
log(options, "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) {
registerHelpers(options)
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 +97,7 @@ export async function Scaffold(config: ScaffoldConfig) {
}
}
} catch (e: any) {
console.error(e)
log(options, LogLevel.Error, e)
throw e
}
}
@@ -63,35 +110,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
View File

@@ -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
}

95
src/types.ts Normal file
View File

@@ -0,0 +1,95 @@
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
/**
* Additional helpers to add to the template parser. Provide an object whose keys are the name of the function to add,
* and the value is the helper function itself. The signature of helpers is as follows:
* ```typescript
* (text: string) => string
* ```
*
* A full example might be:
*
* ```typescript
* Scaffold({
* //...
* helpers: {
* upperCamelCase: (text) => camelCase(text).toUpperCase()
* }
* })
* ```
*/
helpers?: Record<string, (text: string) => string>
}
export interface ScaffoldCmdConfig {
name: string
templates: string[]
output: string
createSubFolder: boolean
data?: Record<string, string>
overwrite: boolean
quiet: boolean
verbose: LogLevel
dryRun: boolean
}

View File

@@ -1,36 +1,61 @@
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 = {
export const defaultHelpers: Exclude<ScaffoldConfig["helpers"], undefined> = {
camelCase,
snakeCase,
startCase,
kebabCase,
hyphenCase: kebabCase,
pascalCase,
lowerCase: (text) => text.toLowerCase(),
upperCase: (text) => text.toUpperCase(),
}
for (const helperName in helpers) {
Handlebars.registerHelper(helperName, helpers[helperName as keyof typeof helpers])
export function registerHelpers(options: ScaffoldConfig) {
const _helpers = { ...defaultHelpers, ...options.helpers }
for (const helperName in _helpers) {
log(options, LogLevel.Debug, `Registering helper: ${helperName}`)
Handlebars.registerHelper(helperName, _helpers[helperName as keyof typeof _helpers])
}
}
export function handleErr(err: NodeJS.ErrnoException | null) {
if (err) throw err
}
export function log(options: ScaffoldConfig, ...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 +67,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 +121,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
}

View File

@@ -2,6 +2,8 @@ import mockFs from "mock-fs"
import FileSystem from "mock-fs/lib/filesystem"
import Scaffold from "../src/scaffold"
import { readdirSync, readFileSync } from "fs"
import { Console } from "console"
import { defaultHelpers } from "../src/utils"
const fileStructNormal = {
input: {
@@ -19,21 +21,46 @@ 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: {},
}
const defaultHelperNames = Object.keys(defaultHelpers)
const fileStructHelpers = {
input: {
defaults: defaultHelperNames.reduce<Record<string, string>>(
(all, cur) => ({ ...all, [cur + ".txt"]: `{{ ${cur} name }}` }),
{}
),
custom: {
"add1.txt": "{{ add1 name }}",
},
},
output: {},
}
// let logsTemp: any = []
// let logMock: any
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunction): jest.EmptyFunction {
return () => {
beforeEach(() => {
// console.log("Mocking:", fileStruct)
console = new Console(process.stdout, process.stderr)
mockFs(fileStruct)
// logMock = jest.spyOn(console, 'log').mockImplementation((...args) => {
// logsTemp.push(args)
// })
})
testFn()
afterEach(() => {
// console.log("Restoring mock")
mockFs.restore()
})
}
@@ -48,9 +75,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 +87,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 +105,7 @@ describe("Scaffold", () => {
output: "output",
templates: ["input"],
data: { value: "1" },
quiet: true,
verbose: 0,
})
await Scaffold({
@@ -87,7 +113,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 +126,7 @@ describe("Scaffold", () => {
output: "output",
templates: ["input"],
data: { value: "1" },
quiet: true,
verbose: 0,
})
await Scaffold({
@@ -109,7 +135,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 +147,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 +163,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 +181,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 +195,80 @@ 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!")
})
})
)
describe(
"helpers",
withMock(fileStructHelpers, () => {
const _helpers: Record<string, (text: string) => string> = {
add1: (text) => text + " 1",
}
describe("default helpers", () => {
test("should work", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
verbose: 0,
helpers: _helpers,
})
const results = {
camelCase: "appName",
snakeCase: "app_name",
startCase: "App Name",
kebabCase: "app-name",
hyphenCase: "app-name",
pascalCase: "AppName",
lowerCase: "app_name",
upperCase: "APP_NAME",
}
for (const key in results) {
const file = readFileSync(process.cwd() + `/output/defaults/${key}.txt`)
expect(file.toString()).toEqual(results[key as keyof typeof results])
}
})
})
describe("custom helpers", () => {
test("should work", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
verbose: 0,
helpers: _helpers,
})
const results = {
add1: "app_name 1",
}
for (const key in results) {
const file = readFileSync(process.cwd() + `/output/custom/${key}.txt`)
expect(file.toString()).toEqual(results[key as keyof typeof results])
}
})
})
})
)

View File

@@ -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/*"

View File

@@ -932,7 +932,7 @@ chalk@2.4.2, chalk@^2.0.0:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^4.0.0, chalk@^4.1.1:
chalk@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
@@ -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,12 +2161,12 @@ 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.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/massarg/-/massarg-1.0.5.tgz#3dfd49bb63bfca4b0371a2f0ffc7580a9a6cc7a0"
integrity sha512-gkJHZsNfeMurpVPKojCiT2lnG2cSxHPAXZSg+gCLIgCf5bGn7pKOsoeu4qVAc5mlBxV2l/hA1yojt+ZsJd8SDg==
dependencies:
chalk "^4.1.1"
chalk "^4.1.2"
lodash "^4.17.21"
merge-stream@^2.0.0: