fix: strip tmpDir from output dir (#108)

* fix: strip tmpDir from output dir

* ci: test PRs

* fix: cmd

* fix: use relative path for replacement

* chore: remove todo
This commit is contained in:
Chen Asraf
2024-10-03 14:12:30 +03:00
committed by GitHub
parent 162cc8cec1
commit 80c92bfe84
9 changed files with 93 additions and 71 deletions

View File

@@ -1,6 +1,9 @@
name: Release
on:
pull_request:
branches:
- master
push:
branches:
- master
@@ -11,6 +14,7 @@ permissions:
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -20,7 +24,9 @@ jobs:
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm test
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -32,6 +38,8 @@ jobs:
- run: pnpm build
release:
name: Release Please
if: github.event_name == 'push'
needs:
- build
- test
@@ -47,6 +55,7 @@ jobs:
target-branch: master
publish:
name: NPM Publish
needs: release
runs-on: ubuntu-latest
if: ${{ needs.release.outputs.release_created }}

View File

@@ -30,17 +30,17 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
return
}
log(config, LogLevel.info, `Simple Scaffold v${pkg.version}`)
const tmpPath = generateUniqueTmpPath()
config.tmpDir = generateUniqueTmpPath()
try {
log(config, LogLevel.debug, "Parsing config file...", config)
const parsed = await parseConfigFile(config, tmpPath)
const parsed = await parseConfigFile(config)
await Scaffold(parsed)
} catch (e) {
const message = "message" in (e as any) ? (e as any).message : e?.toString()
log(config, LogLevel.error, message)
} finally {
log(config, LogLevel.debug, "Cleaning up temporary files...", tmpPath)
await fs.rm(tmpPath, { recursive: true, force: true })
log(config, LogLevel.debug, "Cleaning up temporary files...", config.tmpDir)
await fs.rm(config.tmpDir, { recursive: true, force: true })
}
})
.option({
@@ -171,7 +171,6 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
aliases: ["ls"],
description: "List all available templates for a given config. See `list -h` for more information.",
run: async (_config) => {
const tmpPath = generateUniqueTmpPath()
const config = {
templates: [],
name: "",
@@ -180,19 +179,20 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
subdir: false,
overwrite: false,
dryRun: false,
tmpDir: generateUniqueTmpPath(),
..._config,
config: _config.config ?? (!_config.git ? process.cwd() : undefined),
}
try {
const file = await getConfigFile(config, tmpPath)
const file = await getConfigFile(config)
console.log(colorize.underline`Available templates:\n`)
console.log(Object.keys(file).join("\n"))
} catch (e) {
const message = "message" in (e as any) ? (e as any).message : e?.toString()
log(config, LogLevel.error, message)
} finally {
log(config, LogLevel.debug, "Cleaning up temporary files...", tmpPath)
await fs.rm(tmpPath, { recursive: true, force: true })
log(config, LogLevel.debug, "Cleaning up temporary files...", config.tmpDir)
await fs.rm(config.tmpDir, { recursive: true, force: true })
}
},
})

View File

@@ -17,7 +17,7 @@ import { log } from "./logger"
import { resolve, wrapNoopResolver } from "./utils"
import { getGitConfig } from "./git"
import { createDirIfNotExists, getUniqueTmpPath, isDir, pathExists } from "./file"
import { exec, spawn } from "node:child_process"
import { exec } from "node:child_process"
/** @internal */
export function getOptionValueForFile<T>(
@@ -31,8 +31,8 @@ export function getOptionValueForFile<T>(
}
return (fn as FileResponseHandler<T>)(
filePath,
path.dirname(handlebarsParse(config, filePath, { isPath: true }).toString()),
path.basename(handlebarsParse(config, filePath, { isPath: true }).toString()),
path.dirname(handlebarsParse(config, filePath, { asPath: true }).toString()),
path.basename(handlebarsParse(config, filePath, { asPath: true }).toString()),
)
}
@@ -52,7 +52,7 @@ function isWrappedWithQuotes(string: string): boolean {
}
/** @internal */
export async function getConfigFile(config: ScaffoldCmdConfig, tmpPath: string): Promise<ScaffoldConfigMap> {
export async function getConfigFile(config: ScaffoldCmdConfig): Promise<ScaffoldConfigMap> {
if (config.git && !config.git.includes("://")) {
log(config, LogLevel.info, `Loading config from GitHub ${config.git}`)
config.git = githubPartToUrl(config.git)
@@ -65,7 +65,7 @@ export async function getConfigFile(config: ScaffoldCmdConfig, tmpPath: string):
log(config, LogLevel.info, `Loading config from file ${configFilename}`)
const configPromise = await (isGit
? getRemoteConfig({ git: configPath, config: configFilename, logLevel: config.logLevel, tmpPath })
? getRemoteConfig({ git: configPath, config: configFilename, logLevel: config.logLevel, tmpDir: config.tmpDir! })
: getLocalConfig({ config: configFilename, logLevel: config.logLevel }))
// resolve the config
@@ -80,8 +80,20 @@ export async function getConfigFile(config: ScaffoldCmdConfig, tmpPath: string):
}
/** @internal */
export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string): Promise<ScaffoldConfig> {
let output: ScaffoldConfig = { ...config, beforeWrite: undefined }
export async function parseConfigFile(config: ScaffoldCmdConfig): Promise<ScaffoldConfig> {
let output: ScaffoldConfig = {
name: config.name,
templates: [],
output: config.output,
logLevel: config.logLevel,
dryRun: config.dryRun,
data: config.data,
subdir: config.subdir,
overwrite: config.overwrite,
subdirHelper: config.subdirHelper,
beforeWrite: undefined,
tmpDir: config.tmpDir!,
}
if (config.quiet) {
config.logLevel = LogLevel.none
@@ -91,7 +103,7 @@ export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string
if (shouldLoadConfig) {
const key = config.key ?? "default"
const configImport = await getConfigFile(config, tmpPath)
const configImport = await getConfigFile(config)
if (!configImport[key]) {
throw new Error(`Template "${key}" not found in ${config.config}`)
@@ -100,11 +112,11 @@ export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string
const imported = configImport[key]
log(config, LogLevel.debug, "Imported result", imported)
output = {
...config,
...output,
...imported,
beforeWrite: undefined,
data: {
...(imported as any).data,
...imported.data,
...config.data,
},
}
@@ -158,7 +170,7 @@ export async function getLocalConfig(config: ConfigLoadConfig & Partial<LogConfi
export async function getRemoteConfig(
config: RemoteConfigLoadConfig & Partial<LogConfig>,
): Promise<ScaffoldConfigFile> {
const { config: configFile, git, tmpPath, ...logConfig } = config as Required<typeof config>
const { config: configFile, git, tmpDir, ...logConfig } = config as Required<typeof config>
log(logConfig, LogLevel.info, `Loading config from remote ${git}, file ${configFile}`)
@@ -170,7 +182,7 @@ export async function getRemoteConfig(
throw new Error(`Unsupported protocol ${url.protocol}`)
}
return getGitConfig(url, configFile, tmpPath, logConfig)
return getGitConfig(url, configFile, tmpDir, logConfig)
}
/** @internal */
@@ -194,13 +206,13 @@ function wrapBeforeWrite(
beforeWrite: string,
): ScaffoldConfig["beforeWrite"] {
return async (content, rawContent, outputFile) => {
const tmpPath = path.join(getUniqueTmpPath(), path.basename(outputFile))
await createDirIfNotExists(path.dirname(tmpPath), config)
const tmpDir = path.join(getUniqueTmpPath(), path.basename(outputFile))
await createDirIfNotExists(path.dirname(tmpDir), config)
const ext = path.extname(outputFile)
const rawTmpPath = tmpPath.replace(ext, ".raw" + ext)
const rawTmpPath = tmpDir.replace(ext, ".raw" + ext)
try {
log(config, LogLevel.debug, "Parsing beforeWrite command", beforeWrite)
let cmd = await prepareBeforeWriteCmd({ beforeWrite, tmpPath, content, rawTmpPath, rawContent })
let cmd = await prepareBeforeWriteCmd({ beforeWrite, tmpDir, content, rawTmpPath, rawContent })
const result = await new Promise<string | undefined>((resolve, reject) => {
log(config, LogLevel.debug, "Running parsed beforeWrite command:", cmd)
const proc = exec(cmd)
@@ -221,7 +233,7 @@ function wrapBeforeWrite(
log(config, LogLevel.warning, "Error running beforeWrite command, returning original content")
return undefined
} finally {
await fs.rm(tmpPath, { force: true })
await fs.rm(tmpDir, { force: true })
await fs.rm(rawTmpPath, { force: true })
}
}
@@ -229,13 +241,13 @@ function wrapBeforeWrite(
async function prepareBeforeWriteCmd({
beforeWrite,
tmpPath,
tmpDir,
content,
rawTmpPath,
rawContent,
}: {
beforeWrite: string
tmpPath: string
tmpDir: string
content: Buffer
rawTmpPath: string
rawContent: Buffer
@@ -244,16 +256,16 @@ async function prepareBeforeWriteCmd({
const pathReg = /\{\{\s*path\s*\}\}/gi
const rawPathReg = /\{\{\s*rawpath\s*\}\}/gi
if (pathReg.test(beforeWrite)) {
await fs.writeFile(tmpPath, content)
cmd = beforeWrite.replaceAll(pathReg, tmpPath)
await fs.writeFile(tmpDir, content)
cmd = beforeWrite.replaceAll(pathReg, tmpDir)
}
if (rawPathReg.test(beforeWrite)) {
await fs.writeFile(rawTmpPath, rawContent)
cmd = beforeWrite.replaceAll(rawPathReg, rawTmpPath)
}
if (!cmd) {
await fs.writeFile(tmpPath, content)
cmd = [beforeWrite, tmpPath].join(" ")
await fs.writeFile(tmpDir, content)
cmd = [beforeWrite, tmpDir].join(" ")
}
return cmd
}

View File

@@ -119,10 +119,9 @@ export async function getTemplateFileInfo(
): Promise<OutputFileInfo> {
const inputPath = path.resolve(process.cwd(), templatePath)
const outputPathOpt = getOptionValueForFile(config, inputPath, config.output)
const outputDir = getOutputDir(config, outputPathOpt, basePath)
const outputPath = handlebarsParse(config, path.join(outputDir, path.basename(inputPath)), {
isPath: true,
}).toString()
const outputDir = getOutputDir(config, outputPathOpt, basePath.replace(config.tmpDir!, "./"))
const rawOutputPath = path.join(outputDir, path.basename(inputPath))
const outputPath = handlebarsParse(config, rawOutputPath, { asPath: true }).toString()
const exists = await pathExists(outputPath)
return { inputPath, outputPathOpt, outputDir, outputPath, exists }
}

View File

@@ -105,17 +105,17 @@ export function registerHelpers(config: ScaffoldConfig): void {
export function handlebarsParse(
config: ScaffoldConfig,
templateBuffer: Buffer | string,
{ isPath = false }: { isPath?: boolean } = {},
{ asPath = false }: { asPath?: boolean } = {},
): Buffer {
const { data } = config
try {
let str = templateBuffer.toString()
if (isPath) {
if (asPath) {
str = str.replace(/\\/g, "/")
}
const parser = Handlebars.compile(str, { noEscape: true })
let outputContents = parser(data)
if (isPath && path.sep !== "/") {
if (asPath && path.sep !== "/") {
outputContents = outputContents.replace(/\//g, "\\")
}
return Buffer.from(outputContents)

View File

@@ -118,7 +118,7 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
* @category Main
* @return {Promise<void>} 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 */
@@ -126,6 +126,7 @@ Scaffold.fromConfig = async function(
/** Any overrides to the loaded config */
overrides?: Resolver<ScaffoldCmdConfig, Partial<Omit<ScaffoldConfig, "name">>>,
): Promise<void> {
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
const _cmdConfig: ScaffoldCmdConfig = {
dryRun: false,
output: process.cwd(),
@@ -136,11 +137,11 @@ Scaffold.fromConfig = async function(
quiet: false,
config: pathOrUrl,
version: false,
tmpDir: tmpPath,
...config,
}
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
const _overrides = resolve(overrides, _cmdConfig)
const _config = await parseConfigFile(_cmdConfig, tmpPath)
const _config = await parseConfigFile(_cmdConfig)
return Scaffold({ ..._config, ..._overrides })
}

View File

@@ -165,6 +165,9 @@ export interface ScaffoldConfig {
rawContent: Buffer,
outputPath: string,
): string | Buffer | undefined | Promise<string | Buffer | undefined>
/** @internal */
tmpDir?: string
}
/**
@@ -377,6 +380,8 @@ export type ScaffoldCmdConfig = {
version: boolean
/** Run a script before writing the files. This can be a command or a path to a file. The file contents will be passed to the given command. */
beforeWrite?: string
/** @internal */
tmpDir?: string
}
/**
@@ -418,7 +423,7 @@ export type LogConfig = Pick<ScaffoldConfig, "logLevel">
export type ConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config">
/** @internal */
export type RemoteConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config" | "git"> & { tmpPath: string }
export type RemoteConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config" | "git" | "tmpDir">
/** @internal */
export type MinimalConfig = Pick<ScaffoldCmdConfig, "name" | "key">

View File

@@ -68,38 +68,34 @@ describe("config", () => {
describe("parseConfigFile", () => {
test("normal config does not change", async () => {
const tmpDir = `/tmp/scaffold-config-${Date.now()}`
const { quiet, tmpDir: _tmpDir, version, ...conf } = blankCliConf
expect(
await parseConfigFile(
{
...blankCliConf,
name: "-",
},
`/tmp/scaffold-config-${Date.now()}`,
),
).toEqual({ ...blankCliConf, name: "-" })
await parseConfigFile({
...blankCliConf,
name: "-",
tmpDir,
}),
).toEqual({ ...conf, name: "-", tmpDir, subdirHelper: undefined, beforeWrite: undefined })
})
describe("appendData", () => {
test("appends", async () => {
const result = await parseConfigFile(
{
...blankCliConf,
name: "-",
appendData: { key: "value" },
},
`/tmp/scaffold-config-${Date.now()}`,
)
const result = await parseConfigFile({
...blankCliConf,
name: "-",
appendData: { key: "value" },
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result?.data?.key).toEqual("value")
})
test("overwrites existing value", async () => {
const result = await parseConfigFile(
{
...blankCliConf,
name: "-",
data: { num: "123" },
appendData: { num: "1234" },
},
`/tmp/scaffold-config-${Date.now()}`,
)
const result = await parseConfigFile({
...blankCliConf,
name: "-",
data: { num: "123" },
appendData: { num: "1234" },
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result?.data?.num).toEqual("1234")
})
})
@@ -110,7 +106,7 @@ describe("config", () => {
const resultFn = await config.getRemoteConfig({
git: "https://github.com/chenasraf/simple-scaffold.git",
logLevel: LogLevel.none,
tmpPath: `/tmp/scaffold-config-${Date.now()}`,
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
const result = await resolve(resultFn, blankCliConf)
expect(result).toEqual(blankCliConf)

View File

@@ -23,7 +23,7 @@ describe("parser", () => {
Object.defineProperty(path, "sep", { value: origSep })
})
test("should work for windows paths", async () => {
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { isPath: true }).toString()).toEqual(
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { asPath: true }).toString()).toEqual(
"C:\\exports\\test.txt",
)
})
@@ -37,7 +37,7 @@ describe("parser", () => {
Object.defineProperty(path, "sep", { value: origSep })
})
test("should work for non-windows paths", async () => {
expect(handlebarsParse(blankConf, "/home/test/{{name}}.txt", { isPath: true })).toEqual(
expect(handlebarsParse(blankConf, "/home/test/{{name}}.txt", { asPath: true })).toEqual(
Buffer.from("/home/test/test.txt"),
)
})
@@ -48,7 +48,7 @@ describe("parser", () => {
{ ...blankConf, data: { ...blankConf.data, escaped: "value" } },
"/home/test/{{name}} \\{{escaped}}.txt",
{
isPath: false,
asPath: false,
},
),
).toEqual(Buffer.from("/home/test/test {{escaped}}.txt"))