mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-18 01:29:09 +00:00
Compare commits
6 Commits
v1.0.0-alp
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf08408a64 | ||
|
|
134c15d7dc | ||
|
|
5af283292e | ||
|
|
dcf0d04b68 | ||
|
|
41076366a6 | ||
|
|
f35baebf3a |
3
.github/workflows/alpha.yml
vendored
3
.github/workflows/alpha.yml
vendored
@@ -36,6 +36,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: ${{ steps.update_tag.outputs.tagname }}
|
||||
release_name: Release ${{ steps.update_tag.outputs.tagname }}
|
||||
- name: Upload Release Asset
|
||||
@@ -47,5 +48,5 @@ jobs:
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./package.tgz
|
||||
asset_name: package.tgz
|
||||
asset_name: simple-scaffold ${{ steps.update_tag.outputs.tagname }}.tgz
|
||||
asset_content_type: application/tgz
|
||||
|
||||
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -46,5 +46,5 @@ jobs:
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./package.tgz
|
||||
asset_name: package.tgz
|
||||
asset_name: simple-scaffold ${{ steps.update_tag.outputs.tagname }}.tgz
|
||||
asset_content_type: application/tgz
|
||||
|
||||
11
README.md
11
README.md
@@ -47,8 +47,7 @@ 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. (default:
|
||||
)
|
||||
pattern for multiple file matching easily. (default: current dir)
|
||||
|
||||
--overwrite|-w Enable to override output files, even if they already exist.
|
||||
(default: false)
|
||||
@@ -88,13 +87,13 @@ You can also add this as a script in your `package.json`:
|
||||
## Use in Node.js
|
||||
|
||||
You can also build the scaffold yourself, if you want to create more complex arguments or scaffold groups.
|
||||
Simply pass a config object to the constructor, and invoke `run()` when you are ready to start.
|
||||
Simply pass a config object to the Scaffold function when you are ready to start.
|
||||
The config takes similar arguments to the command line:
|
||||
|
||||
```typescript
|
||||
import Scaffold from "simple-scaffold"
|
||||
|
||||
const scaffold = SimpleScaffold({
|
||||
const config = {
|
||||
name: "component",
|
||||
templates: [path.join(__dirname, "scaffolds", "component")],
|
||||
output: path.join(__dirname, "src", "components"),
|
||||
@@ -105,7 +104,9 @@ const scaffold = SimpleScaffold({
|
||||
helpers: {
|
||||
twice: (text) => [text, text].join(" ")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const scaffold = Scaffold(config)
|
||||
```
|
||||
|
||||
### Additional Node.js options
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simple-scaffold",
|
||||
"version": "1.0.0-alpha.15",
|
||||
"version": "1.0.0-alpha.18",
|
||||
"description": "Create files based on templates",
|
||||
"repository": "https://github.com/chenasraf/simple-scaffold.git",
|
||||
"author": "Chen Asraf <inbox@casraf.com>",
|
||||
@@ -9,7 +9,7 @@
|
||||
"bin": "cmd.js",
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist/",
|
||||
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./dist/ && cp ./README.md ./dist/",
|
||||
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/scaffold.js",
|
||||
"test": "jest --verbose",
|
||||
|
||||
97
src/cmd.ts
97
src/cmd.ts
@@ -1,2 +1,97 @@
|
||||
import { parseCliArgs } from "./cmd_util"
|
||||
#!/usr/bin/env node
|
||||
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. ${chalk.reset`${chalk.white`(default: current dir)`}`}`,
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
parseCliArgs()
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
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)
|
||||
)
|
||||
}
|
||||
@@ -20,9 +20,11 @@ import {
|
||||
import { LogLevel, ScaffoldConfig } from "./types"
|
||||
|
||||
export async function Scaffold({ ...options }: ScaffoldConfig) {
|
||||
options.output ??= process.cwd()
|
||||
|
||||
registerHelpers(options)
|
||||
try {
|
||||
const data = { name: options.name, Name: pascalCase(options.name), ...options.data }
|
||||
options.data = { name: options.name, Name: pascalCase(options.name), ...options.data }
|
||||
log(options, LogLevel.Debug, "Full config:", {
|
||||
name: options.name,
|
||||
templates: options.templates,
|
||||
@@ -36,10 +38,10 @@ export async function Scaffold({ ...options }: ScaffoldConfig) {
|
||||
(k) => (LogLevel[k as any] as unknown as number) === options.verbose!
|
||||
)})`,
|
||||
})
|
||||
log(options, LogLevel.Info, "Data:", data)
|
||||
log(options, LogLevel.Info, "Data:", options.data)
|
||||
for (let template of options.templates) {
|
||||
try {
|
||||
const _isGlob = template.includes("*")
|
||||
const _isGlob = glob.hasMagic(template)
|
||||
if (!_isGlob && !(await pathExists(template))) {
|
||||
const err: NodeJS.ErrnoException = new Error(`ENOENT, no such file or directory ${template}`)
|
||||
err.code = "ENOENT"
|
||||
@@ -59,37 +61,34 @@ export async function Scaffold({ ...options }: ScaffoldConfig) {
|
||||
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,
|
||||
debug: options.verbose === LogLevel.Debug,
|
||||
nodir: 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)
|
||||
if (await isDir(inputFilePath)) {
|
||||
continue
|
||||
}
|
||||
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, options.data)
|
||||
}
|
||||
} catch (e: any) {
|
||||
handleErr(e)
|
||||
@@ -110,12 +109,12 @@ async function handleTemplateFile(
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const inputPath = path.resolve(process.cwd(), templatePath)
|
||||
const outputPathOpt = getOptionValueForFile(inputPath, data, options.output)
|
||||
const outputPathOpt = getOptionValueForFile(options, inputPath, data, options.output)
|
||||
const outputDir = path.resolve(
|
||||
process.cwd(),
|
||||
...([outputPathOpt, basePath, options.createSubFolder ? options.name : undefined].filter(Boolean) as string[])
|
||||
)
|
||||
const outputPath = handlebarsParse(path.join(outputDir, path.basename(inputPath)), data)
|
||||
const outputPath = handlebarsParse(options, path.join(outputDir, path.basename(inputPath)), data).toString()
|
||||
log(
|
||||
options,
|
||||
LogLevel.Debug,
|
||||
@@ -127,7 +126,7 @@ async function handleTemplateFile(
|
||||
`\nFull output path: ${outputPath}`,
|
||||
`\n`
|
||||
)
|
||||
const overwrite = getOptionValueForFile(inputPath, data, options.overwrite ?? false)
|
||||
const overwrite = getOptionValueForFile(options, inputPath, data, options.overwrite ?? false)
|
||||
const exists = await pathExists(outputPath)
|
||||
|
||||
await createDirIfNotExists(path.dirname(outputPath), options)
|
||||
@@ -138,7 +137,7 @@ async function handleTemplateFile(
|
||||
log(options, LogLevel.Info, `File ${outputPath} exists, overwriting`)
|
||||
}
|
||||
const templateBuffer = await readFile(inputPath)
|
||||
const outputContents = handlebarsParse(templateBuffer, data)
|
||||
const outputContents = handlebarsParse(options, templateBuffer, data)
|
||||
|
||||
if (!options.dryRun) {
|
||||
await writeFile(outputPath, outputContents)
|
||||
|
||||
22
src/utils.ts
22
src/utils.ts
@@ -80,6 +80,7 @@ export async function createDirIfNotExists(dir: string, options: ScaffoldConfig)
|
||||
}
|
||||
|
||||
export function getOptionValueForFile<T>(
|
||||
options: ScaffoldConfig,
|
||||
filePath: string,
|
||||
data: Record<string, string>,
|
||||
fn: FileResponse<T>,
|
||||
@@ -90,15 +91,24 @@ export function getOptionValueForFile<T>(
|
||||
}
|
||||
return (fn as FileResponseFn<T>)(
|
||||
filePath,
|
||||
path.dirname(handlebarsParse(filePath, data)),
|
||||
path.basename(handlebarsParse(filePath, data))
|
||||
path.dirname(handlebarsParse(options, filePath, data).toString()),
|
||||
path.basename(handlebarsParse(options, filePath, data).toString())
|
||||
)
|
||||
}
|
||||
|
||||
export function handlebarsParse(templateBuffer: Buffer | string, data: Record<string, string>) {
|
||||
const parser = Handlebars.compile(templateBuffer.toString(), { noEscape: true })
|
||||
const outputContents = parser(data)
|
||||
return outputContents
|
||||
export function handlebarsParse(
|
||||
options: ScaffoldConfig,
|
||||
templateBuffer: Buffer | string,
|
||||
data: Record<string, string>
|
||||
) {
|
||||
try {
|
||||
const parser = Handlebars.compile(templateBuffer.toString(), { noEscape: true })
|
||||
const outputContents = parser(data)
|
||||
return outputContents
|
||||
} catch {
|
||||
log(options, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
|
||||
return templateBuffer
|
||||
}
|
||||
}
|
||||
|
||||
export async function pathExists(filePath: string): Promise<boolean> {
|
||||
|
||||
Reference in New Issue
Block a user