Compare commits

...

23 Commits

Author SHA1 Message Date
Chen Asraf
fe871eb97e Merge pull request #28 from chenasraf/dependabot/npm_and_yarn/minimist-1.2.6
Security fix: Bump minimist from 1.2.5 to 1.2.6
2022-04-10 14:51:02 +03:00
Chen Asraf
dffa81fde1 bump version number 2022-04-10 14:47:06 +03:00
dependabot[bot]
56be5f32cd Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-10 02:49:52 +00:00
Chen Asraf
4a4e024aec Add keywords to package.json [skip ci] 2022-03-15 11:42:53 +02:00
Chen Asraf
86a7a2c063 Update README.md [skip ci] 2022-03-15 11:40:55 +02:00
Chen Asraf
d3259c44aa Update README.md [skip ci] 2022-03-13 11:22:44 +02:00
Chen Asraf
e26a434dba update README.md 2022-03-03 21:53:50 +02:00
Chen Asraf
1783ddf230 remove unnecessary package [skip ci] 2022-03-03 21:53:08 +02:00
Chen Asraf
4575f73d69 Windows compatibility
Fix transform of windows-style paths
2022-03-03 21:49:44 +02:00
Chen Asraf
f07df79e82 improved test 2022-03-03 21:48:13 +02:00
Chen Asraf
cb6e06f1c9 bump version number 2022-03-03 21:43:55 +02:00
Chen Asraf
a043a05bc9 updated tests 2022-03-03 21:42:31 +02:00
Chen Asraf
52cb3e7353 fixed more windows paths, updated tests 2022-03-03 12:25:39 +02:00
Chen Asraf
89d7897f4e refactor handlebarsParse - remove redundant arg 2022-03-03 10:31:30 +02:00
Chen Asraf
d6e1693074 import/file cleanup 2022-03-03 10:25:44 +02:00
Chen Asraf
56f1340093 fix transform of windows-style paths 2022-03-03 10:21:37 +02:00
Chen Asraf
8782f18a73 v1.0.1
Merge pull request #24 from chenasraf/alpha
2022-02-20 14:10:59 +02:00
Chen Asraf
21c4ab6e1a bump version number [skip ci] 2022-02-20 14:06:56 +02:00
Chen Asraf
d797e5b640 bump alpha version number 2022-02-17 19:19:07 +02:00
Chen Asraf
c3835a7b04 refactoring - code cleanup 2022-02-17 19:18:48 +02:00
Chen Asraf
81ba5f50fd add subFolderNameHelper arg 2022-02-17 19:12:17 +02:00
Chen Asraf
edcf1aceaa Update README.md 2022-02-14 23:34:59 +02:00
Chen Asraf
d0a0db0f82 Update MIGRATION.md [skip ci] 2022-02-14 23:31:52 +02:00
12 changed files with 515 additions and 210 deletions

View File

@@ -3,10 +3,11 @@
"npm.packageManager": "yarn",
"cSpell.words": [
"massarg",
"nodir",
"nobrace",
"noext",
"nocomment",
"nonegate"
"nodir",
"noext",
"nonegate",
"subdir"
]
}

View File

@@ -5,6 +5,7 @@ between versions. With these notable exceptions:
- Some of the argument names have changed
- Template syntax has been improved
- The command to run Scaffold has been simplified from `new SimpleScaffold(opts).run()` to `SimpleScaffold(opts)`, which now returns a promise that you can await to know when the process has been completed.
## Argument changes

129
README.md
View File

@@ -1,16 +1,13 @@
# simple-scaffold
Simple Scaffold allows you to create your structured files based on templates.
Simple Scaffold allows you to generate any set of files in the easiest way possible with simple commands.
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.
It is completely framework agnostic so you can use it for anything from a few simple files to an
entire app boilerplate setup.
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.
Simply organize your commonly-created files in their original structure, and running Simple Scaffold
will copy the files to the output path, while replacing values (such as component or app name, or
other custom data) inside the paths or contents of the files using Handlebars.js syntax.
## Install
@@ -22,7 +19,7 @@ npm install [-g] simple-scaffold
# yarn
yarn [global] add simple-scaffold
# run without installing
npx simple-scaffold <...args>
npx simple-scaffold@latest <...args>
```
## Use as a command line tool
@@ -36,50 +33,53 @@ Create structured files based on templates.
Options:
--help|-h Display help information
--help|-h Display help information
--name|-n Name to be passed to the generated files. {{name}} and
{{Name}} inside contents and file names will be replaced
accordingly.
--name|-n Name to be passed to the generated files. {{name}} and
{{Name}} inside contents and file names will be replaced
accordingly.
--output|-o Path to output to. If --create-sub-folder is enabled, the
subfolder will be created inside this path.
--output|-o Path to output to. If --create-sub-folder is enabled,
the subfolder will be created inside this path.
(default: current dir)
--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: current dir)
--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.
--overwrite|-w Enable to override output files, even if they already exist.
(default: false)
--overwrite|-w Enable to override output files, even if they already
exist. (default: false)
--data|-d Add custom data to the templates. By default, only your app
name is included.
--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 (Same as --verbose 0)
(default: false)
--sub-folder-name-helper|-sh Default helper to apply to subfolder name when using
`--create-sub-folder true`.
--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)
--quiet|-q Suppress output logs (Same as --verbose 0)
(default: false)
--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)
--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\"}'"
"scaffold": "npx simple-scaffold@latest -t scaffolds/component/**/* -o src/components -d '{\"myProp\": \"propName\", \"myVal\": 123}'"
}
}
```
@@ -98,7 +98,8 @@ const config = {
templates: [path.join(__dirname, "scaffolds", "component")],
output: path.join(__dirname, "src", "components"),
createSubFolder: true,
locals: {
subFolderNameHelper: "upperCase"
data: {
property: "value",
},
helpers: {
@@ -142,15 +143,29 @@ transformed files in the output directory.
The data available for the template parser is the data you pass to the `data` config option (or
`--data` argument in CLI).
For example, using the following command:
```bash
npx simple-scaffold@latest --templates templates/components/{{name}}.jsx --output src/components -create-sub-folder true MyComponent
```
Will output a file with the path:
```plaintext
<working_dir>/src/components/MyComponent.jsx
```
The contents of the file will be transformed in a similar fashion.
Your `data` will be pre-populated with the following:
- `{{Name}}`: PascalCase of the component name
- `{{name}}`: raw name of the component
- `{{name}}`: raw name of the component as you entered it
> Simple-Scaffold uses [Handlebars.js](https://handlebarsjs.com/) for outputting the file contents,
> see their documentation for more information on syntax.
> Simple-Scaffold uses [Handlebars.js](https://handlebarsjs.com/) for outputting the file contents.
> Any `data` you add in the config will be available for use with their names wrapped in
> `{{` and `}}`.
> `{{` and `}}`. Other Handlebars built-ins such as `each`, `if` and `with` are also supported, see
> [Handlebars.js Language Features](https://handlebarsjs.com/guide/#language-features) for more information.
#### Helpers
@@ -163,14 +178,15 @@ Here are the built-in helpers available for use:
| Helper name | Example code | Example output |
| ----------- | ----------------------- | -------------- |
| [None] | `{{ name }}` | my name |
| 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 |
| upperCase | `{{ upperCase name }}` | MY NAME |
| lowerCase | `{{ lowerCase name }}` | my name |
> These helpers are available for any data property, not exclusive to `name`.
@@ -184,6 +200,9 @@ config.helpers = {
}
```
These helpers will also be available to you when using `subFolderNameHelper` or
`--sub-folder-name-helper` as a possible value.
## Examples
### Command Example
@@ -211,12 +230,12 @@ simple-scaffold MyComponent \
#### Contents of `project/scaffold/{{Name}}.jsx`
```js
const React = require('react')
```typescriptreact
import React from 'react'
module.exports = function {{Name}}(props) {
export default {{camelCase ame}}: React.FC = (props) => {
return (
<div className="{{className}}">{{Name}} Component</div>
<div className="{{className}}">{{camelCase name}} Component</div>
)
}
```
@@ -246,10 +265,10 @@ With `createSubFolder = false`:
#### Contents of `project/scaffold/MyComponent/MyComponent.jsx`
```js
const React = require("react")
```typescriptreact
import React from 'react'
module.exports = function MyComponent(props) {
export default MyComponent: React.FC = (props) => {
return (
<div className="myClassName">MyComponent Component</div>
)
@@ -280,6 +299,6 @@ Some tips on getting around the code:
- 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.
> This requires an updated build, and does not trigger one itself. Either use `yarn dev` to watch
> for changes and build, 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,12 +1,13 @@
{
"name": "simple-scaffold",
"version": "1.0.0",
"version": "1.0.4",
"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",
"keywords": ["javascript", "cli", "template", "files", "typescript", "generator", "scaffold", "file", "scaffolding"],
"scripts": {
"clean": "rm -rf dist/",
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
@@ -18,7 +19,6 @@
"build-cmd": "yarn build && yarn cmd"
},
"dependencies": {
"args": "^5.0.1",
"chalk": "^4.1.2",
"glob": "^7.1.3",
"handlebars": "^4.7.7",

View File

@@ -51,6 +51,11 @@ export function parseCliArgs(args = process.argv.slice(2)) {
defaultValue: false,
description: "Create subfolder with the input name",
})
.option({
name: "sub-folder-name-helper",
aliases: ["sh"],
description: "Default helper to apply to subfolder name when using `--create-sub-folder true`.",
})
.option({
name: "quiet",
aliases: ["q"],

View File

View File

@@ -1,94 +1,88 @@
import { glob } from "glob"
import path from "path"
import { promisify } from "util"
import { promises as fsPromises } from "fs"
const { readFile, writeFile } = fsPromises
import {
createDirIfNotExists,
getOptionValueForFile,
handleErr,
handlebarsParse,
log,
pathExists,
pascalCase,
isDir,
removeGlob,
makeRelativePath,
registerHelpers,
getTemplateGlobInfo,
ensureFileExists,
getFileList,
getBasePath,
copyFileTransformed,
getTemplateFileInfo,
logInitStep,
logInputFile,
} from "./utils"
import { LogLevel, ScaffoldConfig } from "./types"
/**
* Create a scaffold using given `options`.
*
* #### Create files
* To create a file structure to output, use any directory and file structure you would like.
* Inside folder names, file names or file contents, you may place `{{ var }}` where `var` is either
* `name` which is the scaffold name you provided or one of the keys you provided in the `data` option.
*
* The contents and names will be replaced with the transformed values so you can use your original structure as a
* boilerplate for other projects, components, modules, or even single files.
*
* #### Helpers
* Helpers are functions you can use to transform your `{{ var }}` contents into other values without having to
* pre-define the data and use a duplicated key. Common cases are transforming name-case format
* (e.g. `MyName` &rarr; `my_name`), so these have been provided as defaults:
*
* | 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 |
*
* Any functions you provide in `helpers` option will also be available to you to make custom formatting as you see fit
* (for example, formatting a date)
*/
export async function Scaffold({ ...options }: ScaffoldConfig) {
options.output ??= process.cwd()
registerHelpers(options)
try {
options.data = { name: options.name, Name: pascalCase(options.name), ...options.data }
log(options, LogLevel.Debug, "Full config:", {
name: options.name,
templates: options.templates,
output: options.output,
createSubfolder: options.createSubFolder,
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, LogLevel.Info, "Data:", options.data)
for (let template of options.templates) {
logInitStep(options)
for (let _template of options.templates) {
try {
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"
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 + "/**/*"
}
log(options, LogLevel.Debug, "before glob")
const files = await promisify(glob)(template, {
dot: true,
debug: options.verbose === LogLevel.Debug,
nodir: true,
})
log(options, LogLevel.Debug, "after glob")
const { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template } = await getTemplateGlobInfo(
options,
_template
)
await ensureFileExists(template, isDirOrGlob)
const files = await getFileList(options, template)
for (const inputFilePath of files) {
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)
const relPath = makeRelativePath(path.dirname(removeGlob(inputFilePath).replace(nonGlobTemplate, "")))
const basePath = getBasePath(relPath)
logInputFile(options, {
origTemplate,
relPath,
template,
inputFilePath,
nonGlobTemplate,
basePath,
isDirOrGlob,
isGlob,
})
await handleTemplateFile(options, options.data, { templatePath: inputFilePath, basePath })
}
} catch (e: any) {
handleErr(e)
@@ -99,22 +93,19 @@ export async function Scaffold({ ...options }: ScaffoldConfig) {
throw e
}
}
async function handleTemplateFile(
templatePath: string,
basePath: string,
options: ScaffoldConfig,
data: Record<string, string>
data: Record<string, string>,
{ templatePath, basePath }: { templatePath: string; basePath: string }
): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
const inputPath = path.resolve(process.cwd(), templatePath)
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(options, path.join(outputDir, path.basename(inputPath)), data).toString()
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(options, data, {
templatePath,
basePath,
})
const overwrite = getOptionValueForFile(options, inputPath, data, options.overwrite ?? false)
log(
options,
LogLevel.Debug,
@@ -126,29 +117,11 @@ async function handleTemplateFile(
`\nFull output path: ${outputPath}`,
`\n`
)
const overwrite = getOptionValueForFile(options, inputPath, data, options.overwrite ?? false)
const exists = await pathExists(outputPath)
await createDirIfNotExists(path.dirname(outputPath), options)
log(options, LogLevel.Info, `Writing to ${outputPath}`)
if (!exists || overwrite) {
if (exists && overwrite) {
log(options, LogLevel.Info, `File ${outputPath} exists, overwriting`)
}
const templateBuffer = await readFile(inputPath)
const outputContents = handlebarsParse(options, templateBuffer, data)
if (!options.dryRun) {
await writeFile(outputPath, outputContents)
log(options, LogLevel.Info, "Done.")
} else {
log(options, LogLevel.Info, "Content output:")
log(options, LogLevel.Info, outputContents)
}
} else if (exists) {
log(options, LogLevel.Info, `File ${outputPath} already exists, skipping`)
}
await copyFileTransformed(options, data, { exists, overwrite, outputPath, inputPath })
resolve()
} catch (e: any) {
handleErr(e)

View File

@@ -10,6 +10,20 @@ export type FileResponseFn<T> = (fullPath: string, basedir: string, basename: st
export type FileResponse<T> = T | FileResponseFn<T>
export type DefaultHelperKeys =
| "camelCase"
| "snakeCase"
| "startCase"
| "kebabCase"
| "hyphenCase"
| "pascalCase"
| "lowerCase"
| "upperCase"
export type HelperKeys<T> = DefaultHelperKeys | T
export type Helper = (text: string) => string
export interface ScaffoldConfig {
/**
* Name to be passed to the generated files. `{{name}}` and `{{Name}}` inside contents and file names will be replaced
@@ -79,8 +93,26 @@ export interface ScaffoldConfig {
* }
* })
* ```
*
* Here are the built-in helpers available for use:
* | 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 |
*/
helpers?: Record<string, (text: string) => string>
helpers?: Record<string, Helper>
/**
* Default transformer to apply to subfolder name when using `createSubFolder: true`. Can be one of the default
* helpers, or a custom one you provide to `helpers`. Defaults to `undefined`, which means no transformation is done.
*/
subFolderNameHelper?: DefaultHelperKeys | string
}
export interface ScaffoldCmdConfig {
name: string

View File

@@ -1,6 +1,6 @@
import path from "path"
import { F_OK } from "constants"
import { FileResponse, FileResponseFn, LogLevel, ScaffoldConfig } from "./types"
import { DefaultHelperKeys, FileResponse, FileResponseFn, Helper, LogLevel, ScaffoldConfig } from "./types"
import camelCase from "lodash/camelCase"
import snakeCase from "lodash/snakeCase"
import kebabCase from "lodash/kebabCase"
@@ -10,7 +10,11 @@ import { promises as fsPromises } from "fs"
import chalk from "chalk"
const { stat, access, mkdir } = fsPromises
export const defaultHelpers: Exclude<ScaffoldConfig["helpers"], undefined> = {
import { glob } from "glob"
import { promisify } from "util"
const { readFile, writeFile } = fsPromises
export const defaultHelpers: Record<DefaultHelperKeys, Helper> = {
camelCase,
snakeCase,
startCase,
@@ -34,7 +38,7 @@ export function handleErr(err: NodeJS.ErrnoException | null) {
}
export function log(options: ScaffoldConfig, level: LogLevel, ...obj: any[]) {
if (options.quiet || options.verbose === LogLevel.None || level <= (options.verbose ?? LogLevel.Info)) {
if (options.quiet || options.verbose === LogLevel.None || level < (options.verbose ?? LogLevel.Info)) {
return
}
const levelColor: Record<LogLevel, keyof typeof chalk> = {
@@ -91,19 +95,27 @@ export function getOptionValueForFile<T>(
}
return (fn as FileResponseFn<T>)(
filePath,
path.dirname(handlebarsParse(options, filePath, data).toString()),
path.basename(handlebarsParse(options, filePath, data).toString())
path.dirname(handlebarsParse(options, filePath, { isPath: true }).toString()),
path.basename(handlebarsParse(options, filePath, { isPath: true }).toString())
)
}
export function handlebarsParse(
options: ScaffoldConfig,
templateBuffer: Buffer | string,
data: Record<string, string>
{ isPath = false }: { isPath?: boolean } = {}
) {
const { data } = options
try {
const parser = Handlebars.compile(templateBuffer.toString(), { noEscape: true })
const outputContents = parser(data)
let str = templateBuffer.toString()
if (isPath) {
str = str.replace(/\\/g, "/")
}
const parser = Handlebars.compile(str, { noEscape: true })
let outputContents = parser(data)
if (isPath && path.sep !== "/") {
outputContents = outputContents.replace(/\//g, "\\")
}
return outputContents
} catch {
log(options, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
@@ -133,9 +145,187 @@ export async function isDir(path: string): Promise<boolean> {
}
export function removeGlob(template: string) {
return template.replace(/\*/g, "").replace(/\/\//g, "/")
return template.replace(/\*/g, "").replace(/(\/\/|\\\\)/g, path.sep)
}
export function makeRelativePath(str: string): string {
return str.startsWith("/") ? str.slice(1) : str
return str.startsWith(path.sep) ? str.slice(1) : str
}
export function getBasePath(relPath: string) {
return path
.resolve(process.cwd(), relPath)
.replace(process.cwd() + path.sep, "")
.replace(process.cwd(), "")
}
export async function getFileList(options: ScaffoldConfig, template: string) {
return (
await promisify(glob)(template, {
dot: true,
debug: options.verbose === LogLevel.Debug,
nodir: true,
})
).map((f) => f.replace(/\//g, path.sep))
}
export interface GlobInfo {
nonGlobTemplate: string
origTemplate: string
isDirOrGlob: boolean
isGlob: boolean
template: string
}
export async function getTemplateGlobInfo(options: ScaffoldConfig, template: string): Promise<GlobInfo> {
const isGlob = glob.hasMagic(template)
log(options, LogLevel.Debug, "before isDir", "isGlob:", isGlob, template)
let _template = template
const nonGlobTemplate = isGlob ? removeGlob(template) : template
const isDirOrGlob = isGlob ? true : await isDir(template)
log(options, LogLevel.Debug, "after isDir", isDirOrGlob)
const _shouldAddGlob = !isGlob && isDirOrGlob
const origTemplate = template
if (_shouldAddGlob) {
_template = path.join(template, "**", "*")
}
return { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template: _template }
}
export async function ensureFileExists(template: string, isGlob: boolean) {
if (!isGlob && !(await pathExists(template))) {
const err: NodeJS.ErrnoException = new Error(`ENOENT, no such file or directory ${template}`)
err.code = "ENOENT"
err.path = template
err.errno = -2
throw err
}
}
export interface OutputFileInfo {
inputPath: string
outputPathOpt: string
outputDir: string
outputPath: string
exists: boolean
}
export async function getTemplateFileInfo(
options: ScaffoldConfig,
data: Record<string, string>,
{ templatePath, basePath }: { templatePath: string; basePath: string }
): Promise<OutputFileInfo> {
const inputPath = path.resolve(process.cwd(), templatePath)
const outputPathOpt = getOptionValueForFile(options, inputPath, data, options.output)
const outputDir = getOutputDir(options, data, outputPathOpt, basePath)
const outputPath = handlebarsParse(options, path.join(outputDir, path.basename(inputPath)), {
isPath: true,
}).toString()
const exists = await pathExists(outputPath)
return { inputPath, outputPathOpt, outputDir, outputPath, exists }
}
export async function copyFileTransformed(
options: ScaffoldConfig,
data: Record<string, string>,
{
exists,
overwrite,
outputPath,
inputPath,
}: { exists: boolean; overwrite: boolean; outputPath: string; inputPath: string }
) {
if (!exists || overwrite) {
if (exists && overwrite) {
log(options, LogLevel.Info, `File ${outputPath} exists, overwriting`)
}
const templateBuffer = await readFile(inputPath)
const outputContents = handlebarsParse(options, templateBuffer)
if (!options.dryRun) {
await writeFile(outputPath, outputContents)
log(options, LogLevel.Info, "Done.")
} else {
log(options, LogLevel.Info, "Content output:")
log(options, LogLevel.Info, outputContents)
}
} else if (exists) {
log(options, LogLevel.Info, `File ${outputPath} already exists, skipping`)
}
}
export function getOutputDir(
options: ScaffoldConfig,
data: Record<string, string>,
outputPathOpt: string,
basePath: string
) {
return path.resolve(
process.cwd(),
...([
outputPathOpt,
basePath,
options.createSubFolder
? options.subFolderNameHelper
? handlebarsParse(options, `{{ ${options.subFolderNameHelper} name }}`)
: options.name
: undefined,
].filter(Boolean) as string[])
)
}
export function logInputFile(
options: ScaffoldConfig,
{
origTemplate,
relPath,
template,
inputFilePath,
nonGlobTemplate,
basePath,
isDirOrGlob,
isGlob,
}: {
origTemplate: string
relPath: string
template: string
inputFilePath: string
nonGlobTemplate: string
basePath: string
isDirOrGlob: boolean
isGlob: boolean
}
) {
log(
options,
LogLevel.Debug,
`\nprocess.cwd(): ${process.cwd()}`,
`\norigTemplate: ${origTemplate}`,
`\nrelPath: ${relPath}`,
`\ntemplate: ${template}`,
`\ninputFilePath: ${inputFilePath}`,
`\nnonGlobTemplate: ${nonGlobTemplate}`,
`\nbasePath: ${basePath}`,
`\nisDirOrGlob: ${isDirOrGlob}`,
`\nisGlob: ${isGlob}`,
`\n`
)
}
export function logInitStep(options: ScaffoldConfig) {
log(options, LogLevel.Debug, "Full config:", {
name: options.name,
templates: options.templates,
output: options.output,
createSubfolder: options.createSubFolder,
data: options.data,
overwrite: options.overwrite,
quiet: options.quiet,
subFolderTransformHelper: options.subFolderNameHelper,
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, LogLevel.Info, "Data:", options.data)
}

View File

@@ -4,6 +4,7 @@ import Scaffold from "../src/scaffold"
import { readdirSync, readFileSync } from "fs"
import { Console } from "console"
import { defaultHelpers } from "../src/utils"
import { join } from "path"
const fileStructNormal = {
input: {
@@ -31,6 +32,12 @@ const fileStructNested = {
},
output: {},
}
const fileStructSubdirTransformer = {
input: {
"{{name}}.txt": "Hello, my app is {{name}}",
},
output: {},
}
const defaultHelperNames = Object.keys(defaultHelpers)
const fileStructHelpers = {
@@ -77,7 +84,7 @@ describe("Scaffold", () => {
templates: ["input"],
verbose: 0,
})
const data = readFileSync(process.cwd() + "/output/app_name.txt")
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toBe("Hello, my app is app_name")
})
@@ -90,7 +97,7 @@ describe("Scaffold", () => {
verbose: 0,
})
const data = readFileSync(process.cwd() + "/output/app_name/app_name.txt")
const data = readFileSync(join(process.cwd(), "output", "app_name", "app_name.txt"))
expect(data.toString()).toBe("Hello, my app is app_name")
})
})
@@ -116,7 +123,7 @@ describe("Scaffold", () => {
verbose: 0,
})
const data = readFileSync(process.cwd() + "/output/app_name.txt")
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toBe("Hello, my value is 1")
})
@@ -138,7 +145,7 @@ describe("Scaffold", () => {
verbose: 0,
})
const data = readFileSync(process.cwd() + "/output/app_name.txt")
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toBe("Hello, my value is 2")
})
})
@@ -167,7 +174,7 @@ describe("Scaffold", () => {
})
).rejects.toThrow()
expect(() => readFileSync(process.cwd() + "/output/app_name.txt")).toThrow()
expect(() => readFileSync(join(process.cwd(), "output", "app_name.txt"))).toThrow()
})
})
)
@@ -178,12 +185,12 @@ describe("Scaffold", () => {
test("should allow override function", async () => {
await Scaffold({
name: "app_name",
output: (fullPath, basedir, basename) => `custom-output/${basename.split(".")[0]}`,
output: (fullPath, basedir, basename) => join("custom-output", `${basename.split(".")[0]}`),
templates: ["input"],
data: { value: "1" },
verbose: 0,
})
const data = readFileSync(process.cwd() + "/custom-output/app_name/app_name.txt")
const data = readFileSync(join(process.cwd(), "/custom-output/app_name/app_name.txt"))
expect(data.toString()).toBe("Hello, my app is app_name")
})
})
@@ -201,16 +208,16 @@ describe("Scaffold", () => {
verbose: 0,
})
const rootDir = readdirSync(process.cwd() + "/output")
const dir = readdirSync(process.cwd() + "/output/AppName")
const nestedDir = readdirSync(process.cwd() + "/output/AppName/moreNesting")
const rootDir = readdirSync(join(process.cwd(), "output"))
const dir = readdirSync(join(process.cwd(), "output", "AppName"))
const nestedDir = readdirSync(join(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")
const rootFile = readFileSync(join(process.cwd(), "output", "app_name-1.txt"))
const oneDeepFile = readFileSync(join(process.cwd(), "output", "AppName/app_name-2.txt"))
const twoDeepFile = readFileSync(join(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!")
@@ -246,7 +253,7 @@ describe("Scaffold", () => {
upperCase: "APP_NAME",
}
for (const key in results) {
const file = readFileSync(process.cwd() + `/output/defaults/${key}.txt`)
const file = readFileSync(join(process.cwd(), "output", "defaults", `${key}.txt`))
expect(file.toString()).toEqual(results[key as keyof typeof results])
}
})
@@ -265,11 +272,59 @@ describe("Scaffold", () => {
add1: "app_name 1",
}
for (const key in results) {
const file = readFileSync(process.cwd() + `/output/custom/${key}.txt`)
const file = readFileSync(join(process.cwd(), "output", "custom", `${key}.txt`))
expect(file.toString()).toEqual(results[key as keyof typeof results])
}
})
})
})
)
describe(
"transform subfolder",
withMock(fileStructSubdirTransformer, () => {
test("should work with no helper", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
createSubFolder: true,
verbose: 0,
})
const data = readFileSync(join(process.cwd(), "output", "app_name/app_name.txt"))
expect(data.toString()).toBe("Hello, my app is app_name")
})
test("should work with default helper", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
createSubFolder: true,
verbose: 0,
subFolderNameHelper: "upperCase",
})
const data = readFileSync(join(process.cwd(), "output", "APP_NAME/app_name.txt"))
expect(data.toString()).toBe("Hello, my app is app_name")
})
test("should work with custom helper", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
createSubFolder: true,
verbose: 0,
subFolderNameHelper: "test",
helpers: {
test: () => "REPLACED",
},
})
const data = readFileSync(join(process.cwd(), "output", "REPLACED/app_name.txt"))
expect(data.toString()).toBe("Hello, my app is app_name")
})
})
)
})

54
tests/utils.test.ts Normal file
View File

@@ -0,0 +1,54 @@
import { handlebarsParse } from "../src/utils"
import { ScaffoldConfig } from "../src/types"
import path from "path"
const blankConf: ScaffoldConfig = {
verbose: 0,
name: "",
output: "",
templates: [],
data: { name: "test" },
}
describe("Utils", () => {
describe("handlebarsParse", () => {
let origSep: any
describe("windows paths", () => {
beforeAll(() => {
origSep = path.sep
Object.defineProperty(path, "sep", { value: "\\" })
})
afterAll(() => {
Object.defineProperty(path, "sep", { value: origSep })
})
test("should work for windows paths", async () => {
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { isPath: true })).toEqual(
"C:\\exports\\test.txt"
)
})
})
describe("non-windows paths", () => {
beforeAll(() => {
origSep = path.sep
Object.defineProperty(path, "sep", { value: "/" })
})
afterAll(() => {
Object.defineProperty(path, "sep", { value: origSep })
})
test("should work for non-windows paths", async () => {
expect(handlebarsParse(blankConf, "/home/test/{{name}}.txt", { isPath: true })).toEqual("/home/test/test.txt")
})
})
test("should not do path escaping on non-path compiles", async () => {
expect(
handlebarsParse(
{ ...blankConf, data: { ...blankConf.data, escaped: "value" } },
"/home/test/{{name}} \\{{escaped}}.txt",
{
isPath: false,
}
)
).toEqual("/home/test/test {{escaped}}.txt")
})
})
})

View File

@@ -759,16 +759,6 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
args@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/args/-/args-5.0.1.tgz#4bf298df90a4799a09521362c579278cc2fdd761"
integrity sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==
dependencies:
camelcase "5.0.0"
chalk "2.4.2"
leven "2.1.0"
mri "1.1.4"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -903,11 +893,6 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
camelcase@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
@@ -923,7 +908,7 @@ caniuse-lite@^1.0.30001219:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001245.tgz#45b941bbd833cb0fa53861ff2bae746b3c6ca5d4"
integrity sha512-768fM9j1PKXpOCKws6eTo3RHmvTUsG9UrpT4WoREFeZgJBTi4/X9g565azS/rVUGtqb8nt7FjLeF5u4kukERnA==
chalk@2.4.2, chalk@^2.0.0:
chalk@^2.0.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -2105,11 +2090,6 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
leven@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA=
leven@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@@ -2207,9 +2187,9 @@ minimatch@^3.0.4:
brace-expansion "^1.1.7"
minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp@1.x:
version "1.0.4"
@@ -2221,11 +2201,6 @@ mock-fs@^5.0.0:
resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.0.0.tgz#5574520ac824c01a10091bf951c66f677c71acaa"
integrity sha512-A5mm/SpSDwwc/klSaEvvKMGQQtiGiQy8UcDAd/vpVO1fV+4zaHjt39yKgCSErFzv2zYxZIUx9Ud/7ybeHBf8Fg==
mri@1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a"
integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"