\| String \| Buffer \| undefined` | Supply this function to override the final output contents of each of your files, allowing you to add more pre-processing to your generator pipeline. The return value of this function will replace the output content of the respective file, which you may discriminate (if needed) using the `outputPath` argument. |
## Preparing files
@@ -145,11 +171,11 @@ Examples:
> In the following examples, the config `name` is `AppName`, and the config `output` is `src`.
-| Input template | Output path(s) |
-| ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
-| `./templates/{{ name }}.txt` | `src/AppName.txt` |
-| `./templates/directory`
Directory contents:
- `outer/{{name}}.txt`
- `outer2/inner/{{name.txt}}`
| `src/outer/AppName.txt`,
`src/outer2/inner/AppName.txt` |
-| `./templates/others/**/*.txt`
Directory contents:
- `outer/{{name}}.jpg`
- `outer2/inner/{{name.txt}}`
| `src/outer2/inner/AppName.txt` |
+| Input template | Files in template | Output path(s) |
+| ----------------------------- | ------------------------------------------------------ | ------------------------------------------------------------ |
+| `./templates/{{ name }}.txt` | `./templates/{{ name }}.txt` | `src/AppName.txt` |
+| `./templates/directory` | `outer/{{name}}.txt`,
`outer2/inner/{{name.txt}}` | `src/outer/AppName.txt`,
`src/outer2/inner/AppName.txt` |
+| `./templates/others/**/*.txt` | `outer/{{name}}.jpg`,
`outer2/inner/{{name.txt}}` | `src/outer2/inner/AppName.txt` |
### Variable/token replacement
@@ -165,7 +191,7 @@ For example, using the following command:
npx simple-scaffold@latest \
--templates templates/components/{{name}}.jsx \
--output src/components \
- -create-sub-folder true \
+ --create-sub-folder true \
MyComponent
```
@@ -188,28 +214,58 @@ Your `data` will be pre-populated with the following:
> [Handlebars.js Language Features](https://handlebarsjs.com/guide/#language-features) for more
> information.
-#### Helpers
+### Built-in Helpers
Simple-Scaffold provides some built-in text transformation filters usable by handleBars.
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:
+#### Capitalization Helpers
-| 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 }}` | MY NAME |
-| lowerCase | `{{ lowerCase name }}` | my name |
+| 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 }}` | MY NAME |
+| `lowerCase` | `{{ lowerCase name }}` | my name |
-> These helpers are available for any data property, not exclusive to `name`.
+#### Date helpers
+
+| Helper name | Description | Example code | Example output |
+| -------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------ |
+| `now` | Current date with format | `{{ now "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
+| `now` (with offset) | Current date with format, and with offset | `{{ now "yyyy-MM-dd HH:mm" -1 "hours" }}` | `2042-01-01 14:00` |
+| `date` | Custom date with format | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
+| `date` (with offset) | Custom date with format, and with offset | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" -1 "days" }}` | `2041-31-12 15:00` |
+| `date` (with date from `--data`) | Custom date with format, with data from the `data` config option | `{{ date myCustomDate "yyyy-MM-dd HH:mm" }}` | `2042-01-01 12:00` |
+
+Further details:
+
+- We use [`date-fns`](https://date-fns.org/docs/) for parsing/manipulating the dates.
+ If you want more information on the date tokens to use, refer to
+ [their format documentation](https://date-fns.org/docs/format).
+
+- The date helper format takes the following arguments:
+
+ ```typescript
+ (
+ date: string,
+ format: string,
+ offsetAmount?: number,
+ offsetType?: "years" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds"
+ )
+ ```
+
+- **The now helper** (for current time) takes the same arguments, minus the first one (`date`) as
+ it is implicitly the current date.
+
+### Custom Helpers
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
@@ -221,8 +277,11 @@ config.helpers = {
}
```
-These helpers will also be available to you when using `subFolderNameHelper` or
-`--sub-folder-name-helper` as a possible value.
+All of the above helpers (built in and custom) will also be available to you when using
+`subFolderNameHelper` (`--sub-folder-name-helper`/`-sh`) as a possible value.
+
+> To see more information on how helpers work and more features, see
+> [Handlebars.js docs](https://handlebarsjs.com/guide/#custom-helpers).
## Examples
@@ -254,7 +313,7 @@ simple-scaffold MyComponent \
```typescriptreact
import React from 'react'
-export default {{camelCase ame}}: React.FC = (props) => {
+export default {{camelCase name}}: React.FC = (props) => {
return (
{{camelCase name}} Component
)
@@ -298,6 +357,16 @@ export default MyComponent: React.FC = (props) => {
## Contributing
+I am developing this package on my free time, so any support, whether code, issues, or just stars
+is very helpful to sustaining its life. If you would like to donate a bit to help keep the project
+alive, I would be very thankful!
+
+
+
+
+
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.
diff --git a/package.json b/package.json
index 4299ce8..e0da4d7 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,23 @@
{
"name": "simple-scaffold",
- "version": "1.0.4",
+ "version": "1.1.0",
"description": "Create files based on templates",
"repository": "https://github.com/chenasraf/simple-scaffold.git",
"author": "Chen Asraf ",
"license": "MIT",
"main": "index.js",
"bin": "cmd.js",
- "keywords": ["javascript", "cli", "template", "files", "typescript", "generator", "scaffold", "file", "scaffolding"],
+ "keywords": [
+ "javascript",
+ "cli",
+ "template",
+ "files",
+ "typescript",
+ "generator",
+ "scaffold",
+ "file",
+ "scaffolding"
+ ],
"scripts": {
"clean": "rimraf dist/",
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
@@ -20,6 +30,7 @@
},
"dependencies": {
"chalk": "^4.1.2",
+ "date-fns": "^2.28.0",
"glob": "^7.1.3",
"handlebars": "^4.7.7",
"lodash": "^4.17.21",
@@ -27,7 +38,6 @@
"util.promisify": "^1.1.1"
},
"devDependencies": {
- "@types/args": "^3.0.1",
"@types/glob": "^7.1.1",
"@types/jest": "^26.0.24",
"@types/lodash": "^4.14.171",
diff --git a/src/scaffold.ts b/src/scaffold.ts
index a91e023..b6af43d 100644
--- a/src/scaffold.ts
+++ b/src/scaffold.ts
@@ -11,7 +11,6 @@ import {
makeRelativePath,
registerHelpers,
getTemplateGlobInfo,
- ensureFileExists,
getFileList,
getBasePath,
copyFileTransformed,
@@ -51,7 +50,7 @@ import { LogLevel, ScaffoldConfig } from "./types"
* 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) {
+export async function Scaffold({ ...options }: ScaffoldConfig): Promise {
options.output ??= process.cwd()
registerHelpers(options)
@@ -64,7 +63,6 @@ export async function Scaffold({ ...options }: ScaffoldConfig) {
options,
_template
)
- await ensureFileExists(template, isDirOrGlob)
const files = await getFileList(options, template)
for (const inputFilePath of files) {
if (await isDir(inputFilePath)) {
@@ -82,7 +80,10 @@ export async function Scaffold({ ...options }: ScaffoldConfig) {
isDirOrGlob,
isGlob,
})
- await handleTemplateFile(options, options.data, { templatePath: inputFilePath, basePath })
+ await handleTemplateFile(options, options.data, {
+ templatePath: inputFilePath,
+ basePath,
+ })
}
} catch (e: any) {
handleErr(e)
@@ -104,7 +105,7 @@ async function handleTemplateFile(
templatePath,
basePath,
})
- const overwrite = getOptionValueForFile(options, inputPath, data, options.overwrite ?? false)
+ const overwrite = getOptionValueForFile(options, inputPath, options.overwrite ?? false)
log(
options,
@@ -121,7 +122,7 @@ async function handleTemplateFile(
await createDirIfNotExists(path.dirname(outputPath), options)
log(options, LogLevel.Info, `Writing to ${outputPath}`)
- await copyFileTransformed(options, data, { exists, overwrite, outputPath, inputPath })
+ await copyFileTransformed(options, { exists, overwrite, outputPath, inputPath })
resolve()
} catch (e: any) {
handleErr(e)
diff --git a/src/types.ts b/src/types.ts
index e5b6ed5..9f9baf8 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,3 +1,5 @@
+import { HelperDelegate } from "handlebars/runtime"
+
export enum LogLevel {
None = 0,
Debug = 1,
@@ -12,17 +14,19 @@ export type FileResponse = T | FileResponseFn
export type DefaultHelperKeys =
| "camelCase"
+ | "date"
+ | "hyphenCase"
+ | "kebabCase"
+ | "lowerCase"
+ | "now"
+ | "pascalCase"
| "snakeCase"
| "startCase"
- | "kebabCase"
- | "hyphenCase"
- | "pascalCase"
- | "lowerCase"
| "upperCase"
export type HelperKeys = DefaultHelperKeys | T
-export type Helper = (text: string) => string
+export type Helper = HelperDelegate
export interface ScaffoldConfig {
/**
@@ -94,17 +98,7 @@ 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 |
+ * See the list of all the built-in available helpers, or how to define your own in the [readme](https://github.com/chenasraf/simple-scaffold#built-in-helpers).
*/
helpers?: Record
@@ -113,6 +107,26 @@ export interface ScaffoldConfig {
* helpers, or a custom one you provide to `helpers`. Defaults to `undefined`, which means no transformation is done.
*/
subFolderNameHelper?: DefaultHelperKeys | string
+
+ /**
+ * This callback runs right before content is being written to the disk. If you supply this function, you may return
+ * a string that represents the final content of your file, you may process the content as you see fit. For example,
+ * you may run formatters on a file, fix output in edge-cases not supported by helpers or data, etc.
+ *
+ * If the return value of this function is `undefined`, the original content will be used.
+ *
+ * @param content The original template after token replacement
+ * @param rawContent The original template before token replacement
+ * @param outputPath The final output path of the processed file
+ *
+ * @returns `String | Buffer | undefined` The final output of the file contents-only, after further modifications -
+ * or `undefined` to use the original content (i.e. `content.toString()`)
+ */
+ beforeWrite?(
+ content: Buffer,
+ rawContent: Buffer,
+ outputPath: string
+ ): string | Buffer | undefined | Promise
}
export interface ScaffoldCmdConfig {
name: string
diff --git a/src/utils.ts b/src/utils.ts
index c6dc942..36a6c9a 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -9,6 +9,15 @@ import Handlebars from "handlebars"
import { promises as fsPromises } from "fs"
import chalk from "chalk"
const { stat, access, mkdir } = fsPromises
+import dtAdd from "date-fns/add"
+import dtFormat from "date-fns/format"
+import dtParseISO from "date-fns/parseISO"
+
+const dateFns = {
+ add: dtAdd,
+ format: dtFormat,
+ parseISO: dtParseISO,
+}
import { glob } from "glob"
import { promisify } from "util"
@@ -23,9 +32,53 @@ export const defaultHelpers: Record = {
pascalCase,
lowerCase: (text) => text.toLowerCase(),
upperCase: (text) => text.toUpperCase(),
+ now: nowHelper,
+ date: dateHelper,
}
-export function registerHelpers(options: ScaffoldConfig) {
+export function _dateHelper(date: Date, formatString: string): string
+export function _dateHelper(
+ date: Date,
+ formatString: string,
+ durationDifference: number,
+ durationType: keyof Duration
+): string
+export function _dateHelper(
+ date: Date,
+ formatString: string,
+ durationDifference?: number,
+ durationType?: keyof Duration
+): string {
+ if (durationType && durationDifference !== undefined) {
+ return dateFns.format(dateFns.add(date, { [durationType]: durationDifference }), formatString)
+ }
+ return dateFns.format(date, formatString)
+}
+
+export function nowHelper(formatString: string): string
+export function nowHelper(formatString: string, durationDifference: number, durationType: keyof Duration): string
+export function nowHelper(formatString: string, durationDifference?: number, durationType?: keyof Duration): string {
+ return _dateHelper(new Date(), formatString, durationDifference!, durationType!)
+}
+
+export function dateHelper(date: string, formatString: string): string
+export function dateHelper(
+ date: string,
+ formatString: string,
+ durationDifference: number,
+ durationType: keyof Duration
+): string
+
+export function dateHelper(
+ date: string,
+ formatString: string,
+ durationDifference?: number,
+ durationType?: keyof Duration
+): string {
+ return _dateHelper(dateFns.parseISO(date), formatString, durationDifference!, durationType!)
+}
+
+export function registerHelpers(options: ScaffoldConfig): void {
const _helpers = { ...defaultHelpers, ...options.helpers }
for (const helperName in _helpers) {
log(options, LogLevel.Debug, `Registering helper: ${helperName}`)
@@ -33,11 +86,11 @@ export function registerHelpers(options: ScaffoldConfig) {
}
}
-export function handleErr(err: NodeJS.ErrnoException | null) {
+export function handleErr(err: NodeJS.ErrnoException | null): void {
if (err) throw err
}
-export function log(options: ScaffoldConfig, level: LogLevel, ...obj: any[]) {
+export function log(options: ScaffoldConfig, level: LogLevel, ...obj: any[]): void {
if (options.quiet || options.verbose === LogLevel.None || level < (options.verbose ?? LogLevel.Info)) {
return
}
@@ -86,7 +139,6 @@ export async function createDirIfNotExists(dir: string, options: ScaffoldConfig)
export function getOptionValueForFile(
options: ScaffoldConfig,
filePath: string,
- data: Record,
fn: FileResponse,
defaultValue?: T
): T {
@@ -104,7 +156,7 @@ export function handlebarsParse(
options: ScaffoldConfig,
templateBuffer: Buffer | string,
{ isPath = false }: { isPath?: boolean } = {}
-) {
+): Buffer {
const { data } = options
try {
let str = templateBuffer.toString()
@@ -116,11 +168,11 @@ export function handlebarsParse(
if (isPath && path.sep !== "/") {
outputContents = outputContents.replace(/\//g, "\\")
}
- return outputContents
+ return Buffer.from(outputContents)
} catch (e) {
log(options, LogLevel.Debug, e)
log(options, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
- return templateBuffer
+ return Buffer.from(templateBuffer)
}
}
@@ -145,7 +197,7 @@ export async function isDir(path: string): Promise {
return tplStat.isDirectory()
}
-export function removeGlob(template: string) {
+export function removeGlob(template: string): string {
return template.replace(/\*/g, "").replace(/(\/\/|\\\\)/g, path.sep)
}
@@ -153,14 +205,14 @@ export function makeRelativePath(str: string): string {
return str.startsWith(path.sep) ? str.slice(1) : str
}
-export function getBasePath(relPath: string) {
+export function getBasePath(relPath: string): string {
return path
.resolve(process.cwd(), relPath)
.replace(process.cwd() + path.sep, "")
.replace(process.cwd(), "")
}
-export async function getFileList(options: ScaffoldConfig, template: string) {
+export async function getFileList(options: ScaffoldConfig, template: string): Promise {
return (
await promisify(glob)(template, {
dot: true,
@@ -193,16 +245,6 @@ export async function getTemplateGlobInfo(options: ScaffoldConfig, template: str
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
@@ -217,8 +259,8 @@ export async function getTemplateFileInfo(
{ templatePath, basePath }: { templatePath: string; basePath: string }
): Promise {
const inputPath = path.resolve(process.cwd(), templatePath)
- const outputPathOpt = getOptionValueForFile(options, inputPath, data, options.output)
- const outputDir = getOutputDir(options, data, outputPathOpt, basePath)
+ const outputPathOpt = getOptionValueForFile(options, inputPath, options.output)
+ const outputDir = getOutputDir(options, outputPathOpt, basePath)
const outputPath = handlebarsParse(options, path.join(outputDir, path.basename(inputPath)), {
isPath: true,
}).toString()
@@ -228,39 +270,41 @@ export async function getTemplateFileInfo(
export async function copyFileTransformed(
options: ScaffoldConfig,
- data: Record,
{
exists,
overwrite,
outputPath,
inputPath,
- }: { exists: boolean; overwrite: boolean; outputPath: string; inputPath: string }
-) {
+ }: {
+ exists: boolean
+ overwrite: boolean
+ outputPath: string
+ inputPath: string
+ }
+): Promise {
if (!exists || overwrite) {
if (exists && overwrite) {
log(options, LogLevel.Info, `File ${outputPath} exists, overwriting`)
}
const templateBuffer = await readFile(inputPath)
- const outputContents = handlebarsParse(options, templateBuffer)
+ const unprocessedOutputContents = handlebarsParse(options, templateBuffer)
+ const finalOutputContents = (
+ (await options.beforeWrite?.(unprocessedOutputContents, templateBuffer, outputPath)) ?? unprocessedOutputContents
+ ).toString()
if (!options.dryRun) {
- await writeFile(outputPath, outputContents)
+ await writeFile(outputPath, finalOutputContents)
log(options, LogLevel.Info, "Done.")
} else {
log(options, LogLevel.Info, "Content output:")
- log(options, LogLevel.Info, outputContents)
+ log(options, LogLevel.Info, finalOutputContents)
}
} else if (exists) {
log(options, LogLevel.Info, `File ${outputPath} already exists, skipping`)
}
}
-export function getOutputDir(
- options: ScaffoldConfig,
- data: Record,
- outputPathOpt: string,
- basePath: string
-) {
+export function getOutputDir(options: ScaffoldConfig, outputPathOpt: string, basePath: string): string {
return path.resolve(
process.cwd(),
...([
@@ -268,7 +312,7 @@ export function getOutputDir(
basePath,
options.createSubFolder
? options.subFolderNameHelper
- ? handlebarsParse(options, `{{ ${options.subFolderNameHelper} name }}`)
+ ? handlebarsParse(options, `{{ ${options.subFolderNameHelper} name }}`).toString()
: options.name
: undefined,
].filter(Boolean) as string[])
@@ -296,7 +340,7 @@ export function logInputFile(
isDirOrGlob: boolean
isGlob: boolean
}
-) {
+): void {
log(
options,
LogLevel.Debug,
@@ -313,7 +357,7 @@ export function logInputFile(
)
}
-export function logInitStep(options: ScaffoldConfig) {
+export function logInitStep(options: ScaffoldConfig): void {
log(options, LogLevel.Debug, "Full config:", {
name: options.name,
templates: options.templates,
diff --git a/tests/scaffold.test.ts b/tests/scaffold.test.ts
index c68ffbd..e65622c 100644
--- a/tests/scaffold.test.ts
+++ b/tests/scaffold.test.ts
@@ -5,6 +5,7 @@ import { readdirSync, readFileSync } from "fs"
import { Console } from "console"
import { defaultHelpers } from "../src/utils"
import { join } from "path"
+import * as dateFns from "date-fns"
const fileStructNormal = {
input: {
@@ -52,8 +53,16 @@ const fileStructHelpers = {
},
output: {},
}
-// let logsTemp: any = []
-// let logMock: any
+
+const fileStructDates = {
+ input: {
+ "now.txt": "Today is {{ now 'mmm' }}, time is {{ now 'HH:mm' }}",
+ "offset.txt": "Yesterday was {{ now 'mmm' -1 'days' }}, time is {{ now 'HH:mm' -1 'days' }}",
+ "custom.txt": "Custom date is {{ date customDate 'mmm' }}, time is {{ date customDate 'HH:mm' }}",
+ },
+ output: {},
+}
+
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunction): jest.EmptyFunction {
return () => {
beforeEach(() => {
@@ -85,7 +94,7 @@ describe("Scaffold", () => {
verbose: 0,
})
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
- expect(data.toString()).toBe("Hello, my app is app_name")
+ expect(data.toString()).toEqual("Hello, my app is app_name")
})
test("should create with config", async () => {
@@ -98,7 +107,7 @@ describe("Scaffold", () => {
})
const data = readFileSync(join(process.cwd(), "output", "app_name", "app_name.txt"))
- expect(data.toString()).toBe("Hello, my app is app_name")
+ expect(data.toString()).toEqual("Hello, my app is app_name")
})
})
)
@@ -124,7 +133,7 @@ describe("Scaffold", () => {
})
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
- expect(data.toString()).toBe("Hello, my value is 1")
+ expect(data.toString()).toEqual("Hello, my value is 1")
})
test("should overwrite with config", async () => {
@@ -146,7 +155,7 @@ describe("Scaffold", () => {
})
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
- expect(data.toString()).toBe("Hello, my value is 2")
+ expect(data.toString()).toEqual("Hello, my value is 2")
})
})
)
@@ -174,6 +183,43 @@ describe("Scaffold", () => {
})
).rejects.toThrow()
+ await expect(
+ Scaffold({
+ name: "app_name",
+ output: "output",
+ templates: ["non-existing-input/non-existing-file.txt"],
+ data: { value: "1" },
+ verbose: 0,
+ })
+ ).rejects.toThrow()
+
+ expect(() => readFileSync(join(process.cwd(), "output", "app_name.txt"))).toThrow()
+ })
+ })
+ )
+
+ describe(
+ "dry run",
+ withMock(fileStructNormal, () => {
+ let consoleMock1: jest.SpyInstance
+ beforeAll(() => {
+ consoleMock1 = jest.spyOn(console, "error").mockImplementation(() => void 0)
+ })
+
+ afterAll(() => {
+ consoleMock1.mockRestore()
+ })
+
+ test("should not write to disk", async () => {
+ Scaffold({
+ name: "app_name",
+ output: "output",
+ templates: ["input"],
+ data: { value: "1" },
+ verbose: 0,
+ dryRun: true,
+ })
+
expect(() => readFileSync(join(process.cwd(), "output", "app_name.txt"))).toThrow()
})
})
@@ -191,7 +237,7 @@ describe("Scaffold", () => {
verbose: 0,
})
const data = readFileSync(join(process.cwd(), "/custom-output/app_name/app_name.txt"))
- expect(data.toString()).toBe("Hello, my app is app_name")
+ expect(data.toString()).toEqual("Hello, my app is app_name")
})
})
)
@@ -226,56 +272,99 @@ describe("Scaffold", () => {
)
describe(
- "helpers",
+ "capitalization helpers",
withMock(fileStructHelpers, () => {
const _helpers: Record 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(join(process.cwd(), "output", "defaults", `${key}.txt`))
- expect(file.toString()).toEqual(results[key as keyof typeof results])
- }
+ 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(join(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,
- })
+ })
+ )
+ describe(
+ "date helpers",
+ withMock(fileStructDates, () => {
+ test("should work", async () => {
+ const now = new Date()
+ const yesterday = dateFns.add(new Date(), { days: -1 })
+ const customDate = dateFns.formatISO(dateFns.add(new Date(), { days: -1 }))
- const results = {
- add1: "app_name 1",
- }
- for (const key in results) {
- const file = readFileSync(join(process.cwd(), "output", "custom", `${key}.txt`))
- expect(file.toString()).toEqual(results[key as keyof typeof results])
- }
+ await Scaffold({
+ name: "app_name",
+ output: "output",
+ templates: ["input"],
+ verbose: 0,
+ data: { customDate },
})
+
+ const nowFile = readFileSync(join(process.cwd(), "output", "now.txt"))
+ const offsetFile = readFileSync(join(process.cwd(), "output", "offset.txt"))
+ const customFile = readFileSync(join(process.cwd(), "output", "custom.txt"))
+
+ // "now.txt": "Today is {{ now 'mmm' }}, time is {{ now 'HH:mm' }}",
+ // "offset.txt": "Yesterday was {{ now 'mmm' -1 'days' }}, time is {{ now 'HH:mm' -1 'days' }}",
+ // "custom.txt": "Custom date is {{ date customDate 'mmm' }}, time is {{ date customDate 'HH:mm' }}",
+
+ expect(nowFile.toString()).toEqual(
+ `Today is ${dateFns.format(now, "mmm")}, time is ${dateFns.format(now, "HH:mm")}`
+ )
+ expect(offsetFile.toString()).toEqual(
+ `Yesterday was ${dateFns.format(yesterday, "mmm")}, time is ${dateFns.format(yesterday, "HH:mm")}`
+ )
+ expect(customFile.toString()).toEqual(
+ `Custom date is ${dateFns.format(dateFns.parseISO(customDate), "mmm")}, time is ${dateFns.format(
+ dateFns.parseISO(customDate),
+ "HH:mm"
+ )}`
+ )
+ })
+ })
+ )
+ describe(
+ "custom helpers",
+ withMock(fileStructHelpers, () => {
+ const _helpers: Record string> = {
+ add1: (text) => text + " 1",
+ }
+ 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(join(process.cwd(), "output", "custom", `${key}.txt`))
+ expect(file.toString()).toEqual(results[key as keyof typeof results])
+ }
})
})
)
@@ -291,8 +380,8 @@ describe("Scaffold", () => {
verbose: 0,
})
- const data = readFileSync(join(process.cwd(), "output", "app_name/app_name.txt"))
- expect(data.toString()).toBe("Hello, my app is app_name")
+ const data = readFileSync(join(process.cwd(), "output", "app_name", "app_name.txt"))
+ expect(data.toString()).toEqual("Hello, my app is app_name")
})
test("should work with default helper", async () => {
@@ -305,8 +394,8 @@ describe("Scaffold", () => {
subFolderNameHelper: "upperCase",
})
- const data = readFileSync(join(process.cwd(), "output", "APP_NAME/app_name.txt"))
- expect(data.toString()).toBe("Hello, my app is app_name")
+ const data = readFileSync(join(process.cwd(), "output", "APP_NAME", "app_name.txt"))
+ expect(data.toString()).toEqual("Hello, my app is app_name")
})
test("should work with custom helper", async () => {
@@ -322,8 +411,65 @@ describe("Scaffold", () => {
},
})
- const data = readFileSync(join(process.cwd(), "output", "REPLACED/app_name.txt"))
- expect(data.toString()).toBe("Hello, my app is app_name")
+ const data = readFileSync(join(process.cwd(), "output", "REPLACED", "app_name.txt"))
+ expect(data.toString()).toEqual("Hello, my app is app_name")
+ })
+ })
+ )
+ describe(
+ "before write",
+ withMock(fileStructNormal, () => {
+ test("should work with no callback", async () => {
+ await Scaffold({
+ name: "app_name",
+ output: "output",
+ templates: ["input"],
+ verbose: 0,
+ data: {
+ value: "value",
+ },
+ })
+
+ const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
+ expect(data.toString()).toEqual("Hello, my app is app_name")
+ })
+
+ test("should work with custom callback", async () => {
+ await Scaffold({
+ name: "app_name",
+ output: "output",
+ templates: ["input"],
+ verbose: 0,
+ data: {
+ value: "value",
+ },
+ beforeWrite: (content, beforeContent, outputPath) =>
+ [content.toString().toUpperCase(), beforeContent, outputPath].join(", "),
+ })
+
+ const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
+ expect(data.toString()).toEqual(
+ [
+ "Hello, my app is app_name".toUpperCase(),
+ fileStructNormal.input["{{name}}.txt"],
+ join(process.cwd(), "output", "app_name.txt"),
+ ].join(", ")
+ )
+ })
+ test("should work with undefined response custom callback", async () => {
+ await Scaffold({
+ name: "app_name",
+ output: "output",
+ templates: ["input"],
+ verbose: 0,
+ data: {
+ value: "value",
+ },
+ beforeWrite: () => undefined,
+ })
+
+ const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
+ expect(data.toString()).toEqual("Hello, my app is app_name")
})
})
)
diff --git a/tests/utils.test.ts b/tests/utils.test.ts
index c9d071b..d3df48e 100644
--- a/tests/utils.test.ts
+++ b/tests/utils.test.ts
@@ -1,6 +1,7 @@
-import { handlebarsParse } from "../src/utils"
+import { dateHelper, handlebarsParse, nowHelper } from "../src/utils"
import { ScaffoldConfig } from "../src/types"
import path from "path"
+import * as dateFns from "date-fns"
const blankConf: ScaffoldConfig = {
verbose: 0,
@@ -23,7 +24,7 @@ describe("Utils", () => {
})
test("should work for windows paths", async () => {
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { isPath: true })).toEqual(
- "C:\\exports\\test.txt"
+ Buffer.from("C:\\exports\\test.txt")
)
})
})
@@ -36,7 +37,9 @@ describe("Utils", () => {
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")
+ expect(handlebarsParse(blankConf, "/home/test/{{name}}.txt", { isPath: true })).toEqual(
+ Buffer.from("/home/test/test.txt")
+ )
})
})
test("should not do path escaping on non-path compiles", async () => {
@@ -48,7 +51,41 @@ describe("Utils", () => {
isPath: false,
}
)
- ).toEqual("/home/test/test {{escaped}}.txt")
+ ).toEqual(Buffer.from("/home/test/test {{escaped}}.txt"))
+ })
+ })
+
+ describe("Helpers", () => {
+ describe("date helpers", () => {
+ describe("now", () => {
+ test("should work without extra params", () => {
+ const now = new Date()
+ const fmt = "yyyy-MM-dd HH:mm"
+
+ expect(nowHelper(fmt)).toEqual(dateFns.format(now, fmt))
+ })
+ })
+
+ describe("date", () => {
+ test("should work with no offset params", () => {
+ const now = new Date()
+ const fmt = "yyyy-MM-dd HH:mm"
+
+ expect(dateHelper(now.toISOString(), fmt)).toEqual(dateFns.format(now, fmt))
+ })
+
+ test("should work with offset params", () => {
+ const now = new Date()
+ const fmt = "yyyy-MM-dd HH:mm"
+
+ expect(dateHelper(now.toISOString(), fmt, -1, "days")).toEqual(
+ dateFns.format(dateFns.add(now, { days: -1 }), fmt)
+ )
+ expect(dateHelper(now.toISOString(), fmt, 1, "months")).toEqual(
+ dateFns.format(dateFns.add(now, { months: 1 }), fmt)
+ )
+ })
+ })
})
})
})
diff --git a/yarn.lock b/yarn.lock
index fcd60b6..d3ca4f2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -537,11 +537,6 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.1.tgz#a6ca6a9a0ff366af433f42f5f0e124794ff6b8f1"
integrity sha512-FTgBI767POY/lKNDNbIzgAX6miIDBs6NTCbdlDb8TrWovHsSvaVIZDlTqym29C6UqhzwcJx4CYr+AlrMywA0cA==
-"@types/args@^3.0.1":
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/@types/args/-/args-3.0.1.tgz#59e8e787ed8a810408dd9a324157cc20b7001468"
- integrity sha512-Mfre8T2comeJbw2D6W8mzQP+0Q8fpS7nkbHgatzU31tWsLs0Lkyc+ObdYfgV4SuMZn/n5MEWlxh2rc25125s0Q==
-
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14":
version "7.1.15"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.15.tgz#2ccfb1ad55a02c83f8e0ad327cbc332f55eb1024"
@@ -1055,6 +1050,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
+date-fns@^2.28.0:
+ version "2.28.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
+ integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
+
debug@4, debug@^4.1.0, debug@^4.1.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"