mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-18 01:29:09 +00:00
Compare commits
13 Commits
release-pl
...
v2.3.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 429f12d1b8 | |||
| dcba30689b | |||
| 7745385573 | |||
| 29f2afe097 | |||
| 4b0b4e7380 | |||
|
|
c1536839e3 | ||
| 7e029fd122 | |||
| 41f4ca52f1 | |||
|
|
78d6bf186d | ||
|
|
80c92bfe84 | ||
| 162cc8cec1 | |||
|
|
db6177c200 | ||
| ae64db846f |
33
.github/workflows/develop.yml
vendored
33
.github/workflows/develop.yml
vendored
@@ -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
|
||||
1
.github/workflows/docs.yml
vendored
1
.github/workflows/docs.yml
vendored
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -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
|
||||
@@ -44,8 +52,10 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
||||
release-type: node
|
||||
target-branch: master
|
||||
|
||||
publish:
|
||||
name: NPM Publish
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.release.outputs.release_created }}
|
||||
@@ -58,6 +68,6 @@ jobs:
|
||||
- run: npm i -g pnpm
|
||||
- run: pnpm run ci
|
||||
- run: pnpm build
|
||||
- run: cd build && npm publish
|
||||
- run: cd dist && npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -1,5 +1,38 @@
|
||||
# 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 ([ab9322e](https://github.com/chenasraf/simple-scaffold/commit/ab9322e1ab9c0a07cdab7275f3398286dee67a64))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 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
18
eslint.config.mjs
Normal 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/'],
|
||||
},
|
||||
]
|
||||
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simple-scaffold",
|
||||
"version": "2.2.2",
|
||||
"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": {
|
||||
@@ -31,6 +31,7 @@
|
||||
"dev": "tsc --watch",
|
||||
"start": "ts-node src/scaffold.ts",
|
||||
"test": "jest",
|
||||
"coverage": "open coverage/lcov-report/index.html",
|
||||
"cmd": "ts-node src/cmd.ts",
|
||||
"docs:build": "cd docs && pnpm build",
|
||||
"docs:watch": "cd docs && pnpm start",
|
||||
@@ -38,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
2843
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
20
src/cmd.ts
20
src/cmd.ts
@@ -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 })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
66
src/file.ts
66
src/file.ts
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
11
src/types.ts
11
src/types.ts
@@ -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">
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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
3
tests/test-config.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare const config: import("../dist").ScaffoldConfigFile;
|
||||
export = config;
|
||||
|
||||
24
tests/test-config.js
Normal file
24
tests/test-config.js
Normal 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: "---",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user