diff --git a/package.json b/package.json index 6d80c1a..97b2e2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-scaffold", - "version": "1.0.1", + "version": "1.0.2", "description": "Create files based on templates", "repository": "https://github.com/chenasraf/simple-scaffold.git", "author": "Chen Asraf ", diff --git a/src/filters.ts b/src/filters.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/scaffold.ts b/src/scaffold.ts index dd64f78..a91e023 100644 --- a/src/scaffold.ts +++ b/src/scaffold.ts @@ -1,16 +1,10 @@ -import { glob } from "glob" import path from "path" -import { promisify } from "util" -import { promises as fsPromises } from "fs" -const { readFile, writeFile } = fsPromises import { createDirIfNotExists, getOptionValueForFile, handleErr, - handlebarsParse, log, - pathExists, pascalCase, isDir, removeGlob, @@ -24,10 +18,8 @@ import { getTemplateFileInfo, logInitStep, logInputFile, - GlobInfo, - OutputFileInfo, } from "./utils" -import { FileResponse, LogLevel, ScaffoldConfig } from "./types" +import { LogLevel, ScaffoldConfig } from "./types" /** * Create a scaffold using given `options`. diff --git a/src/utils.ts b/src/utils.ts index 0565d20..2d2a287 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -95,19 +95,27 @@ export function getOptionValueForFile( } return (fn as FileResponseFn)( filePath, - path.dirname(handlebarsParse(options, filePath, data).toString()), - path.basename(handlebarsParse(options, filePath, data).toString()) + path.dirname(handlebarsParse(options, filePath, { isPath: true }).toString()), + path.basename(handlebarsParse(options, filePath, { isPath: true }).toString()) ) } export function handlebarsParse( options: ScaffoldConfig, templateBuffer: Buffer | string, - data: Record + { isPath = false }: { isPath?: boolean } = {} ) { + const { data } = options try { - const parser = Handlebars.compile(templateBuffer.toString(), { noEscape: true }) - const outputContents = parser(data) + let str = templateBuffer.toString() + if (isPath) { + str = str.replace(/\\/g, "/") + } + const parser = Handlebars.compile(str, { noEscape: true }) + let outputContents = parser(data) + if (isPath && path.sep !== "/") { + outputContents = outputContents.replace(/\//g, "\\") + } return outputContents } catch { log(options, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content") @@ -137,26 +145,28 @@ export async function isDir(path: string): Promise { } export function removeGlob(template: string) { - return template.replace(/\*/g, "").replace(/\/\//g, "/") + return template.replace(/\*/g, "").replace(/(\/\/|\\\\)/g, path.sep) } export function makeRelativePath(str: string): string { - return str.startsWith("/") ? str.slice(1) : str + return str.startsWith(path.sep) ? str.slice(1) : str } export function getBasePath(relPath: string) { return path .resolve(process.cwd(), relPath) - .replace(process.cwd() + "/", "") + .replace(process.cwd() + path.sep, "") .replace(process.cwd(), "") } export async function getFileList(options: ScaffoldConfig, template: string) { - return await promisify(glob)(template, { - dot: true, - debug: options.verbose === LogLevel.Debug, - nodir: true, - }) + return ( + await promisify(glob)(template, { + dot: true, + debug: options.verbose === LogLevel.Debug, + nodir: true, + }) + ).map((f) => f.replace(/\//g, path.sep)) } export interface GlobInfo { @@ -177,7 +187,7 @@ export async function getTemplateGlobInfo(options: ScaffoldConfig, template: str const _shouldAddGlob = !isGlob && isDirOrGlob const origTemplate = template if (_shouldAddGlob) { - _template = template + "/**/*" + _template = path.join(template, "**", "*") } return { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template: _template } } @@ -208,7 +218,9 @@ export async function getTemplateFileInfo( const inputPath = path.resolve(process.cwd(), templatePath) const outputPathOpt = getOptionValueForFile(options, inputPath, data, options.output) const outputDir = getOutputDir(options, data, outputPathOpt, basePath) - const outputPath = handlebarsParse(options, path.join(outputDir, path.basename(inputPath)), data).toString() + const outputPath = handlebarsParse(options, path.join(outputDir, path.basename(inputPath)), { + isPath: true, + }).toString() const exists = await pathExists(outputPath) return { inputPath, outputPathOpt, outputDir, outputPath, exists } } @@ -228,7 +240,7 @@ export async function copyFileTransformed( log(options, LogLevel.Info, `File ${outputPath} exists, overwriting`) } const templateBuffer = await readFile(inputPath) - const outputContents = handlebarsParse(options, templateBuffer, data) + const outputContents = handlebarsParse(options, templateBuffer) if (!options.dryRun) { await writeFile(outputPath, outputContents) @@ -255,7 +267,7 @@ export function getOutputDir( basePath, options.createSubFolder ? options.subFolderNameHelper - ? handlebarsParse(options, `{{ ${options.subFolderNameHelper} name }}`, data) + ? handlebarsParse(options, `{{ ${options.subFolderNameHelper} name }}`) : options.name : undefined, ].filter(Boolean) as string[]) diff --git a/tests/scaffold.test.ts b/tests/scaffold.test.ts index ed5fe4a..c68ffbd 100644 --- a/tests/scaffold.test.ts +++ b/tests/scaffold.test.ts @@ -4,6 +4,7 @@ import Scaffold from "../src/scaffold" import { readdirSync, readFileSync } from "fs" import { Console } from "console" import { defaultHelpers } from "../src/utils" +import { join } from "path" const fileStructNormal = { input: { @@ -83,7 +84,7 @@ describe("Scaffold", () => { templates: ["input"], verbose: 0, }) - const data = readFileSync(process.cwd() + "/output/app_name.txt") + const data = readFileSync(join(process.cwd(), "output", "app_name.txt")) expect(data.toString()).toBe("Hello, my app is app_name") }) @@ -96,7 +97,7 @@ describe("Scaffold", () => { verbose: 0, }) - const data = readFileSync(process.cwd() + "/output/app_name/app_name.txt") + const data = readFileSync(join(process.cwd(), "output", "app_name", "app_name.txt")) expect(data.toString()).toBe("Hello, my app is app_name") }) }) @@ -122,7 +123,7 @@ describe("Scaffold", () => { verbose: 0, }) - const data = readFileSync(process.cwd() + "/output/app_name.txt") + const data = readFileSync(join(process.cwd(), "output", "app_name.txt")) expect(data.toString()).toBe("Hello, my value is 1") }) @@ -144,7 +145,7 @@ describe("Scaffold", () => { verbose: 0, }) - const data = readFileSync(process.cwd() + "/output/app_name.txt") + const data = readFileSync(join(process.cwd(), "output", "app_name.txt")) expect(data.toString()).toBe("Hello, my value is 2") }) }) @@ -173,7 +174,7 @@ describe("Scaffold", () => { }) ).rejects.toThrow() - expect(() => readFileSync(process.cwd() + "/output/app_name.txt")).toThrow() + expect(() => readFileSync(join(process.cwd(), "output", "app_name.txt"))).toThrow() }) }) ) @@ -184,12 +185,12 @@ describe("Scaffold", () => { test("should allow override function", async () => { await Scaffold({ name: "app_name", - output: (fullPath, basedir, basename) => `custom-output/${basename.split(".")[0]}`, + output: (fullPath, basedir, basename) => join("custom-output", `${basename.split(".")[0]}`), templates: ["input"], data: { value: "1" }, verbose: 0, }) - const data = readFileSync(process.cwd() + "/custom-output/app_name/app_name.txt") + const data = readFileSync(join(process.cwd(), "/custom-output/app_name/app_name.txt")) expect(data.toString()).toBe("Hello, my app is app_name") }) }) @@ -207,16 +208,16 @@ describe("Scaffold", () => { verbose: 0, }) - const rootDir = readdirSync(process.cwd() + "/output") - const dir = readdirSync(process.cwd() + "/output/AppName") - const nestedDir = readdirSync(process.cwd() + "/output/AppName/moreNesting") + 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(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") + 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!") @@ -252,7 +253,7 @@ describe("Scaffold", () => { upperCase: "APP_NAME", } for (const key in results) { - const file = readFileSync(process.cwd() + `/output/defaults/${key}.txt`) + const file = readFileSync(join(process.cwd(), "output", "defaults", `${key}.txt`)) expect(file.toString()).toEqual(results[key as keyof typeof results]) } }) @@ -271,7 +272,7 @@ describe("Scaffold", () => { add1: "app_name 1", } for (const key in results) { - const file = readFileSync(process.cwd() + `/output/custom/${key}.txt`) + const file = readFileSync(join(process.cwd(), "output", "custom", `${key}.txt`)) expect(file.toString()).toEqual(results[key as keyof typeof results]) } }) @@ -290,7 +291,7 @@ describe("Scaffold", () => { verbose: 0, }) - const data = readFileSync(process.cwd() + "/output/app_name/app_name.txt") + const data = readFileSync(join(process.cwd(), "output", "app_name/app_name.txt")) expect(data.toString()).toBe("Hello, my app is app_name") }) @@ -304,7 +305,7 @@ describe("Scaffold", () => { subFolderNameHelper: "upperCase", }) - const data = readFileSync(process.cwd() + "/output/APP_NAME/app_name.txt") + const data = readFileSync(join(process.cwd(), "output", "APP_NAME/app_name.txt")) expect(data.toString()).toBe("Hello, my app is app_name") }) @@ -321,7 +322,7 @@ describe("Scaffold", () => { }, }) - const data = readFileSync(process.cwd() + "/output/REPLACED/app_name.txt") + const data = readFileSync(join(process.cwd(), "output", "REPLACED/app_name.txt")) expect(data.toString()).toBe("Hello, my app is app_name") }) }) diff --git a/tests/utils.test.ts b/tests/utils.test.ts new file mode 100644 index 0000000..c9d071b --- /dev/null +++ b/tests/utils.test.ts @@ -0,0 +1,54 @@ +import { handlebarsParse } from "../src/utils" +import { ScaffoldConfig } from "../src/types" +import path from "path" + +const blankConf: ScaffoldConfig = { + verbose: 0, + name: "", + output: "", + templates: [], + data: { name: "test" }, +} + +describe("Utils", () => { + describe("handlebarsParse", () => { + let origSep: any + 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 })).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("/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, + } + ) + ).toEqual("/home/test/test {{escaped}}.txt") + }) + }) +})