mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-18 01:29:09 +00:00
Compare commits
2 Commits
v1.0.0-alp
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
385829aa27 | ||
|
|
33c357bccc |
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -2,6 +2,11 @@
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
"npm.packageManager": "yarn",
|
||||
"cSpell.words": [
|
||||
"massarg"
|
||||
"massarg",
|
||||
"nodir",
|
||||
"nobrace",
|
||||
"noext",
|
||||
"nocomment",
|
||||
"nonegate"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "simple-scaffold",
|
||||
"version": "1.0.0-alpha.8",
|
||||
"version": "1.0.0-alpha.10",
|
||||
"description": "Create files based on templates",
|
||||
"repository": "https://github.com/chenasraf/simple-scaffold.git",
|
||||
"author": "Chen Asraf <inbox@casraf.com>",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"bin": "cmd.js",
|
||||
"types": "types.d.ts",
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist/",
|
||||
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./dist/ && cp ./README.md ./dist/",
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
import { LogLevel, ScaffoldConfig } from "./types"
|
||||
|
||||
export async function Scaffold(config: ScaffoldConfig) {
|
||||
const options = { ...config }
|
||||
try {
|
||||
const options = { ...config }
|
||||
const data = { name: options.name, Name: pascalCase(options.name), ...options.data }
|
||||
log(options, LogLevel.Debug, "Full config:", {
|
||||
name: options.name,
|
||||
@@ -29,35 +29,64 @@ export async function Scaffold(config: ScaffoldConfig) {
|
||||
data: options.data,
|
||||
overwrite: options.overwrite,
|
||||
quiet: options.quiet,
|
||||
verbose: `${options.verbose} (${Object.keys(LogLevel).find(
|
||||
(k) => (LogLevel[k as any] as unknown as number) === options.verbose!
|
||||
)})`,
|
||||
})
|
||||
log(options, LogLevel.Info, "Data:", data)
|
||||
for (let template of config.templates) {
|
||||
try {
|
||||
const _isGlob = template.includes("*")
|
||||
if (!_isGlob && !(await pathExists(template))) {
|
||||
const err: NodeJS.ErrnoException = new Error(`ENOENT, no such file or directory ${template}`)
|
||||
err.code = "ENOENT"
|
||||
err.path = "non-existing-input"
|
||||
err.errno = -2
|
||||
throw err
|
||||
}
|
||||
const _nonGlobTemplate = _isGlob ? removeGlob(template) : template
|
||||
const _isDir = _isGlob ? false : await isDir(template)
|
||||
const _shouldAddGlob = !_isGlob && !_isDir
|
||||
log(options, LogLevel.Debug, "before isDir", "isGlob:", _isGlob, template)
|
||||
const _isDir = _isGlob ? true : await isDir(template)
|
||||
log(options, LogLevel.Debug, "after isDir", _isDir)
|
||||
const _shouldAddGlob = !_isGlob && _isDir
|
||||
const origTemplate = template
|
||||
if (_shouldAddGlob) {
|
||||
template = template + "/**/*"
|
||||
}
|
||||
const files = await promisify(glob)(template, { dot: true, debug: false })
|
||||
for (const templatePath of files) {
|
||||
if (!(await isDir(templatePath))) {
|
||||
log(options, LogLevel.Debug, "before glob")
|
||||
const files = await promisify(glob)(template, {
|
||||
dot: true,
|
||||
debug: false,
|
||||
nodir: options.verbose === LogLevel.Debug,
|
||||
nobrace: true,
|
||||
noext: true,
|
||||
nocomment: true,
|
||||
nonegate: true,
|
||||
})
|
||||
log(options, LogLevel.Debug, "after glob")
|
||||
for (const inputFilePath of files) {
|
||||
if (!(await isDir(inputFilePath))) {
|
||||
const basePath = path
|
||||
.resolve(
|
||||
process.cwd(),
|
||||
_isDir
|
||||
? templatePath.replace(template, "")
|
||||
: path.dirname(removeGlob(templatePath).replace(_nonGlobTemplate, ""))
|
||||
)
|
||||
.resolve(process.cwd(), path.dirname(removeGlob(inputFilePath).replace(_nonGlobTemplate, "")).slice(1))
|
||||
.replace(process.cwd() + "/", "")
|
||||
.replace(process.cwd(), "")
|
||||
log(
|
||||
options,
|
||||
LogLevel.Debug,
|
||||
`\ntemplate: ${template}\ntemplatePath: ${templatePath}, \nbase path: ${basePath}\n`
|
||||
`\nprocess.cwd(): ${process.cwd()}`,
|
||||
`\norigTemplate: ${origTemplate}`,
|
||||
`\nremoveGlob(inputFilePath).replace(_nonGlobTemplate, "").slice(1): ${removeGlob(inputFilePath)
|
||||
.replace(_nonGlobTemplate, "")
|
||||
.slice(1)}`,
|
||||
`\ntemplate: ${template}`,
|
||||
`\ninputFilePath: ${inputFilePath}`,
|
||||
`\nnonGlobTemplate: ${_nonGlobTemplate}`,
|
||||
`\nbase path: ${basePath}`,
|
||||
`\nisDir: ${_isDir}`,
|
||||
`\nisGlob: ${_isGlob}`,
|
||||
`\n`
|
||||
)
|
||||
await handleTemplateFile(templatePath, basePath, options, data)
|
||||
await handleTemplateFile(inputFilePath, basePath, options, data)
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -65,7 +94,7 @@ export async function Scaffold(config: ScaffoldConfig) {
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
log(options, LogLevel.Error, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -84,30 +113,22 @@ async function handleTemplateFile(
|
||||
process.cwd(),
|
||||
...([outputPathOpt, basePath, options.createSubFolder ? options.name : undefined].filter(Boolean) as string[])
|
||||
)
|
||||
const outputPath = handlebarsParse(path.join(outputDir, path.basename(inputPath)), data)
|
||||
log(
|
||||
options,
|
||||
LogLevel.Debug,
|
||||
`\nParsing ${templatePath}`,
|
||||
`\nBase path: ${basePath}`,
|
||||
`\nFull input path: ${inputPath}`,
|
||||
`\nFull output path: ${outputDir}\n`
|
||||
`\nOutput Path Opt: ${outputPathOpt}`,
|
||||
`\nFull output dir: ${outputDir}`,
|
||||
`\nFull output path: ${outputPath}`,
|
||||
`\n`
|
||||
)
|
||||
const outputPath = path.join(outputDir, handlebarsParse(path.basename(inputPath), data))
|
||||
const overwrite = getOptionValueForFile(inputPath, data, options.overwrite ?? false)
|
||||
const exists = await pathExists(outputPath)
|
||||
|
||||
log(
|
||||
options,
|
||||
LogLevel.Debug,
|
||||
"Filename parsed:",
|
||||
handlebarsParse(path.basename(inputPath), data),
|
||||
"Orig:",
|
||||
path.basename(inputPath)
|
||||
// "Test:",
|
||||
// handlebarsParse("{{name}} {{name pascalCase}}", data)
|
||||
)
|
||||
|
||||
await createDirIfNotExists(outputDir, options)
|
||||
await createDirIfNotExists(path.dirname(outputPath), options)
|
||||
|
||||
log(options, LogLevel.Info, `Writing to ${outputPath}`)
|
||||
if (!exists || overwrite) {
|
||||
@@ -119,6 +140,7 @@ async function handleTemplateFile(
|
||||
|
||||
if (!options.dryRun) {
|
||||
await writeFile(outputPath, outputContents)
|
||||
log(options, LogLevel.Info, "Done.")
|
||||
} else {
|
||||
log(options, LogLevel.Info, "Content output:")
|
||||
log(options, LogLevel.Info, outputContents)
|
||||
|
||||
16
src/utils.ts
16
src/utils.ts
@@ -28,7 +28,7 @@ export function handleErr(err: NodeJS.ErrnoException | null) {
|
||||
}
|
||||
|
||||
export function log(options: ScaffoldConfig, level: LogLevel, ...obj: any[]) {
|
||||
if (options.quiet || options.verbose === LogLevel.None || (options.verbose ?? LogLevel.Info) > level) {
|
||||
if (options.quiet || options.verbose === LogLevel.None || level <= (options.verbose ?? LogLevel.Info)) {
|
||||
return
|
||||
}
|
||||
const levelColor: Record<LogLevel, keyof typeof chalk> = {
|
||||
@@ -39,8 +39,17 @@ export function log(options: ScaffoldConfig, level: LogLevel, ...obj: any[]) {
|
||||
[LogLevel.Error]: "red",
|
||||
}
|
||||
const chalkFn: any = chalk[levelColor[level]]
|
||||
console["log"](...obj.map((i) => (typeof i === "object" ? chalkFn(JSON.stringify(i, undefined, 1)) : chalkFn(i))))
|
||||
// console["log"](...obj)
|
||||
const key: "log" | "warn" | "error" = level === LogLevel.Error ? "error" : level === LogLevel.Warning ? "warn" : "log"
|
||||
const logFn: any = console[key]
|
||||
logFn(
|
||||
...obj.map((i) =>
|
||||
i instanceof Error
|
||||
? chalkFn(i, JSON.stringify(i, undefined, 1), i.stack)
|
||||
: typeof i === "object"
|
||||
? chalkFn(JSON.stringify(i, undefined, 1))
|
||||
: chalkFn(i)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export async function createDirIfNotExists(dir: string, options: ScaffoldConfig): Promise<void> {
|
||||
@@ -52,6 +61,7 @@ export async function createDirIfNotExists(dir: string, options: ScaffoldConfig)
|
||||
|
||||
if (!(await pathExists(dir))) {
|
||||
try {
|
||||
log(options, LogLevel.Debug, `Creating dir ${dir}`)
|
||||
await mkdir(dir)
|
||||
return
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import mockFs from "mock-fs"
|
||||
import FileSystem from "mock-fs/lib/filesystem"
|
||||
import Scaffold from "../src/scaffold"
|
||||
import { readdirSync, readFileSync } from "fs"
|
||||
import { Console } from "console"
|
||||
|
||||
const fileStructNormal = {
|
||||
input: {
|
||||
@@ -19,21 +20,32 @@ const fileStructWithData = {
|
||||
|
||||
const fileStructNested = {
|
||||
input: {
|
||||
"{{name}}-1.text": "This should be in root",
|
||||
"{{name}}-1.txt": "This should be in root",
|
||||
"{{Name}}": {
|
||||
"{{name}}-2.txt": "Hello, my value is {{value}}",
|
||||
moreNesting: {
|
||||
"{{name}}-3.txt": "Hi! My value is actually NOT {{value}}!",
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {},
|
||||
}
|
||||
|
||||
// let logsTemp: any = []
|
||||
// let logMock: any
|
||||
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunction): jest.EmptyFunction {
|
||||
return () => {
|
||||
beforeEach(() => {
|
||||
// console.log("Mocking:", fileStruct)
|
||||
console = new Console(process.stdout, process.stderr)
|
||||
|
||||
mockFs(fileStruct)
|
||||
// logMock = jest.spyOn(console, 'log').mockImplementation((...args) => {
|
||||
// logsTemp.push(args)
|
||||
// })
|
||||
})
|
||||
testFn()
|
||||
afterEach(() => {
|
||||
// console.log("Restoring mock")
|
||||
mockFs.restore()
|
||||
})
|
||||
}
|
||||
@@ -48,9 +60,8 @@ describe("Scaffold", () => {
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const data = readFileSync(process.cwd() + "/output/app_name.txt")
|
||||
expect(data.toString()).toBe("Hello, my app is app_name")
|
||||
})
|
||||
@@ -61,7 +72,7 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
createSubFolder: true,
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const data = readFileSync(process.cwd() + "/output/app_name/app_name.txt")
|
||||
@@ -79,7 +90,7 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
await Scaffold({
|
||||
@@ -87,7 +98,7 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
data: { value: "2" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const data = readFileSync(process.cwd() + "/output/app_name.txt")
|
||||
@@ -100,7 +111,7 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
await Scaffold({
|
||||
@@ -109,7 +120,7 @@ describe("Scaffold", () => {
|
||||
templates: ["input"],
|
||||
data: { value: "2" },
|
||||
overwrite: true,
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const data = readFileSync(process.cwd() + "/output/app_name.txt")
|
||||
@@ -121,9 +132,13 @@ describe("Scaffold", () => {
|
||||
describe(
|
||||
"errors",
|
||||
withMock(fileStructNormal, () => {
|
||||
let consoleMock: jest.SpyInstance
|
||||
let consoleMock1: jest.SpyInstance
|
||||
beforeAll(() => {
|
||||
consoleMock = jest.spyOn(console, "error").mockImplementation(() => void 0)
|
||||
consoleMock1 = jest.spyOn(console, "error").mockImplementation(() => void 0)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
consoleMock1.mockRestore()
|
||||
})
|
||||
|
||||
test("should throw for bad input", async () => {
|
||||
@@ -133,16 +148,12 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["non-existing-input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
).rejects.toThrow()
|
||||
|
||||
expect(() => readFileSync(process.cwd() + "/output/app_name.txt")).toThrow()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
@@ -155,7 +166,7 @@ describe("Scaffold", () => {
|
||||
output: (fullPath, basedir, basename) => `custom-output/${basename.split(".")[0]}`,
|
||||
templates: ["input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
const data = readFileSync(process.cwd() + "/custom-output/app_name/app_name.txt")
|
||||
expect(data.toString()).toBe("Hello, my app is app_name")
|
||||
@@ -169,14 +180,25 @@ describe("Scaffold", () => {
|
||||
test("should maintain input structure on output", async () => {
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: "./",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const dir = readdirSync(process.cwd())
|
||||
const rootDir = readdirSync(process.cwd() + "/output")
|
||||
const dir = readdirSync(process.cwd() + "/output/AppName")
|
||||
const nestedDir = readdirSync(process.cwd() + "/output/AppName/moreNesting")
|
||||
expect(rootDir).toHaveProperty("length")
|
||||
expect(dir).toHaveProperty("length")
|
||||
expect(nestedDir).toHaveProperty("length")
|
||||
|
||||
const rootFile = readFileSync(process.cwd() + "/output/app_name-1.txt")
|
||||
const oneDeepFile = readFileSync(process.cwd() + "/output/AppName/app_name-2.txt")
|
||||
const twoDeepFile = readFileSync(process.cwd() + "/output/AppName/moreNesting/app_name-3.txt")
|
||||
expect(rootFile.toString()).toEqual("This should be in root")
|
||||
expect(oneDeepFile.toString()).toEqual("Hello, my value is 1")
|
||||
expect(twoDeepFile.toString()).toEqual("Hi! My value is actually NOT 1!")
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user