Compare commits

...

11 Commits

Author SHA1 Message Date
429f12d1b8 chore(master): release 2.3.3 2025-06-19 07:50:55 +03:00
dcba30689b chore: update formatting & lints 2025-06-19 01:28:15 +03:00
7745385573 chore: update dependencies 2025-06-19 01:28:03 +03:00
29f2afe097 test: add cli precedence test 2025-06-19 01:27:50 +03:00
4b0b4e7380 fix: config CLI precedence over file 2025-06-19 00:51:40 +03:00
Chen Asraf
c1536839e3 chore(master): release 2.3.2 2024-10-27 03:32:33 +02:00
7e029fd122 chore: update deps 2024-10-27 01:18:18 +02:00
41f4ca52f1 fix: template config from CLI 2024-10-27 01:12:43 +02:00
Chen Asraf
78d6bf186d chore(master): release 2.3.1 2024-10-03 14:16:18 +03:00
Chen Asraf
80c92bfe84 fix: strip tmpDir from output dir (#108)
* fix: strip tmpDir from output dir

* ci: test PRs

* fix: cmd

* fix: use relative path for replacement

* chore: remove todo
2024-10-03 14:12:30 +03:00
162cc8cec1 ci: remove develop branch 2024-09-18 00:27:51 +03:00
20 changed files with 2145 additions and 1134 deletions

View File

@@ -1,33 +0,0 @@
name: Test
on:
pull_request:
branches:
- master
- develop
permissions:
contents: write
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm build

View File

@@ -4,7 +4,6 @@ on:
push:
branches:
- master
- develop
permissions:
contents: write

View File

@@ -1,6 +1,9 @@
name: Release
on:
pull_request:
branches:
- master
push:
branches:
- master
@@ -11,6 +14,7 @@ permissions:
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -20,7 +24,9 @@ jobs:
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm test
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -32,6 +38,8 @@ jobs:
- run: pnpm build
release:
name: Release Please
if: github.event_name == 'push'
needs:
- build
- test
@@ -47,6 +55,7 @@ jobs:
target-branch: master
publish:
name: NPM Publish
needs: release
runs-on: ubuntu-latest
if: ${{ needs.release.outputs.release_created }}

View File

@@ -1,16 +1,37 @@
# Change Log
## [2.3.3](https://github.com/chenasraf/simple-scaffold/compare/v2.3.2...v2.3.3) (2025-06-18)
### Bug Fixes
* config CLI precedence over file ([4b0b4e7](https://github.com/chenasraf/simple-scaffold/commit/4b0b4e73803ff741120b18767ded88db324a8844))
## [2.3.2](https://github.com/chenasraf/simple-scaffold/compare/v2.3.1...v2.3.2) (2024-10-27)
### Bug Fixes
* template config from CLI ([41f4ca5](https://github.com/chenasraf/simple-scaffold/commit/41f4ca52f12d3477e1a9a15757dc816fb99b6743))
## [2.3.1](https://github.com/chenasraf/simple-scaffold/compare/v2.3.0...v2.3.1) (2024-10-03)
### Bug Fixes
* strip tmpDir from output dir ([#108](https://github.com/chenasraf/simple-scaffold/issues/108)) ([80c92bf](https://github.com/chenasraf/simple-scaffold/commit/80c92bfe84dc896412ef98bce222e1d26cdb4e91))
## [2.3.0](https://github.com/chenasraf/simple-scaffold/compare/v2.2.2...v2.3.0) (2024-09-17)
### Features
* remove chalk dependency ([ba96ca6](https://github.com/chenasraf/simple-scaffold/commit/ba96ca64d1e02beb16bd127fc889da3ef016b7d5))
* remove chalk dependency ([ab9322e](https://github.com/chenasraf/simple-scaffold/commit/ab9322e1ab9c0a07cdab7275f3398286dee67a64))
### Bug Fixes
* exclude globs ([a2788e7](https://github.com/chenasraf/simple-scaffold/commit/a2788e7c7d27f46d55cf4810e1a8193b5d403568))
* exclude globs ([89dc43c](https://github.com/chenasraf/simple-scaffold/commit/89dc43c73d9a8640f45ae77e5c89e4f08f7f99ad))
## [2.2.2](https://github.com/chenasraf/simple-scaffold/compare/v2.2.1...v2.2.2) (2024-08-27)

18
eslint.config.mjs Normal file
View File

@@ -0,0 +1,18 @@
import eslint from '@eslint/js'
import tseslint from 'typescript-eslint'
export default [
...tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended),
{
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
{
ignores: ['node_modules/', 'build/', 'dist/', 'gen/'],
},
]

View File

@@ -1,6 +1,6 @@
{
"name": "simple-scaffold",
"version": "2.3.0",
"version": "2.3.3",
"description": "Generate any file structure - from single components to entire app boilerplates, with a single command.",
"homepage": "https://chenasraf.github.io/simple-scaffold",
"repository": {
@@ -39,20 +39,22 @@
"ci": "pnpm install --frozen-lockfile"
},
"dependencies": {
"date-fns": "^4.0.0",
"glob": "^11.0.0",
"date-fns": "^4.1.0",
"glob": "^11.0.3",
"handlebars": "^4.7.8",
"massarg": "2.0.1"
},
"devDependencies": {
"@types/jest": "^29.5.13",
"@eslint/js": "^9.29.0",
"@types/jest": "^30.0.0",
"@types/mock-fs": "^4.13.4",
"@types/node": "^22.5.5",
"jest": "^29.7.0",
"mock-fs": "^5.2.0",
"@types/node": "^24.0.3",
"jest": "^30.0.0",
"mock-fs": "^5.5.0",
"rimraf": "^6.0.1",
"ts-jest": "^29.2.5",
"ts-jest": "^29.4.0",
"ts-node": "^10.9.2",
"typescript": "^5.6.2"
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.1"
}
}

2843
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,17 +30,17 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
return
}
log(config, LogLevel.info, `Simple Scaffold v${pkg.version}`)
const tmpPath = generateUniqueTmpPath()
config.tmpDir = generateUniqueTmpPath()
try {
log(config, LogLevel.debug, "Parsing config file...", config)
const parsed = await parseConfigFile(config, tmpPath)
const parsed = await parseConfigFile(config)
await Scaffold(parsed)
} catch (e) {
const message = "message" in (e as any) ? (e as any).message : e?.toString()
const message = "message" in (e as object) ? (e as Error).message : e?.toString()
log(config, LogLevel.error, message)
} finally {
log(config, LogLevel.debug, "Cleaning up temporary files...", tmpPath)
await fs.rm(tmpPath, { recursive: true, force: true })
log(config, LogLevel.debug, "Cleaning up temporary files...", config.tmpDir)
await fs.rm(config.tmpDir, { recursive: true, force: true })
}
})
.option({
@@ -171,7 +171,6 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
aliases: ["ls"],
description: "List all available templates for a given config. See `list -h` for more information.",
run: async (_config) => {
const tmpPath = generateUniqueTmpPath()
const config = {
templates: [],
name: "",
@@ -180,19 +179,20 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
subdir: false,
overwrite: false,
dryRun: false,
tmpDir: generateUniqueTmpPath(),
..._config,
config: _config.config ?? (!_config.git ? process.cwd() : undefined),
}
try {
const file = await getConfigFile(config, tmpPath)
const file = await getConfigFile(config)
console.log(colorize.underline`Available templates:\n`)
console.log(Object.keys(file).join("\n"))
} catch (e) {
const message = "message" in (e as any) ? (e as any).message : e?.toString()
const message = "message" in (e as object) ? (e as Error).message : e?.toString()
log(config, LogLevel.error, message)
} finally {
log(config, LogLevel.debug, "Cleaning up temporary files...", tmpPath)
await fs.rm(tmpPath, { recursive: true, force: true })
log(config, LogLevel.debug, "Cleaning up temporary files...", config.tmpDir)
await fs.rm(config.tmpDir, { recursive: true, force: true })
}
},
})

View File

@@ -17,7 +17,7 @@ import { log } from "./logger"
import { resolve, wrapNoopResolver } from "./utils"
import { getGitConfig } from "./git"
import { createDirIfNotExists, getUniqueTmpPath, isDir, pathExists } from "./file"
import { exec, spawn } from "node:child_process"
import { exec } from "node:child_process"
/** @internal */
export function getOptionValueForFile<T>(
@@ -31,15 +31,15 @@ export function getOptionValueForFile<T>(
}
return (fn as FileResponseHandler<T>)(
filePath,
path.dirname(handlebarsParse(config, filePath, { isPath: true }).toString()),
path.basename(handlebarsParse(config, filePath, { isPath: true }).toString()),
path.dirname(handlebarsParse(config, filePath, { asPath: true }).toString()),
path.basename(handlebarsParse(config, filePath, { asPath: true }).toString()),
)
}
/** @internal */
export function parseAppendData(value: string, options: ScaffoldCmdConfig): unknown {
const data = options.data ?? {}
const [key, val] = value.split(/\:?=/)
const [key, val] = value.split(/:?=/)
// raw
if (value.includes(":=") && !val.includes(":=")) {
return { ...data, [key]: JSON.parse(val) }
@@ -52,7 +52,7 @@ function isWrappedWithQuotes(string: string): boolean {
}
/** @internal */
export async function getConfigFile(config: ScaffoldCmdConfig, tmpPath: string): Promise<ScaffoldConfigMap> {
export async function getConfigFile(config: ScaffoldCmdConfig): Promise<ScaffoldConfigMap> {
if (config.git && !config.git.includes("://")) {
log(config, LogLevel.info, `Loading config from GitHub ${config.git}`)
config.git = githubPartToUrl(config.git)
@@ -65,7 +65,7 @@ export async function getConfigFile(config: ScaffoldCmdConfig, tmpPath: string):
log(config, LogLevel.info, `Loading config from file ${configFilename}`)
const configPromise = await (isGit
? getRemoteConfig({ git: configPath, config: configFilename, logLevel: config.logLevel, tmpPath })
? getRemoteConfig({ git: configPath, config: configFilename, logLevel: config.logLevel, tmpDir: config.tmpDir! })
: getLocalConfig({ config: configFilename, logLevel: config.logLevel }))
// resolve the config
@@ -80,8 +80,20 @@ export async function getConfigFile(config: ScaffoldCmdConfig, tmpPath: string):
}
/** @internal */
export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string): Promise<ScaffoldConfig> {
let output: ScaffoldConfig = { ...config, beforeWrite: undefined }
export async function parseConfigFile(config: ScaffoldCmdConfig): Promise<ScaffoldConfig> {
let output: ScaffoldConfig = {
name: config.name,
templates: config.templates ?? [],
output: config.output,
logLevel: config.logLevel,
dryRun: config.dryRun,
data: config.data,
subdir: config.subdir,
overwrite: config.overwrite,
subdirHelper: config.subdirHelper,
beforeWrite: undefined,
tmpDir: config.tmpDir!,
}
if (config.quiet) {
config.logLevel = LogLevel.none
@@ -91,7 +103,7 @@ export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string
if (shouldLoadConfig) {
const key = config.key ?? "default"
const configImport = await getConfigFile(config, tmpPath)
const configImport = await getConfigFile(config)
if (!configImport[key]) {
throw new Error(`Template "${key}" not found in ${config.config}`)
@@ -100,11 +112,13 @@ export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string
const imported = configImport[key]
log(config, LogLevel.debug, "Imported result", imported)
output = {
...config,
...output,
...imported,
beforeWrite: undefined,
templates: config.templates || imported.templates,
output: config.output || imported.output,
data: {
...(imported as any).data,
...imported.data,
...config.data,
},
}
@@ -158,9 +172,9 @@ export async function getLocalConfig(config: ConfigLoadConfig & Partial<LogConfi
export async function getRemoteConfig(
config: RemoteConfigLoadConfig & Partial<LogConfig>,
): Promise<ScaffoldConfigFile> {
const { config: configFile, git, tmpPath, ...logConfig } = config as Required<typeof config>
const { config: configFile, git, tmpDir, ...logConfig } = config as Required<typeof config>
log(logConfig, LogLevel.info, `Loading config from remote ${git}, file ${configFile}`)
log(logConfig, LogLevel.info, `Loading config from remote ${git}, config file ${configFile || "<auto-detect>"}`)
const url = new URL(git!)
const isHttp = url.protocol === "http:" || url.protocol === "https:"
@@ -170,7 +184,7 @@ export async function getRemoteConfig(
throw new Error(`Unsupported protocol ${url.protocol}`)
}
return getGitConfig(url, configFile, tmpPath, logConfig)
return getGitConfig(url, configFile, tmpDir, logConfig)
}
/** @internal */
@@ -194,13 +208,13 @@ function wrapBeforeWrite(
beforeWrite: string,
): ScaffoldConfig["beforeWrite"] {
return async (content, rawContent, outputFile) => {
const tmpPath = path.join(getUniqueTmpPath(), path.basename(outputFile))
await createDirIfNotExists(path.dirname(tmpPath), config)
const tmpDir = path.join(getUniqueTmpPath(), path.basename(outputFile))
await createDirIfNotExists(path.dirname(tmpDir), config)
const ext = path.extname(outputFile)
const rawTmpPath = tmpPath.replace(ext, ".raw" + ext)
const rawTmpPath = tmpDir.replace(ext, ".raw" + ext)
try {
log(config, LogLevel.debug, "Parsing beforeWrite command", beforeWrite)
let cmd = await prepareBeforeWriteCmd({ beforeWrite, tmpPath, content, rawTmpPath, rawContent })
const cmd = await prepareBeforeWriteCmd({ beforeWrite, tmpDir, content, rawTmpPath, rawContent })
const result = await new Promise<string | undefined>((resolve, reject) => {
log(config, LogLevel.debug, "Running parsed beforeWrite command:", cmd)
const proc = exec(cmd)
@@ -221,7 +235,7 @@ function wrapBeforeWrite(
log(config, LogLevel.warning, "Error running beforeWrite command, returning original content")
return undefined
} finally {
await fs.rm(tmpPath, { force: true })
await fs.rm(tmpDir, { force: true })
await fs.rm(rawTmpPath, { force: true })
}
}
@@ -229,13 +243,13 @@ function wrapBeforeWrite(
async function prepareBeforeWriteCmd({
beforeWrite,
tmpPath,
tmpDir,
content,
rawTmpPath,
rawContent,
}: {
beforeWrite: string
tmpPath: string
tmpDir: string
content: Buffer
rawTmpPath: string
rawContent: Buffer
@@ -244,16 +258,16 @@ async function prepareBeforeWriteCmd({
const pathReg = /\{\{\s*path\s*\}\}/gi
const rawPathReg = /\{\{\s*rawpath\s*\}\}/gi
if (pathReg.test(beforeWrite)) {
await fs.writeFile(tmpPath, content)
cmd = beforeWrite.replaceAll(pathReg, tmpPath)
await fs.writeFile(tmpDir, content)
cmd = beforeWrite.replaceAll(pathReg, tmpDir)
}
if (rawPathReg.test(beforeWrite)) {
await fs.writeFile(rawTmpPath, rawContent)
cmd = beforeWrite.replaceAll(rawPathReg, rawTmpPath)
}
if (!cmd) {
await fs.writeFile(tmpPath, content)
cmd = [beforeWrite, tmpPath].join(" ")
await fs.writeFile(tmpDir, content)
cmd = [beforeWrite, tmpDir].join(" ")
}
return cmd
}

View File

@@ -30,8 +30,8 @@ export async function createDirIfNotExists(
log(config, LogLevel.debug, `Creating dir ${dir}`)
await mkdir(dir)
return
} catch (e: any) {
if (e.code !== "EEXIST") {
} catch (e: unknown) {
if (e && (e as NodeJS.ErrnoException).code !== "EEXIST") {
throw e
}
return
@@ -43,8 +43,8 @@ export async function pathExists(filePath: string): Promise<boolean> {
try {
await access(filePath, F_OK)
return true
} catch (e: any) {
if (e.code === "ENOENT") {
} catch (e: unknown) {
if (e && (e as NodeJS.ErrnoException).code === "ENOENT") {
return false
}
throw e
@@ -119,10 +119,9 @@ export async function getTemplateFileInfo(
): Promise<OutputFileInfo> {
const inputPath = path.resolve(process.cwd(), templatePath)
const outputPathOpt = getOptionValueForFile(config, inputPath, config.output)
const outputDir = getOutputDir(config, outputPathOpt, basePath)
const outputPath = handlebarsParse(config, path.join(outputDir, path.basename(inputPath)), {
isPath: true,
}).toString()
const outputDir = getOutputDir(config, outputPathOpt, basePath.replace(config.tmpDir!, "./"))
const rawOutputPath = path.join(outputDir, path.basename(inputPath))
const outputPath = handlebarsParse(config, rawOutputPath, { asPath: true }).toString()
const exists = await pathExists(outputPath)
return { inputPath, outputPathOpt, outputDir, outputPath, exists }
}
@@ -182,36 +181,33 @@ export async function handleTemplateFile(
config: ScaffoldConfig,
{ templatePath, basePath }: { templatePath: string; basePath: string },
): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(config, {
templatePath,
basePath,
})
const overwrite = getOptionValueForFile(config, inputPath, config.overwrite ?? false)
try {
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(config, {
templatePath,
basePath,
})
const overwrite = getOptionValueForFile(config, inputPath, config.overwrite ?? false)
log(
config,
LogLevel.debug,
`\nParsing ${templatePath}`,
`\nBase path: ${basePath}`,
`\nFull input path: ${inputPath}`,
`\nOutput Path Opt: ${outputPathOpt}`,
`\nFull output dir: ${outputDir}`,
`\nFull output path: ${outputPath}`,
`\n`,
)
log(
config,
LogLevel.debug,
`\nParsing ${templatePath}`,
`\nBase path: ${basePath}`,
`\nFull input path: ${inputPath}`,
`\nOutput Path Opt: ${outputPathOpt}`,
`\nFull output dir: ${outputDir}`,
`\nFull output path: ${outputPath}`,
`\n`,
)
await createDirIfNotExists(path.dirname(outputPath), config)
await createDirIfNotExists(path.dirname(outputPath), config)
log(config, LogLevel.info, `Writing to ${outputPath}`)
await copyFileTransformed(config, { exists, overwrite, outputPath, inputPath })
resolve()
} catch (e: any) {
handleErr(e)
reject(e)
}
})
log(config, LogLevel.info, `Writing to ${outputPath}`)
await copyFileTransformed(config, { exists, overwrite, outputPath, inputPath })
} catch (e: unknown) {
handleErr(e as NodeJS.ErrnoException)
throw e
}
}
/** @internal */

View File

@@ -2,7 +2,7 @@ import util from "util"
import { LogConfig, LogLevel, ScaffoldConfig } from "./types"
import { colorize, TermColor } from "./utils"
export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
export function log(config: LogConfig, level: LogLevel, ...obj: unknown[]): void {
const priority: Record<LogLevel, number> = {
[LogLevel.none]: 0,
[LogLevel.debug]: 1,
@@ -25,7 +25,7 @@ export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
const colorFn = colorize[levelColor[level]]
const key: "log" | "warn" | "error" = level === LogLevel.error ? "error" : level === LogLevel.warning ? "warn" : "log"
const logFn: any = console[key]
const logFn: (..._args: unknown[]) => void = console[key]
logFn(
...obj.map((i) =>
i instanceof Error

View File

@@ -105,17 +105,17 @@ export function registerHelpers(config: ScaffoldConfig): void {
export function handlebarsParse(
config: ScaffoldConfig,
templateBuffer: Buffer | string,
{ isPath = false }: { isPath?: boolean } = {},
{ asPath = false }: { asPath?: boolean } = {},
): Buffer {
const { data } = config
try {
let str = templateBuffer.toString()
if (isPath) {
if (asPath) {
str = str.replace(/\\/g, "/")
}
const parser = Handlebars.compile(str, { noEscape: true })
let outputContents = parser(data)
if (isPath && path.sep !== "/") {
if (asPath && path.sep !== "/") {
outputContents = outputContents.replace(/\//g, "\\")
}
return Buffer.from(outputContents)

View File

@@ -65,15 +65,15 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
const excludes = config.templates.filter((t) => t.startsWith("!"))
const includes = config.templates.filter((t) => !t.startsWith("!"))
const templates: GlobInfo[] = []
for (let _template of includes) {
for (const includedTemplate of includes) {
try {
const { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template } = await getTemplateGlobInfo(
config,
_template,
includedTemplate,
)
templates.push({ nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template })
} catch (e: any) {
handleErr(e)
} catch (e: unknown) {
handleErr(e as NodeJS.ErrnoException)
}
}
for (const tpl of templates) {
@@ -101,7 +101,7 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
})
}
}
} catch (e: any) {
} catch (e: unknown) {
log(config, LogLevel.error, e)
throw e
}
@@ -118,7 +118,7 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
* @category Main
* @return {Promise<void>} A promise that resolves when the scaffold is complete
*/
Scaffold.fromConfig = async function(
Scaffold.fromConfig = async function (
/** The path or URL to the config file */
pathOrUrl: string,
/** Information needed before loading the config */
@@ -126,6 +126,7 @@ Scaffold.fromConfig = async function(
/** Any overrides to the loaded config */
overrides?: Resolver<ScaffoldCmdConfig, Partial<Omit<ScaffoldConfig, "name">>>,
): Promise<void> {
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
const _cmdConfig: ScaffoldCmdConfig = {
dryRun: false,
output: process.cwd(),
@@ -136,11 +137,11 @@ Scaffold.fromConfig = async function(
quiet: false,
config: pathOrUrl,
version: false,
tmpDir: tmpPath,
...config,
}
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
const _overrides = resolve(overrides, _cmdConfig)
const _config = await parseConfigFile(_cmdConfig, tmpPath)
const _config = await parseConfigFile(_cmdConfig)
return Scaffold({ ..._config, ..._overrides })
}

View File

@@ -56,7 +56,7 @@ export interface ScaffoldConfig {
*
* This can be any object that will be usable by Handlebars.
*/
data?: Record<string, any>
data?: Record<string, unknown>
/**
* Enable to override output files, even if they already exist.
@@ -165,6 +165,9 @@ export interface ScaffoldConfig {
rawContent: Buffer,
outputPath: string,
): string | Buffer | undefined | Promise<string | Buffer | undefined>
/** @internal */
tmpDir?: string
}
/**
@@ -377,6 +380,8 @@ export type ScaffoldCmdConfig = {
version: boolean
/** Run a script before writing the files. This can be a command or a path to a file. The file contents will be passed to the given command. */
beforeWrite?: string
/** @internal */
tmpDir?: string
}
/**
@@ -406,7 +411,7 @@ export type ScaffoldConfigMap = Record<string, ScaffoldConfig>
export type ScaffoldConfigFile = AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>
/** @internal */
export type Resolver<T, R = T> = R | ((value: T) => R)
export type Resolver<T, R = T> = R | ((_value: T) => R)
/** @internal */
export type AsyncResolver<T, R = T> = Resolver<T, Promise<R> | R>
@@ -418,7 +423,7 @@ export type LogConfig = Pick<ScaffoldConfig, "logLevel">
export type ConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config">
/** @internal */
export type RemoteConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config" | "git"> & { tmpPath: string }
export type RemoteConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config" | "git" | "tmpDir">
/** @internal */
export type MinimalConfig = Pick<ScaffoldCmdConfig, "name" | "key">

View File

@@ -4,9 +4,9 @@ import { Console } from "console"
import { LogLevel, ScaffoldCmdConfig } from "../src/types"
import * as config from "../src/config"
import { resolve } from "../src/utils"
// @ts-ignore
import * as configFile from "../scaffold.config"
import configFile from "./test-config"
import { findConfigFile } from "../src/config"
import path from "path"
jest.mock("../src/git", () => {
return {
@@ -68,40 +68,53 @@ describe("config", () => {
describe("parseConfigFile", () => {
test("normal config does not change", async () => {
const tmpDir = `/tmp/scaffold-config-${Date.now()}`
const { quiet: _, tmpDir: _tmpDir, version: __, ...conf } = blankCliConf
expect(
await parseConfigFile(
{
...blankCliConf,
name: "-",
},
`/tmp/scaffold-config-${Date.now()}`,
),
).toEqual({ ...blankCliConf, name: "-" })
await parseConfigFile({
...blankCliConf,
name: "-",
tmpDir,
}),
).toEqual({ ...conf, name: "-", tmpDir, subdirHelper: undefined, beforeWrite: undefined })
})
describe("appendData", () => {
test("appends", async () => {
const result = await parseConfigFile(
{
...blankCliConf,
name: "-",
appendData: { key: "value" },
},
`/tmp/scaffold-config-${Date.now()}`,
)
const result = await parseConfigFile({
...blankCliConf,
name: "-",
appendData: { key: "value" },
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result?.data?.key).toEqual("value")
})
test("overwrites existing value", async () => {
const result = await parseConfigFile(
{
...blankCliConf,
name: "-",
data: { num: "123" },
appendData: { num: "1234" },
},
`/tmp/scaffold-config-${Date.now()}`,
)
const result = await parseConfigFile({
...blankCliConf,
name: "-",
data: { num: "123" },
appendData: { num: "1234" },
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result?.data?.num).toEqual("1234")
})
test("CLI output overrides config file output", async () => {
const tmpDir = `/tmp/scaffold-config-${Date.now()}`
const result = await parseConfigFile({
...blankCliConf,
config: path.resolve(__dirname, "test-config.js"),
key: "component",
output: "examples/test-output/override",
name: "Component",
tmpDir,
})
expect(result.output).toEqual("examples/test-output/override")
})
})
})
@@ -110,7 +123,7 @@ describe("config", () => {
const resultFn = await config.getRemoteConfig({
git: "https://github.com/chenasraf/simple-scaffold.git",
logLevel: LogLevel.none,
tmpPath: `/tmp/scaffold-config-${Date.now()}`,
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
const result = await resolve(resultFn, blankCliConf)
expect(result).toEqual(blankCliConf)
@@ -118,10 +131,10 @@ describe("config", () => {
test("gets local file config", async () => {
const resultFn = await config.getLocalConfig({
config: "scaffold.config.js",
config: path.join(__dirname, "test-config.js"),
logLevel: LogLevel.none,
})
const result = await resolve(resultFn, {} as any)
const result = (await resolve(resultFn, {} as ScaffoldCmdConfig)).default
expect(result).toEqual(configFile)
})
})
@@ -161,6 +174,7 @@ describe("config", () => {
for (const struct of [struct1, struct2, struct3, struct4]) {
const [k] = Object.keys(struct)
describe(`finds config file ${k}`, () => {
withMock(struct, async () => {
const result = await findConfigFile(process.cwd())

View File

@@ -13,42 +13,50 @@ const blankConf: ScaffoldConfig = {
describe("parser", () => {
describe("handlebarsParse", () => {
let origSep: any
let origSep: string
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 }).toString()).toEqual(
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { asPath: true }).toString()).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(
expect(handlebarsParse(blankConf, "/home/test/{{name}}.txt", { asPath: true })).toEqual(
Buffer.from("/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,
asPath: false,
},
),
).toEqual(Buffer.from("/home/test/test {{escaped}}.txt"))
@@ -65,6 +73,7 @@ describe("parser", () => {
expect(defaultHelpers.camelCase("TestString")).toEqual("testString")
expect(defaultHelpers.camelCase("Test____String")).toEqual("testString")
})
test("pascalCase", () => {
expect(defaultHelpers.pascalCase("test string")).toEqual("TestString")
expect(defaultHelpers.pascalCase("test_string")).toEqual("TestString")
@@ -73,6 +82,7 @@ describe("parser", () => {
expect(defaultHelpers.pascalCase("TestString")).toEqual("TestString")
expect(defaultHelpers.pascalCase("Test____String")).toEqual("TestString")
})
test("snakeCase", () => {
expect(defaultHelpers.snakeCase("test string")).toEqual("test_string")
expect(defaultHelpers.snakeCase("test_string")).toEqual("test_string")
@@ -81,6 +91,7 @@ describe("parser", () => {
expect(defaultHelpers.snakeCase("TestString")).toEqual("test_string")
expect(defaultHelpers.snakeCase("Test____String")).toEqual("test_string")
})
test("kebabCase", () => {
expect(defaultHelpers.kebabCase("test string")).toEqual("test-string")
expect(defaultHelpers.kebabCase("test_string")).toEqual("test-string")
@@ -89,6 +100,7 @@ describe("parser", () => {
expect(defaultHelpers.kebabCase("TestString")).toEqual("test-string")
expect(defaultHelpers.kebabCase("Test____String")).toEqual("test-string")
})
test("startCase", () => {
expect(defaultHelpers.startCase("test string")).toEqual("Test String")
expect(defaultHelpers.startCase("test_string")).toEqual("Test String")
@@ -98,6 +110,7 @@ describe("parser", () => {
expect(defaultHelpers.startCase("Test____String")).toEqual("Test String")
})
})
describe("date helpers", () => {
describe("now", () => {
test("should work without extra params", () => {

View File

@@ -99,8 +99,10 @@ function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunct
}
describe("Scaffold", () => {
describe(
"create subdir",
withMock(fileStructNormal, () => {
test("should not create by default", async () => {
await Scaffold({
@@ -130,6 +132,7 @@ describe("Scaffold", () => {
describe(
"binary files",
withMock(fileStructWithBinary, () => {
test("should copy as-is", async () => {
await Scaffold({
@@ -322,7 +325,7 @@ describe("Scaffold", () => {
describe(
"capitalization helpers",
withMock(fileStructHelpers, () => {
const _helpers: Record<string, (text: string) => string> = {
const _helpers: Record<string, (_text: string) => string> = {
add1: (text) => text + " 1",
}
@@ -394,7 +397,7 @@ describe("Scaffold", () => {
describe(
"custom helpers",
withMock(fileStructHelpers, () => {
const _helpers: Record<string, (text: string) => string> = {
const _helpers: Record<string, (_text: string) => string> = {
add1: (text) => text + " 1",
}
test("should work", async () => {

3
tests/test-config.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare const config: import("../dist").ScaffoldConfigFile;
export = config;

24
tests/test-config.js Normal file
View File

@@ -0,0 +1,24 @@
// @ts-check
/** @type {import('../dist').ScaffoldConfigFile} */
// eslint-disable-next-line no-undef
module.exports = (conf) => {
// eslint-disable-next-line no-undef
console.log("Config:", conf)
return {
default: {
templates: ["examples/test-input/Component"],
output: "examples/test-output",
data: { property: "myProp", value: "10" },
},
component: {
templates: ["examples/test-input/Component"],
output: "examples/test-output/component",
data: { property: "myProp", value: "10" },
},
configs: {
templates: ["examples/test-input/**/.*"],
output: "examples/test-output/configs",
name: "---",
},
}
}

View File

@@ -5,6 +5,7 @@ describe("utils", () => {
expect(resolve(() => 1, null)).toBe(1)
expect(resolve((x) => x, 2)).toBe(2)
})
test("should resolve value", () => {
expect(resolve(1, null)).toBe(1)
expect(resolve(2, 1)).toBe(2)
@@ -20,22 +21,22 @@ describe("utils", () => {
})
describe("colorize", () => {
it("should colorize text with red color", () => {
test("should colorize text with red color", () => {
const result = colorize("Hello", "red")
expect(result).toBe("\x1b[31mHello\x1b[0m")
})
it("should colorize text with bold", () => {
test("should colorize text with bold", () => {
const result = colorize("Hello", "bold")
expect(result).toBe("\x1b[1mHello\x1b[23m")
})
it("should reset color", () => {
test("should reset color", () => {
const result = colorize("Hello", "reset")
expect(result).toBe("\x1b[0mHello\x1b[0m")
})
it("should have all color functions", () => {
test("should have all color functions", () => {
const colors: TermColor[] = [
"reset",
"dim",
@@ -56,12 +57,12 @@ describe("colorize", () => {
})
})
it("should colorize text using colorize.red", () => {
test("should colorize text using colorize.red", () => {
const result = colorize.red("Hello")
expect(result).toBe("\x1b[31mHello\x1b[0m")
})
it("should colorize text using template strings with colorize.blue", () => {
test("should colorize text using template strings with colorize.blue", () => {
const result = colorize.blue`Hello ${"World"}`
expect(result).toBe("\x1b[34mHello World\x1b[0m")
})