diff --git a/src/file.ts b/src/file.ts index 3d0bdf6..f1db7df 100644 --- a/src/file.ts +++ b/src/file.ts @@ -71,11 +71,10 @@ export function getBasePath(relPath: string): string { .replace(process.cwd(), "") } -export async function getFileList(_config: ScaffoldConfig, template: string): Promise { - template = template.replaceAll(/[\\]+/g, "/") - log(_config, LogLevel.debug, `Getting file list for ${template}`) +export async function getFileList(config: ScaffoldConfig, templates: string[]): Promise { + log(config, LogLevel.debug, `Getting file list for glob list: ${templates}`) return ( - await glob(template, { + await glob(templates, { dot: true, nodir: true, }) diff --git a/src/scaffold.ts b/src/scaffold.ts index 67e0538..6b845a7 100644 --- a/src/scaffold.ts +++ b/src/scaffold.ts @@ -16,6 +16,7 @@ import { getFileList, getBasePath, handleTemplateFile, + GlobInfo, } from "./file" import { LogLevel, MinimalConfig, Resolver, ScaffoldCmdConfig, ScaffoldConfig } from "./types" import { registerHelpers } from "./parser" @@ -61,39 +62,45 @@ export async function Scaffold(config: ScaffoldConfig): Promise { 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 (let _template of includes) { try { const { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template } = await getTemplateGlobInfo( config, _template, ) - 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, - }) - } + templates.push({ nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template }) } catch (e: any) { handleErr(e) } } + 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: any) { log(config, LogLevel.error, e) throw e @@ -111,7 +118,7 @@ export async function Scaffold(config: ScaffoldConfig): Promise { * @category Main * @return {Promise} A promise that resolves when the scaffold is complete */ -Scaffold.fromConfig = async function ( +Scaffold.fromConfig = async function( /** The path or URL to the config file */ pathOrUrl: string, /** Information needed before loading the config */ diff --git a/src/types.ts b/src/types.ts index f3106d3..01b79ff 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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[] @@ -331,7 +336,9 @@ export type FileResponse = T | FileResponseHandler /** * 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. */ diff --git a/tests/scaffold.test.ts b/tests/scaffold.test.ts index d869f7f..3ae1d82 100644 --- a/tests/scaffold.test.ts +++ b/tests/scaffold.test.ts @@ -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(() => { @@ -268,8 +276,7 @@ describe("Scaffold", () => { }), ) - describe( - "output structure", + describe("output structure", () => { withMock(fileStructNested, () => { test("should maintain input structure on output", async () => { await Scaffold({ @@ -294,8 +301,23 @@ 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",