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>( (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> = { 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> = { 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 & "test"' }, }) const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString() expect(content).toEqual('Value: hello & "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() }) }, ), ) })