mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-18 01:29:09 +00:00
1246 lines
35 KiB
TypeScript
1246 lines
35 KiB
TypeScript
import {
|
|
describe,
|
|
test,
|
|
expect,
|
|
beforeEach,
|
|
afterEach,
|
|
beforeAll,
|
|
afterAll,
|
|
vi,
|
|
type MockInstance,
|
|
} from "vitest"
|
|
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"
|
|
import { defaultHelpers } from "../src/parser"
|
|
import { join } from "path"
|
|
import * as dateFns from "date-fns"
|
|
import crypto from "crypto"
|
|
|
|
const fileStructNormal = {
|
|
input: {
|
|
"{{name}}.txt": "Hello, my app is {{name}}",
|
|
},
|
|
output: {},
|
|
}
|
|
const fileStructWithBinary = {
|
|
input: {
|
|
"{{name}}.txt": "Hello, my app is {{name}}",
|
|
"{{name}}.bin": crypto.randomBytes(10000),
|
|
},
|
|
output: {},
|
|
}
|
|
|
|
const fileStructWithData = {
|
|
input: {
|
|
"{{name}}.txt": "Hello, my value is {{value}}",
|
|
},
|
|
output: {},
|
|
}
|
|
|
|
const fileStructNested = {
|
|
input: {
|
|
"{{name}}-1.txt": "This should be in root",
|
|
"{{pascalCase name}}": {
|
|
"{{name}}-2.txt": "Hello, my value is {{value}}",
|
|
moreNesting: {
|
|
"{{name}}-3.txt": "Hi! My value is actually NOT {{value}}!",
|
|
},
|
|
},
|
|
},
|
|
output: {},
|
|
}
|
|
const fileStructSubdirTransformer = {
|
|
input: {
|
|
"{{name}}.txt": "Hello, my app is {{name}}",
|
|
},
|
|
output: {},
|
|
}
|
|
|
|
const defaultHelperNames = Object.keys(defaultHelpers)
|
|
const fileStructHelpers = {
|
|
input: {
|
|
defaults: defaultHelperNames.reduce<Record<string, string>>(
|
|
(all, cur) => ({ ...all, [cur + ".txt"]: `{{ ${cur} name }}` }),
|
|
{},
|
|
),
|
|
custom: {
|
|
"add1.txt": "{{ add1 name }}",
|
|
},
|
|
},
|
|
output: {},
|
|
}
|
|
|
|
const fileStructDates = {
|
|
input: {
|
|
"now.txt": "Today is {{ now 'mmm' }}, time is {{ now 'HH:mm' }}",
|
|
"offset.txt": "Yesterday was {{ now 'mmm' -1 'days' }}, time is {{ now 'HH:mm' -1 'days' }}",
|
|
"custom.txt":
|
|
"Custom date is {{ date customDate 'mmm' }}, time is {{ date customDate 'HH:mm' }}",
|
|
},
|
|
output: {},
|
|
}
|
|
|
|
const fileStructExcludes = {
|
|
input: {
|
|
"include.txt": "This file should be included",
|
|
"exclude.txt": "This file should be excluded",
|
|
},
|
|
output: {},
|
|
}
|
|
|
|
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: () => void): () => void {
|
|
return () => {
|
|
beforeEach(() => {
|
|
// console.log("Mocking:", fileStruct)
|
|
console = new Console(process.stdout, process.stderr)
|
|
|
|
mockFs(fileStruct)
|
|
// logMock = vi.spyOn(console, 'log').mockImplementation((...args) => {
|
|
// logsTemp.push(args)
|
|
// })
|
|
})
|
|
testFn()
|
|
afterEach(() => {
|
|
// console.log("Restoring mock")
|
|
mockFs.restore()
|
|
})
|
|
}
|
|
}
|
|
|
|
describe("Scaffold", () => {
|
|
describe(
|
|
"create subdir",
|
|
|
|
withMock(fileStructNormal, () => {
|
|
test("should not create by default", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
})
|
|
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
|
expect(data.toString()).toEqual("Hello, my app is app_name")
|
|
})
|
|
|
|
test("should create with config", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
subdir: true,
|
|
logLevel: "none",
|
|
})
|
|
|
|
const data = readFileSync(join(process.cwd(), "output", "app_name", "app_name.txt"))
|
|
expect(data.toString()).toEqual("Hello, my app is app_name")
|
|
})
|
|
}),
|
|
)
|
|
|
|
describe(
|
|
"binary files",
|
|
|
|
withMock(fileStructWithBinary, () => {
|
|
test("should copy as-is", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
})
|
|
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
|
expect(data.toString()).toEqual("Hello, my app is app_name")
|
|
const dataBin = readFileSync(join(process.cwd(), "output", "app_name.bin"))
|
|
expect(dataBin).toEqual(fileStructWithBinary.input["{{name}}.bin"])
|
|
})
|
|
}),
|
|
)
|
|
|
|
describe(
|
|
"overwrite",
|
|
withMock(fileStructWithData, () => {
|
|
test("should not overwrite by default", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
data: { value: "1" },
|
|
logLevel: "none",
|
|
})
|
|
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
data: { value: "2" },
|
|
logLevel: "none",
|
|
})
|
|
|
|
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
|
expect(data.toString()).toEqual("Hello, my value is 1")
|
|
})
|
|
|
|
test("should overwrite with config", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
data: { value: "1" },
|
|
logLevel: "none",
|
|
})
|
|
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
data: { value: "2" },
|
|
overwrite: true,
|
|
logLevel: "none",
|
|
})
|
|
|
|
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
|
expect(data.toString()).toEqual("Hello, my value is 2")
|
|
})
|
|
}),
|
|
)
|
|
|
|
describe(
|
|
"errors",
|
|
withMock(fileStructNormal, () => {
|
|
let consoleMock1: MockInstance
|
|
beforeAll(() => {
|
|
consoleMock1 = vi.spyOn(console, "error").mockImplementation(() => void 0)
|
|
})
|
|
|
|
afterAll(() => {
|
|
consoleMock1.mockRestore()
|
|
})
|
|
|
|
test("should throw for bad input", async () => {
|
|
await expect(
|
|
Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["non-existing-input"],
|
|
data: { value: "1" },
|
|
logLevel: "none",
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
await expect(
|
|
Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["non-existing-input/non-existing-file.txt"],
|
|
data: { value: "1" },
|
|
logLevel: "none",
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
expect(() => readFileSync(join(process.cwd(), "output", "app_name.txt"))).toThrow()
|
|
})
|
|
}),
|
|
)
|
|
|
|
describe(
|
|
"dry run",
|
|
withMock(fileStructNormal, () => {
|
|
let consoleMock1: MockInstance
|
|
beforeAll(() => {
|
|
consoleMock1 = vi.spyOn(console, "error").mockImplementation(() => void 0)
|
|
})
|
|
|
|
afterAll(() => {
|
|
consoleMock1.mockRestore()
|
|
})
|
|
|
|
test("should not write to disk", async () => {
|
|
Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
data: { value: "1" },
|
|
logLevel: "none",
|
|
dryRun: true,
|
|
})
|
|
|
|
expect(() => readFileSync(join(process.cwd(), "output", "app_name.txt"))).toThrow()
|
|
})
|
|
}),
|
|
)
|
|
|
|
describe(
|
|
"outputPath override",
|
|
withMock(fileStructNormal, () => {
|
|
test("should allow override function", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: (_, __, basename) => join("custom-output", `${basename.split(".")[0]}`),
|
|
templates: ["input"],
|
|
data: { value: "1" },
|
|
logLevel: "none",
|
|
})
|
|
const data = readFileSync(join(process.cwd(), "/custom-output/app_name/app_name.txt"))
|
|
expect(data.toString()).toEqual("Hello, my app is app_name")
|
|
})
|
|
}),
|
|
)
|
|
|
|
describe(
|
|
"output structure",
|
|
withMock(fileStructNested, () => {
|
|
test("should maintain input structure on output", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
data: { value: "1" },
|
|
logLevel: "none",
|
|
})
|
|
|
|
const rootDir = readdirSync(join(process.cwd(), "output"))
|
|
const dir = readdirSync(join(process.cwd(), "output", "AppName"))
|
|
const nestedDir = readdirSync(join(process.cwd(), "output", "AppName", "moreNesting"))
|
|
expect(rootDir).toHaveProperty("length")
|
|
expect(dir).toHaveProperty("length")
|
|
expect(nestedDir).toHaveProperty("length")
|
|
|
|
const rootFile = readFileSync(join(process.cwd(), "output", "app_name-1.txt"))
|
|
const oneDeepFile = readFileSync(join(process.cwd(), "output", "AppName/app_name-2.txt"))
|
|
const twoDeepFile = readFileSync(
|
|
join(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!")
|
|
})
|
|
}),
|
|
)
|
|
|
|
describe(
|
|
"file exclusion via glob pattern",
|
|
withMock(fileStructExcludes, () => {
|
|
test("should only include matching files", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input/include.*"],
|
|
data: { value: "1" },
|
|
logLevel: "none",
|
|
})
|
|
const outputFiles = readdirSync(join(process.cwd(), "output"))
|
|
expect(outputFiles).toContain("include.txt")
|
|
expect(outputFiles).not.toContain("exclude.txt")
|
|
})
|
|
}),
|
|
)
|
|
|
|
describe(
|
|
"capitalization helpers",
|
|
withMock(fileStructHelpers, () => {
|
|
const _helpers: Record<string, (_text: string) => string> = {
|
|
add1: (text) => text + " 1",
|
|
}
|
|
|
|
test("should work", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
helpers: _helpers,
|
|
})
|
|
|
|
const results = {
|
|
camelCase: "appName",
|
|
snakeCase: "app_name",
|
|
startCase: "App Name",
|
|
kebabCase: "app-name",
|
|
hyphenCase: "app-name",
|
|
pascalCase: "AppName",
|
|
lowerCase: "app_name",
|
|
upperCase: "APP_NAME",
|
|
}
|
|
for (const key in results) {
|
|
const file = readFileSync(join(process.cwd(), "output", "defaults", `${key}.txt`))
|
|
expect(file.toString()).toEqual(results[key as keyof typeof results])
|
|
}
|
|
})
|
|
}),
|
|
)
|
|
describe(
|
|
"date helpers",
|
|
withMock(fileStructDates, () => {
|
|
test("should work", async () => {
|
|
const now = new Date()
|
|
const yesterday = dateFns.add(new Date(), { days: -1 })
|
|
const customDate = dateFns.formatISO(dateFns.add(new Date(), { days: -1 }))
|
|
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
data: { customDate },
|
|
})
|
|
|
|
const nowFile = readFileSync(join(process.cwd(), "output", "now.txt"))
|
|
const offsetFile = readFileSync(join(process.cwd(), "output", "offset.txt"))
|
|
const customFile = readFileSync(join(process.cwd(), "output", "custom.txt"))
|
|
|
|
// "now.txt": "Today is {{ now 'mmm' }}, time is {{ now 'HH:mm' }}",
|
|
// "offset.txt": "Yesterday was {{ now 'mmm' -1 'days' }}, time is {{ now 'HH:mm' -1 'days' }}",
|
|
// "custom.txt": "Custom date is {{ date customDate 'mmm' }}, time is {{ date customDate 'HH:mm' }}",
|
|
|
|
expect(nowFile.toString()).toEqual(
|
|
`Today is ${dateFns.format(now, "mmm")}, time is ${dateFns.format(now, "HH:mm")}`,
|
|
)
|
|
expect(offsetFile.toString()).toEqual(
|
|
`Yesterday was ${dateFns.format(yesterday, "mmm")}, time is ${dateFns.format(yesterday, "HH:mm")}`,
|
|
)
|
|
expect(customFile.toString()).toEqual(
|
|
`Custom date is ${dateFns.format(dateFns.parseISO(customDate), "mmm")}, time is ${dateFns.format(
|
|
dateFns.parseISO(customDate),
|
|
"HH:mm",
|
|
)}`,
|
|
)
|
|
})
|
|
}),
|
|
)
|
|
describe(
|
|
"custom helpers",
|
|
withMock(fileStructHelpers, () => {
|
|
const _helpers: Record<string, (_text: string) => string> = {
|
|
add1: (text) => text + " 1",
|
|
}
|
|
test("should work", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
helpers: _helpers,
|
|
})
|
|
|
|
const results = {
|
|
add1: "app_name 1",
|
|
}
|
|
for (const key in results) {
|
|
const file = readFileSync(join(process.cwd(), "output", "custom", `${key}.txt`))
|
|
expect(file.toString()).toEqual(results[key as keyof typeof results])
|
|
}
|
|
})
|
|
}),
|
|
)
|
|
describe(
|
|
"transform subdir",
|
|
withMock(fileStructSubdirTransformer, () => {
|
|
test("should work with no helper", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
subdir: true,
|
|
logLevel: "none",
|
|
})
|
|
|
|
const data = readFileSync(join(process.cwd(), "output", "app_name", "app_name.txt"))
|
|
expect(data.toString()).toEqual("Hello, my app is app_name")
|
|
})
|
|
|
|
test("should work with default helper", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
subdir: true,
|
|
logLevel: "none",
|
|
subdirHelper: "upperCase",
|
|
})
|
|
|
|
const data = readFileSync(join(process.cwd(), "output", "APP_NAME", "app_name.txt"))
|
|
expect(data.toString()).toEqual("Hello, my app is app_name")
|
|
})
|
|
|
|
test("should work with custom helper", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
subdir: true,
|
|
logLevel: "none",
|
|
subdirHelper: "test",
|
|
helpers: {
|
|
test: () => "REPLACED",
|
|
},
|
|
})
|
|
|
|
const data = readFileSync(join(process.cwd(), "output", "REPLACED", "app_name.txt"))
|
|
expect(data.toString()).toEqual("Hello, my app is app_name")
|
|
})
|
|
}),
|
|
)
|
|
describe(
|
|
"before write",
|
|
withMock(fileStructNormal, () => {
|
|
test("should work with no callback", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
data: {
|
|
value: "value",
|
|
},
|
|
})
|
|
|
|
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
|
expect(data.toString()).toEqual("Hello, my app is app_name")
|
|
})
|
|
|
|
test("should work with custom callback", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
data: {
|
|
value: "value",
|
|
},
|
|
beforeWrite: (content, beforeContent, outputPath) =>
|
|
[content.toString().toUpperCase(), beforeContent, outputPath].join(", "),
|
|
})
|
|
|
|
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
|
expect(data.toString()).toEqual(
|
|
[
|
|
"Hello, my app is app_name".toUpperCase(),
|
|
fileStructNormal.input["{{name}}.txt"],
|
|
join(process.cwd(), "output", "app_name.txt"),
|
|
].join(", "),
|
|
)
|
|
})
|
|
test("should work with undefined response custom callback", async () => {
|
|
await Scaffold({
|
|
name: "app_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
data: {
|
|
value: "value",
|
|
},
|
|
beforeWrite: () => undefined,
|
|
})
|
|
|
|
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
|
expect(data.toString()).toEqual("Hello, my app is app_name")
|
|
})
|
|
}),
|
|
)
|
|
|
|
describe(
|
|
"name is available in data",
|
|
withMock(
|
|
{
|
|
input: { "file.txt": "Name: {{name}}" },
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("name is automatically injected into data", async () => {
|
|
await Scaffold({
|
|
name: "my_project",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
})
|
|
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
|
expect(content).toEqual("Name: my_project")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"data overrides name in data",
|
|
withMock(
|
|
{
|
|
input: { "file.txt": "Name: {{name}}" },
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("explicit data.name takes precedence", async () => {
|
|
await Scaffold({
|
|
name: "original_name",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
data: { name: "custom_name" },
|
|
})
|
|
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
|
expect(content).toEqual("Name: custom_name")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"multiple templates",
|
|
withMock(
|
|
{
|
|
template1: { "file1.txt": "From template 1: {{name}}" },
|
|
template2: { "file2.txt": "From template 2: {{name}}" },
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("processes multiple template directories", async () => {
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["template1", "template2"],
|
|
logLevel: "none",
|
|
})
|
|
const file1 = readFileSync(join(process.cwd(), "output", "file1.txt")).toString()
|
|
const file2 = readFileSync(join(process.cwd(), "output", "file2.txt")).toString()
|
|
expect(file1).toEqual("From template 1: app")
|
|
expect(file2).toEqual("From template 2: app")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"template with custom data",
|
|
withMock(
|
|
{
|
|
input: { "{{name}}.txt": "Author: {{author}}, Version: {{version}}" },
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("uses custom data in content and filename", async () => {
|
|
await Scaffold({
|
|
name: "my_app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
data: { author: "John", version: "2.0" },
|
|
})
|
|
const content = readFileSync(join(process.cwd(), "output", "my_app.txt")).toString()
|
|
expect(content).toEqual("Author: John, Version: 2.0")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"template with helpers in filenames",
|
|
withMock(
|
|
{
|
|
input: { "{{pascalCase name}}.tsx": "component {{pascalCase name}}" },
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("applies helpers to filenames", async () => {
|
|
await Scaffold({
|
|
name: "my_component",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
})
|
|
const content = readFileSync(join(process.cwd(), "output", "MyComponent.tsx")).toString()
|
|
expect(content).toEqual("component MyComponent")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"template with helpers in directory names",
|
|
withMock(
|
|
{
|
|
input: {
|
|
"{{kebabCase name}}": {
|
|
"index.ts": "export from {{name}}",
|
|
},
|
|
},
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("applies helpers to directory names", async () => {
|
|
await Scaffold({
|
|
name: "MyComponent",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
})
|
|
const content = readFileSync(
|
|
join(process.cwd(), "output", "my-component", "index.ts"),
|
|
).toString()
|
|
expect(content).toEqual("export from MyComponent")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"deeply nested template structure",
|
|
withMock(
|
|
{
|
|
input: {
|
|
"root.txt": "root",
|
|
level1: {
|
|
"l1.txt": "level 1",
|
|
level2: {
|
|
"l2.txt": "level 2",
|
|
level3: {
|
|
"l3.txt": "level 3 {{name}}",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("preserves deep nesting", async () => {
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
})
|
|
expect(readFileSync(join(process.cwd(), "output", "root.txt")).toString()).toEqual("root")
|
|
expect(
|
|
readFileSync(join(process.cwd(), "output", "level1", "l1.txt")).toString(),
|
|
).toEqual("level 1")
|
|
expect(
|
|
readFileSync(join(process.cwd(), "output", "level1", "level2", "l2.txt")).toString(),
|
|
).toEqual("level 2")
|
|
expect(
|
|
readFileSync(
|
|
join(process.cwd(), "output", "level1", "level2", "level3", "l3.txt"),
|
|
).toString(),
|
|
).toEqual("level 3 app")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"overwrite as function",
|
|
withMock(
|
|
{
|
|
input: {
|
|
"keep.txt": "new keep",
|
|
"replace.txt": "new replace",
|
|
},
|
|
output: {
|
|
"keep.txt": "old keep",
|
|
"replace.txt": "old replace",
|
|
},
|
|
},
|
|
() => {
|
|
test("per-file overwrite control", async () => {
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
overwrite: (_fullPath, _basedir, basename) => basename === "replace.txt",
|
|
})
|
|
expect(readFileSync(join(process.cwd(), "output", "keep.txt")).toString()).toEqual(
|
|
"old keep",
|
|
)
|
|
expect(readFileSync(join(process.cwd(), "output", "replace.txt")).toString()).toEqual(
|
|
"new replace",
|
|
)
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"multiple custom helpers",
|
|
withMock(
|
|
{
|
|
input: {
|
|
"file.txt": "{{reverse name}} - {{repeat name}}",
|
|
},
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("multiple custom helpers work together", async () => {
|
|
await Scaffold({
|
|
name: "abc",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
helpers: {
|
|
reverse: (text: string) => text.split("").reverse().join(""),
|
|
repeat: (text: string) => text + text,
|
|
},
|
|
})
|
|
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
|
expect(content).toEqual("cba - abcabc")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"subdirHelper with different helpers",
|
|
withMock(
|
|
{
|
|
input: { "file.txt": "content" },
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("subdirHelper camelCase", async () => {
|
|
await Scaffold({
|
|
name: "my_component",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
subdir: true,
|
|
subdirHelper: "camelCase",
|
|
})
|
|
const content = readFileSync(
|
|
join(process.cwd(), "output", "myComponent", "file.txt"),
|
|
).toString()
|
|
expect(content).toEqual("content")
|
|
})
|
|
|
|
test("subdirHelper kebabCase", async () => {
|
|
await Scaffold({
|
|
name: "MyComponent",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
subdir: true,
|
|
subdirHelper: "kebabCase",
|
|
})
|
|
const content = readFileSync(
|
|
join(process.cwd(), "output", "my-component", "file.txt"),
|
|
).toString()
|
|
expect(content).toEqual("content")
|
|
})
|
|
|
|
test("subdirHelper snakeCase", async () => {
|
|
await Scaffold({
|
|
name: "MyComponent",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
subdir: true,
|
|
subdirHelper: "snakeCase",
|
|
})
|
|
const content = readFileSync(
|
|
join(process.cwd(), "output", "my_component", "file.txt"),
|
|
).toString()
|
|
expect(content).toEqual("content")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"empty template directory",
|
|
withMock(
|
|
{
|
|
input: {},
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("handles empty template dir gracefully", async () => {
|
|
await expect(
|
|
Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
}),
|
|
).resolves.toBeUndefined()
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"template with special characters in data",
|
|
withMock(
|
|
{
|
|
input: { "file.txt": "Value: {{value}}" },
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("handles special characters in data values", async () => {
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
data: { value: 'hello & <world> "test"' },
|
|
})
|
|
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
|
expect(content).toEqual('Value: hello & <world> "test"')
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"beforeWrite with async callback",
|
|
withMock(
|
|
{
|
|
input: { "file.txt": "Hello {{name}}" },
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("supports async beforeWrite", async () => {
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
beforeWrite: async (content) => {
|
|
return content.toString().replace("Hello", "Hi")
|
|
},
|
|
})
|
|
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
|
expect(content).toEqual("Hi app")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"beforeWrite receives all arguments",
|
|
withMock(
|
|
{
|
|
input: { "{{name}}.txt": "Template: {{name}}" },
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("beforeWrite gets content, rawContent, and outputPath", async () => {
|
|
const beforeWriteSpy = vi.fn().mockReturnValue(undefined)
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
beforeWrite: beforeWriteSpy,
|
|
})
|
|
expect(beforeWriteSpy).toHaveBeenCalledTimes(1)
|
|
const [content, rawContent, outputPath] = beforeWriteSpy.mock.calls[0]
|
|
expect(content.toString()).toEqual("Template: app")
|
|
expect(rawContent.toString()).toEqual("Template: {{name}}")
|
|
expect(outputPath).toContain("app.txt")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"multiple binary files",
|
|
withMock(
|
|
{
|
|
input: {
|
|
"img1.bin": crypto.randomBytes(5000),
|
|
"img2.bin": crypto.randomBytes(8000),
|
|
"text.txt": "regular text {{name}}",
|
|
},
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("handles mix of binary and text files", async () => {
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
})
|
|
const text = readFileSync(join(process.cwd(), "output", "text.txt")).toString()
|
|
expect(text).toEqual("regular text app")
|
|
const bin1 = readFileSync(join(process.cwd(), "output", "img1.bin"))
|
|
const bin2 = readFileSync(join(process.cwd(), "output", "img2.bin"))
|
|
expect(bin1.length).toBeGreaterThan(0)
|
|
expect(bin2.length).toBeGreaterThan(0)
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"output with function returning dynamic path",
|
|
withMock(
|
|
{
|
|
input: {
|
|
"component.tsx": "component {{name}}",
|
|
"style.css": "style for {{name}}",
|
|
},
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("output function can route files to different directories", async () => {
|
|
await Scaffold({
|
|
name: "Button",
|
|
output: (_fullPath, _basedir, basename) => {
|
|
if (basename.endsWith(".css")) return join("output", "styles")
|
|
return join("output", "components")
|
|
},
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
})
|
|
const component = readFileSync(
|
|
join(process.cwd(), "output", "components", "component.tsx"),
|
|
).toString()
|
|
const style = readFileSync(
|
|
join(process.cwd(), "output", "styles", "style.css"),
|
|
).toString()
|
|
expect(component).toEqual("component Button")
|
|
expect(style).toEqual("style for Button")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"handlebars block helpers",
|
|
withMock(
|
|
{
|
|
input: {
|
|
"file.txt": "{{#if showHeader}}Header\n{{/if}}Body for {{name}}",
|
|
},
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("supports handlebars block helpers in templates", async () => {
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
data: { showHeader: true },
|
|
})
|
|
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
|
expect(content).toEqual("Header\nBody for app")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"glob pattern as template",
|
|
withMock(
|
|
{
|
|
src: {
|
|
"file1.txt": "text 1 {{name}}",
|
|
"file2.txt": "text 2 {{name}}",
|
|
"file3.js": "js {{name}}",
|
|
},
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("glob pattern selects matching files only", async () => {
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["src/*.txt"],
|
|
logLevel: "none",
|
|
})
|
|
// glob templates maintain structure relative to the non-glob part
|
|
const outputFiles = readdirSync(join(process.cwd(), "output", "src"))
|
|
expect(outputFiles).toContain("file1.txt")
|
|
expect(outputFiles).toContain("file2.txt")
|
|
expect(outputFiles).not.toContain("file3.js")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"dotfiles in template",
|
|
withMock(
|
|
{
|
|
input: {
|
|
".gitignore": "node_modules",
|
|
".env.example": "KEY={{name}}",
|
|
},
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("includes dotfiles in output", async () => {
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
})
|
|
expect(readFileSync(join(process.cwd(), "output", ".gitignore")).toString()).toEqual(
|
|
"node_modules",
|
|
)
|
|
expect(readFileSync(join(process.cwd(), "output", ".env.example")).toString()).toEqual(
|
|
"KEY=app",
|
|
)
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"large number of files",
|
|
withMock(
|
|
{
|
|
input: Object.fromEntries(
|
|
Array.from({ length: 50 }, (_, i) => [`file${i}.txt`, `Content ${i} for {{name}}`]),
|
|
),
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("handles many files", async () => {
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
})
|
|
const files = readdirSync(join(process.cwd(), "output"))
|
|
expect(files.length).toBe(50)
|
|
expect(readFileSync(join(process.cwd(), "output", "file0.txt")).toString()).toEqual(
|
|
"Content 0 for app",
|
|
)
|
|
expect(readFileSync(join(process.cwd(), "output", "file49.txt")).toString()).toEqual(
|
|
"Content 49 for app",
|
|
)
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
".scaffoldignore",
|
|
withMock(
|
|
{
|
|
input: {
|
|
".scaffoldignore": "*.log\nREADME.md\n",
|
|
"file.txt": "included",
|
|
"debug.log": "excluded log",
|
|
"README.md": "excluded readme",
|
|
"other.js": "included js",
|
|
},
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("excludes files matching .scaffoldignore patterns", async () => {
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
})
|
|
const files = readdirSync(join(process.cwd(), "output"))
|
|
expect(files).toContain("file.txt")
|
|
expect(files).toContain("other.js")
|
|
expect(files).not.toContain("debug.log")
|
|
expect(files).not.toContain("README.md")
|
|
expect(files).not.toContain(".scaffoldignore")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
".scaffoldignore not copied to output",
|
|
withMock(
|
|
{
|
|
input: {
|
|
".scaffoldignore": "*.log",
|
|
"file.txt": "content",
|
|
},
|
|
output: {},
|
|
},
|
|
() => {
|
|
test(".scaffoldignore is never in output", async () => {
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
})
|
|
const files = readdirSync(join(process.cwd(), "output"))
|
|
expect(files).not.toContain(".scaffoldignore")
|
|
expect(files).toContain("file.txt")
|
|
})
|
|
},
|
|
),
|
|
)
|
|
|
|
describe(
|
|
"afterScaffold hook",
|
|
withMock(
|
|
{
|
|
input: { "file.txt": "Hello {{name}}" },
|
|
output: {},
|
|
},
|
|
() => {
|
|
test("calls function hook with config and files", async () => {
|
|
const hookFn = vi.fn()
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
afterScaffold: hookFn,
|
|
})
|
|
expect(hookFn).toHaveBeenCalledOnce()
|
|
const ctx = hookFn.mock.calls[0][0]
|
|
expect(ctx.config.name).toEqual("app")
|
|
expect(ctx.files.length).toBe(1)
|
|
expect(ctx.files[0]).toContain("file.txt")
|
|
})
|
|
|
|
test("calls async function hook", async () => {
|
|
let called = false
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
afterScaffold: async () => {
|
|
called = true
|
|
},
|
|
})
|
|
expect(called).toBe(true)
|
|
})
|
|
|
|
test("does not call hook when no files written (dry run)", async () => {
|
|
const hookFn = vi.fn()
|
|
await Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
dryRun: true,
|
|
afterScaffold: hookFn,
|
|
})
|
|
expect(hookFn).toHaveBeenCalledOnce()
|
|
expect(hookFn.mock.calls[0][0].files.length).toBe(0)
|
|
})
|
|
|
|
test("does not call hook when not provided", async () => {
|
|
await expect(
|
|
Scaffold({
|
|
name: "app",
|
|
output: "output",
|
|
templates: ["input"],
|
|
logLevel: "none",
|
|
}),
|
|
).resolves.toBeUndefined()
|
|
})
|
|
},
|
|
),
|
|
)
|
|
})
|