mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-18 01:29:09 +00:00
Compare commits
15 Commits
v3.1.1
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
414494734d | ||
|
|
89b588f64e | ||
|
|
cf22e2e62f | ||
|
|
246c139061 | ||
|
|
711d8f0333 | ||
|
|
8b22e96329 | ||
|
|
53c0842ab8 | ||
|
|
48631325c1 | ||
|
|
045ad0118a | ||
|
|
b4dca7a954 | ||
|
|
7c42808f63 | ||
|
|
fd42013e8b | ||
|
|
961a72fcdc | ||
|
|
d6d99cfdf2 | ||
|
|
ea4ecabe02 |
9
.github/workflows/alpha.yml
vendored
9
.github/workflows/alpha.yml
vendored
@@ -13,8 +13,11 @@ jobs:
|
||||
with:
|
||||
node-version: "12.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn pack --filename=release.tgz
|
||||
- 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 +29,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 +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 && !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:
|
||||
|
||||
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
@@ -13,8 +13,11 @@ jobs:
|
||||
with:
|
||||
node-version: "12.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn pack --filename=release.tgz
|
||||
- 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 +25,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 +37,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/
|
||||
|
||||
41
README.md
41
README.md
@@ -43,15 +43,18 @@ Options:
|
||||
--data|-d Add custom data to the templates. By default, only your app
|
||||
name is included.
|
||||
|
||||
--create-sub-folder|-s Create subfolder with the input name (default:
|
||||
false)
|
||||
--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`:
|
||||
@@ -73,7 +76,7 @@ The config takes similar arguments to the command line:
|
||||
```javascript
|
||||
const SimpleScaffold = require("simple-scaffold").default
|
||||
|
||||
const scaffold = new SimpleScaffold({
|
||||
const scaffold = SimpleScaffold({
|
||||
name: "component",
|
||||
templates: [path.join(__dirname, "scaffolds", "component")],
|
||||
output: path.join(__dirname, "src", "components"),
|
||||
@@ -81,7 +84,7 @@ 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
|
||||
@@ -120,19 +123,19 @@ Your `data` will be pre-populated with the following:
|
||||
|
||||
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`.
|
||||
|
||||
@@ -202,7 +205,7 @@ const React = require("react")
|
||||
|
||||
module.exports = class MyComponent extends React.Component {
|
||||
render() {
|
||||
<div className="my-component">MyComponent Component</div>
|
||||
<div className="myClassName">MyComponent Component</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "simple-scaffold",
|
||||
"version": "1.0.0-alpha.1",
|
||||
"version": "1.0.0-alpha.6",
|
||||
"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",
|
||||
"types": "types.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",
|
||||
|
||||
17
src/cmd.ts
17
src/cmd.ts
@@ -1,6 +1,7 @@
|
||||
import Scaffold from "./scaffold"
|
||||
import massarg from "massarg"
|
||||
import { ScaffoldCmdConfig } from "./types"
|
||||
import { LogLevel } from "."
|
||||
|
||||
massarg<ScaffoldCmdConfig & { help: boolean; extras: string[] }>()
|
||||
.main(Scaffold)
|
||||
@@ -46,7 +47,21 @@ massarg<ScaffoldCmdConfig & { help: boolean; extras: string[] }>()
|
||||
defaultValue: false,
|
||||
boolean: true,
|
||||
})
|
||||
.option({ aliases: ["q"], name: "quiet", description: "Suppress output logs", defaultValue: false, boolean: true })
|
||||
.option({
|
||||
aliases: ["q"],
|
||||
name: "quiet",
|
||||
description: "Suppress output logs (Same as --verbose 0)",
|
||||
defaultValue: false,
|
||||
boolean: true,
|
||||
})
|
||||
.option({
|
||||
aliases: ["v"],
|
||||
name: "verbose",
|
||||
description:
|
||||
"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.",
|
||||
defaultValue: LogLevel.Info,
|
||||
parse: Number,
|
||||
})
|
||||
.option({
|
||||
aliases: ["dr"],
|
||||
name: "dry-run",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./scaffold"
|
||||
export * from "./types"
|
||||
import Scaffold from "./scaffold"
|
||||
export default Scaffold
|
||||
|
||||
@@ -13,14 +13,15 @@ import {
|
||||
pathExists,
|
||||
pascalCase,
|
||||
isDir,
|
||||
removeGlob,
|
||||
} from "./utils"
|
||||
import { ScaffoldConfig } from "./types"
|
||||
import { LogLevel, ScaffoldConfig } from "./types"
|
||||
|
||||
export async function Scaffold(config: ScaffoldConfig) {
|
||||
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,
|
||||
@@ -29,19 +30,33 @@ export async function Scaffold(config: ScaffoldConfig) {
|
||||
overwrite: options.overwrite,
|
||||
quiet: options.quiet,
|
||||
})
|
||||
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("*")
|
||||
const _nonGlobTemplate = _isGlob ? removeGlob(template) : template
|
||||
const _isDir = _isGlob ? false : await isDir(template)
|
||||
const _shouldAddGlob = !_isGlob && !_isDir
|
||||
if (_shouldAddGlob) {
|
||||
template = template + "/**/*"
|
||||
}
|
||||
const files = await promisify(glob)(template, { dot: true, debug: false })
|
||||
for (const templatePath of files) {
|
||||
if (!(await isDir(templatePath))) {
|
||||
const basePath = path
|
||||
.resolve(
|
||||
process.cwd(),
|
||||
_isDir
|
||||
? templatePath.replace(template, "")
|
||||
: path.dirname(removeGlob(templatePath).replace(_nonGlobTemplate, ""))
|
||||
)
|
||||
.replace(process.cwd() + "/", "")
|
||||
.replace(process.cwd(), "")
|
||||
log(
|
||||
options,
|
||||
LogLevel.Debug,
|
||||
`\ntemplate: ${template}\ntemplatePath: ${templatePath}, \nbase path: ${basePath}\n`
|
||||
)
|
||||
await handleTemplateFile(templatePath, basePath, options, data)
|
||||
}
|
||||
}
|
||||
@@ -63,23 +78,41 @@ 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[])
|
||||
)
|
||||
log(
|
||||
options,
|
||||
LogLevel.Debug,
|
||||
`\nParsing ${templatePath}`,
|
||||
`\nBase path: ${basePath}`,
|
||||
`\nFull input path: ${inputPath}`,
|
||||
`\nFull output path: ${outputDir}\n`
|
||||
)
|
||||
const outputPath = path.join(outputDir, handlebarsParse(path.basename(inputPath), data))
|
||||
const overwrite = getOptionValueForFile(inputPath, data, options.overwrite ?? false)
|
||||
const exists = await pathExists(outputPath)
|
||||
|
||||
log(
|
||||
options,
|
||||
LogLevel.Debug,
|
||||
"Filename parsed:",
|
||||
handlebarsParse(path.basename(inputPath), data),
|
||||
"Orig:",
|
||||
path.basename(inputPath)
|
||||
// "Test:",
|
||||
// handlebarsParse("{{name}} {{name pascalCase}}", data)
|
||||
)
|
||||
|
||||
await createDirIfNotExists(outputDir, 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)
|
||||
@@ -87,11 +120,11 @@ async function handleTemplateFile(
|
||||
if (!options.dryRun) {
|
||||
await writeFile(outputPath, outputContents)
|
||||
} 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) {
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
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 {
|
||||
/** The name supplied for the output templates */
|
||||
name: string
|
||||
/** Template input files/dirs/glob patterns to use as template input. These will be copied to the output directory. */
|
||||
templates: string[]
|
||||
/** Output directory to put scaffolded files in. */
|
||||
output: FileResponse<string>
|
||||
createSubFolder?: boolean
|
||||
data?: Record<string, string>
|
||||
overwrite?: FileResponse<boolean>
|
||||
quiet?: boolean
|
||||
verbose?: LogLevel
|
||||
dryRun?: boolean
|
||||
}
|
||||
export interface ScaffoldCmdConfig {
|
||||
22
src/utils.ts
22
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,20 @@ 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.Info) > level) {
|
||||
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]]
|
||||
console["log"](...obj.map((i) => (typeof i === "object" ? chalkFn(JSON.stringify(i, undefined, 1)) : chalkFn(i))))
|
||||
// console["log"](...obj)
|
||||
}
|
||||
|
||||
export async function createDirIfNotExists(dir: string, options: ScaffoldConfig): Promise<void> {
|
||||
@@ -95,3 +105,7 @@ 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, "/")
|
||||
}
|
||||
|
||||
@@ -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/*"
|
||||
|
||||
Reference in New Issue
Block a user