Compare commits

...

19 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
Chen Asraf
db6177c200 chore(develop): release 2.3.0 2024-09-18 00:12:00 +03:00
ae64db846f chore: add coverage script 2024-09-18 00:12:00 +03:00
89dc43c73d fix: exclude globs
refactor: split main file iteration to 2 steps
2024-09-17 23:57:48 +03:00
2c43dc4daf build: update target 2024-09-17 23:57:48 +03:00
f4c907e6c9 ci: fix build 2024-09-17 23:57:48 +03:00
a275e688d4 ci: use release-please 2024-09-17 23:57:48 +03:00
ff4ebf0a5b test: add color tests 2024-09-17 23:57:48 +03:00
ab9322e1ab feat: remove chalk dependency 2024-09-17 23:57:48 +03:00
24 changed files with 2277 additions and 3487 deletions

View File

@@ -1,11 +1,12 @@
name: Documentation
permissions:
contents: write
on:
push:
branches: [master, pre, develop]
branches:
- master
permissions:
contents: write
jobs:
docs:
@@ -13,22 +14,15 @@ jobs:
runs-on: ubuntu-latest
# if: "contains(github.event.head_commit.message, 'chore(release)')"
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "20.x"
- name: Install PNPM
run: npm i -g pnpm
- name: Install dependencies
run: |
node-version: 20
- run: npm i -g pnpm
- run: |
pnpm install --frozen-lockfile
cd docs && pnpm install --frozen-lockfile
- name: Build Docs
run: pnpm docs:build
- run: pnpm docs:build
- name: Deploy on GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:

View File

@@ -1,26 +0,0 @@
name: Pull Requests
on:
pull_request:
branches: [master, pre, develop]
jobs:
build:
name: Test & Build PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "20.x"
- name: Install PNPM
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Tests
run: pnpm test
- name: Build Package
run: pnpm build

View File

@@ -1,38 +1,73 @@
name: Release
on:
pull_request:
branches:
- master
push:
branches: [master, pre, develop]
branches:
- master
permissions:
contents: read # for checkout
contents: write
pull-requests: write
jobs:
release:
name: Release
test:
name: Test
runs-on: ubuntu-latest
permissions:
contents: write # to be able to publish a GitHub release
issues: write # to be able to comment on released issues
pull-requests: write # to be able to comment on released pull requests
id-token: write # to enable use of OIDC for npm provenance
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
node-version: 20
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm test
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Install PNPM
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Tests
run: pnpm test
- name: Build Package
run: pnpm build
- name: Semantic Release
run: npx semantic-release
node-version: 20
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm build
release:
name: Release Please
if: github.event_name == 'push'
needs:
- build
- test
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
steps:
- uses: googleapis/release-please-action@v4
id: release
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 }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm build
- run: cd dist && npm publish
env:
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -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
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

@@ -88,7 +88,6 @@ export default {
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// moduleNameMapper: {
// chalk: "<rootDir>/node_modules/chalk/source/index.js",
// "#ansi-styles": "<rootDir>/node_modules/chalk/source/vendor/ansi-styles/index.js",
// "#supports-color": "<rootDir>/node_modules/chalk/source/vendor/supports-color/index.js",
// },

View File

@@ -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": {
@@ -13,7 +13,7 @@
"bin": {
"simple-scaffold": "cmd.js"
},
"packageManager": "pnpm@9.0.4",
"packageManager": "pnpm@9.9.0",
"keywords": [
"javascript",
"cli",
@@ -26,41 +26,35 @@
"scaffolding"
],
"scripts": {
"clean": "rm -rf dist/",
"clean": "rimraf dist",
"build": "pnpm clean && tsc && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
"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",
"audit-fix": "pnpm audit --fix",
"changelog": "conventional-changelog -i CHANGELOG.md -s -r 0; echo \"# Change Log\n\n$(cat CHANGELOG.md)\" > CHANGELOG.md"
"ci": "pnpm install --frozen-lockfile"
},
"dependencies": {
"chalk": "^4.1.2",
"date-fns": "^3.6.0",
"glob": "^11.0.0",
"date-fns": "^4.1.0",
"glob": "^11.0.3",
"handlebars": "^4.7.8",
"massarg": "2.0.1"
},
"devDependencies": {
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/release-notes-generator": "^14.0.1",
"@types/jest": "^29.5.12",
"@eslint/js": "^9.29.0",
"@types/jest": "^30.0.0",
"@types/mock-fs": "^4.13.4",
"@types/node": "^22.5.0",
"@types/semantic-release": "^20.0.6",
"conventional-changelog": "^6.0.0",
"conventional-changelog-cli": "^5.0.0",
"jest": "^29.7.0",
"mock-fs": "^5.2.0",
"semantic-release": "^24.1.0",
"semantic-release-conventional-commits": "^3.0.0",
"ts-jest": "^29.2.5",
"@types/node": "^24.0.3",
"jest": "^30.0.0",
"mock-fs": "^5.5.0",
"rimraf": "^6.0.1",
"ts-jest": "^29.4.0",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.1"
}
}

4891
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,67 +0,0 @@
const ref = process.env.GITHUB_REF || ""
const branch = ref.split("/").pop()
/**
* @type {import('semantic-release').GlobalConfig}
*/
module.exports = {
branches: ["master", { name: "pre", prerelease: true }],
plugins: [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/npm",
{
// only update the pkg version on root, don't publish
// this is to keep package.json version in sync with the release
npmPublish: false,
},
],
[
"@semantic-release/exec",
{
// pack the dist folder, during publish step (after version was bumped)
publishCmd: 'echo "Packing..."; cd ./dist && pnpm pack --pack-destination=../; echo "Done"',
},
],
[
"@semantic-release/npm",
{
// publish from dist dir instead of root
// this is the actual uild output
pkgRoot: "dist",
},
],
[
// Release to GitHub
"@semantic-release/github",
{
assets: ["*.tgz"],
},
],
branch === "master"
? [
// Update CHANGELOG.md only on master
"@semantic-release/changelog",
{
changelogFile: "CHANGELOG.md",
changelogTitle: "# Change Log",
},
]
: undefined,
[
// Commit the package.json and CHANGELOG.md files to git (if modified)
"@semantic-release/git",
{
assets: ["package.json", "CHANGELOG.md"].filter(Boolean),
},
],
//
// [
// '@semantic-release/exec',
// {
// verifyReleaseCmd: 'echo ${nextRelease.version} > .VERSION',
// },
// ],
].filter(Boolean),
}

View File

@@ -3,13 +3,13 @@
import path from "node:path"
import fs from "node:fs/promises"
import { massarg } from "massarg"
import chalk from "chalk"
import { ListCommandCliOptions, LogLevel, ScaffoldCmdConfig } from "./types"
import { Scaffold } from "./scaffold"
import { getConfigFile, parseAppendData, parseConfigFile } from "./config"
import { log } from "./logger"
import { MassargCommand } from "massarg/command"
import { getUniqueTmpPath as generateUniqueTmpPath } from "./file"
import { colorize } from "./utils"
export async function parseCliArgs(args = process.argv.slice(2)) {
const isProjectRoot = Boolean(await fs.stat(path.join(__dirname, "package.json")).catch(() => false))
@@ -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({
@@ -134,7 +134,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
defaultValue: LogLevel.info,
description:
"Determine amount of logs to display. The values are: " +
`${chalk.bold`\`none | debug | info | warn | error\``}. ` +
`${colorize.bold`\`none | debug | info | warn | error\``}. ` +
"The provided level will display messages of the same level or higher.",
parse: (v) => {
const val = v.toLowerCase()
@@ -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)
console.log(chalk.underline`Available templates:\n`)
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 })
}
},
})
@@ -212,7 +212,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
defaultValue: LogLevel.none,
description:
"Determine amount of logs to display. The values are: " +
`${chalk.bold`\`none | debug | info | warn | error\``}. ` +
`${colorize.bold`\`none | debug | info | warn | error\``}. ` +
"The provided level will display messages of the same level or higher.",
parse: (v) => {
const val = v.toLowerCase()
@@ -251,7 +251,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
bindOption: true,
lineLength: 100,
useGlobalTableColumns: true,
usageText: [chalk.yellow`simple-scaffold`, chalk.gray`[options]`, chalk.cyan`<name>`].join(" "),
usageText: [colorize.yellow`simple-scaffold`, colorize.gray`[options]`, colorize.cyan`<name>`].join(" "),
optionOptions: {
displayNegations: true,
},
@@ -259,9 +259,9 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
`Version: ${pkg.version}`,
`Copyright © Chen Asraf 2017-${new Date().getFullYear()}`,
``,
`Documentation: ${chalk.underline`https://chenasraf.github.io/simple-scaffold`}`,
`NPM: ${chalk.underline`https://npmjs.com/package/simple-scaffold`}`,
`GitHub: ${chalk.underline`https://github.com/chenasraf/simple-scaffold`}`,
`Documentation: ${colorize.underline`https://chenasraf.github.io/simple-scaffold`}`,
`NPM: ${colorize.underline`https://npmjs.com/package/simple-scaffold`}`,
`GitHub: ${colorize.underline`https://github.com/chenasraf/simple-scaffold`}`,
].join("\n"),
})
.parse(args)

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
@@ -71,11 +71,10 @@ export function getBasePath(relPath: string): string {
.replace(process.cwd(), "")
}
export async function getFileList(_config: ScaffoldConfig, template: string): Promise<string[]> {
template = template.replaceAll(/[\\]+/g, "/")
log(_config, LogLevel.debug, `Getting file list for ${template}`)
export async function getFileList(config: ScaffoldConfig, templates: string[]): Promise<string[]> {
log(config, LogLevel.debug, `Getting file list for glob list: ${templates}`)
return (
await glob(template, {
await glob(templates, {
dot: true,
nodir: true,
})
@@ -120,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 }
}
@@ -183,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

@@ -1,8 +1,8 @@
import util from "util"
import { LogConfig, LogLevel, ScaffoldConfig } from "./types"
import chalk from "chalk"
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,
@@ -15,7 +15,7 @@ export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
return
}
const levelColor: Record<keyof typeof LogLevel, keyof typeof chalk> = {
const levelColor: Record<keyof typeof LogLevel, TermColor> = {
[LogLevel.none]: "reset",
[LogLevel.debug]: "blue",
[LogLevel.info]: "dim",
@@ -23,16 +23,16 @@ export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
[LogLevel.error]: "red",
}
const chalkFn: any = chalk[levelColor[level]]
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
? chalkFn(i, JSON.stringify(i, undefined, 1), i.stack)
? colorFn(i, JSON.stringify(i, undefined, 1), i.stack)
: typeof i === "object"
? util.inspect(i, { depth: null, colors: true })
: chalkFn(i),
: colorFn(i),
),
)
}

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

@@ -16,6 +16,7 @@ import {
getFileList,
getBasePath,
handleTemplateFile,
GlobInfo,
} from "./file"
import { LogLevel, MinimalConfig, Resolver, ScaffoldCmdConfig, ScaffoldConfig } from "./types"
import { registerHelpers } from "./parser"
@@ -61,40 +62,46 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
try {
config.data = { name: config.name, ...config.data }
logInitStep(config)
for (let _template of config.templates) {
const excludes = config.templates.filter((t) => t.startsWith("!"))
const includes = config.templates.filter((t) => !t.startsWith("!"))
const templates: GlobInfo[] = []
for (const includedTemplate of includes) {
try {
const { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template } = await getTemplateGlobInfo(
config,
_template,
includedTemplate,
)
const files = await getFileList(config, template)
log(config, LogLevel.debug, "Iterating files", { files, template })
for (const inputFilePath of files) {
if (await isDir(inputFilePath)) {
continue
}
const relPath = makeRelativePath(path.dirname(removeGlob(inputFilePath).replace(nonGlobTemplate, "")))
const basePath = getBasePath(relPath)
logInputFile(config, {
originalTemplate: origTemplate,
relativePath: relPath,
parsedTemplate: template,
inputFilePath,
nonGlobTemplate,
basePath,
isDirOrGlob,
isGlob,
})
await handleTemplateFile(config, {
templatePath: inputFilePath,
basePath,
})
}
} catch (e: any) {
handleErr(e)
templates.push({ nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template })
} catch (e: unknown) {
handleErr(e as NodeJS.ErrnoException)
}
}
} catch (e: any) {
for (const tpl of templates) {
const files = await getFileList(config, [tpl.template, ...excludes])
for (const file of files) {
if (await isDir(file)) {
continue
}
log(config, LogLevel.debug, "Iterating files", { files, file })
const relPath = makeRelativePath(path.dirname(removeGlob(file).replace(tpl.nonGlobTemplate, "")))
const basePath = getBasePath(relPath)
logInputFile(config, {
originalTemplate: tpl.origTemplate,
relativePath: relPath,
parsedTemplate: tpl.template,
inputFilePath: file,
nonGlobTemplate: tpl.nonGlobTemplate,
basePath,
isDirOrGlob: tpl.isDirOrGlob,
isGlob: tpl.isGlob,
})
await handleTemplateFile(config, {
templatePath: file,
basePath,
})
}
}
} catch (e: unknown) {
log(config, LogLevel.error, e)
throw e
}
@@ -119,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(),
@@ -129,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

@@ -22,6 +22,11 @@ export interface ScaffoldConfig {
* Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path,
* or a glob pattern for multiple file matching easily.
*
* You may omit files from output by prepending a `!` to their glob pattern.
*
* For example, `["components/**", "!components/README.md"]` will include everything in the directory `components`
* except the `README.md` file inside.
*
* @default Current working directory
*/
templates: string[]
@@ -51,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.
@@ -160,6 +165,9 @@ export interface ScaffoldConfig {
rawContent: Buffer,
outputPath: string,
): string | Buffer | undefined | Promise<string | Buffer | undefined>
/** @internal */
tmpDir?: string
}
/**
@@ -331,7 +339,9 @@ export type FileResponse<T> = T | FileResponseHandler<T>
/**
* The Scaffold config for CLI
* Contains less and more specific options than {@link ScaffoldConfig}
* Contains less and more specific options than {@link ScaffoldConfig}.
*
* For more information about each option, see {@link ScaffoldConfig}.
*/
export type ScaffoldCmdConfig = {
/** The name of the scaffold template to use. */
@@ -370,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
}
/**
@@ -399,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>
@@ -411,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

@@ -15,3 +15,65 @@ export function wrapNoopResolver<T, R = T>(value: Resolver<T, R>): Resolver<T, R
return (_) => value
}
const colorMap = {
reset: 0,
dim: 2,
bold: 1,
italic: 3,
underline: 4,
red: 31,
green: 32,
yellow: 33,
blue: 34,
magenta: 35,
cyan: 36,
white: 37,
gray: 90,
} as const
export type TermColor = keyof typeof colorMap
function _colorize(text: string, color: TermColor): string {
const c = colorMap[color]!
let r = 0
if (c > 1 && c < 30) {
r = c + 20
} else if (c === 1) {
r = 23
} else {
r = 0
}
return `\x1b[${c}m${text}\x1b[${r}m`
}
function isTemplateStringArray(template: TemplateStringsArray | unknown): template is TemplateStringsArray {
return Array.isArray(template) && typeof template[0] === "string"
}
const createColorize =
(color: TermColor) =>
(template: TemplateStringsArray | unknown, ...params: unknown[]): string => {
return isTemplateStringArray(template)
? _colorize(
(template as TemplateStringsArray).reduce((acc, str, i) => acc + str + (params[i] ?? ""), ""),
color,
)
: _colorize(String(template), color)
}
type TemplateStringsFn = ReturnType<typeof createColorize> & ((text: string) => string)
type TemplateStringsFns = { [key in TermColor]: TemplateStringsFn }
export const colorize: typeof _colorize & TemplateStringsFns = Object.assign(
_colorize,
Object.entries(colorMap).reduce(
(acc, [key]) => {
acc[key as TermColor] = createColorize(key as TermColor)
return acc
},
{} as Record<TermColor, TemplateStringsFn>,
),
)

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

@@ -71,6 +71,14 @@ const fileStructDates = {
output: {},
}
const fileStructExcludes = {
input: {
"include.txt": "This file should be included",
"exclude.txt": "This file should be excluded",
},
output: {},
}
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunction): jest.EmptyFunction {
return () => {
beforeEach(() => {
@@ -91,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({
@@ -122,6 +132,7 @@ describe("Scaffold", () => {
describe(
"binary files",
withMock(fileStructWithBinary, () => {
test("should copy as-is", async () => {
await Scaffold({
@@ -268,8 +279,7 @@ describe("Scaffold", () => {
}),
)
describe(
"output structure",
describe("output structure", () => {
withMock(fileStructNested, () => {
test("should maintain input structure on output", async () => {
await Scaffold({
@@ -294,13 +304,28 @@ describe("Scaffold", () => {
expect(oneDeepFile.toString()).toEqual("Hello, my value is 1")
expect(twoDeepFile.toString()).toEqual("Hi! My value is actually NOT 1!")
})
}),
)
})
withMock(fileStructExcludes, () => {
test("should exclude files", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input", "!exclude.txt"],
data: { value: "1" },
logLevel: "none",
})
const includeFile = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(includeFile.toString()).toEqual("This file should be included")
expect(() => readFileSync(join(process.cwd(), "output", "exclude.txt"))).toThrow()
})
})
})
describe(
"capitalization helpers",
withMock(fileStructHelpers, () => {
const _helpers: Record<string, (text: string) => string> = {
const _helpers: Record<string, (_text: string) => string> = {
add1: (text) => text + " 1",
}
@@ -372,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

@@ -1,11 +1,11 @@
import { handleErr, resolve } from "../src/utils"
import { handleErr, resolve, colorize, TermColor } from "../src/utils"
describe("utils", () => {
describe("resolve", () => {
test("should resolve function", () => {
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)
@@ -19,3 +19,51 @@ describe("utils", () => {
})
})
})
describe("colorize", () => {
test("should colorize text with red color", () => {
const result = colorize("Hello", "red")
expect(result).toBe("\x1b[31mHello\x1b[0m")
})
test("should colorize text with bold", () => {
const result = colorize("Hello", "bold")
expect(result).toBe("\x1b[1mHello\x1b[23m")
})
test("should reset color", () => {
const result = colorize("Hello", "reset")
expect(result).toBe("\x1b[0mHello\x1b[0m")
})
test("should have all color functions", () => {
const colors: TermColor[] = [
"reset",
"dim",
"bold",
"italic",
"underline",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
"gray",
]
colors.forEach((color) => {
expect(typeof colorize[color]).toBe("function")
})
})
test("should colorize text using colorize.red", () => {
const result = colorize.red("Hello")
expect(result).toBe("\x1b[31mHello\x1b[0m")
})
test("should colorize text using template strings with colorize.blue", () => {
const result = colorize.blue`Hello ${"World"}`
expect(result).toBe("\x1b[34mHello World\x1b[0m")
})
})

View File

@@ -1,20 +1,29 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"target": "ESNext",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"lib": ["ES2022"],
"lib": [
"ESNext"
],
"declaration": true,
"outDir": "dist",
"strict": true,
"sourceMap": true,
"removeComments": false,
"paths": {
"@/*": ["./src/*"],
"@/*": [
"./src/*"
],
},
},
"include": ["src/index.ts", "src/cmd.ts"],
"exclude": ["tests/*"],
"include": [
"src/index.ts",
"src/cmd.ts"
],
"exclude": [
"tests/*"
],
}