From bb0248f91a012c297e408f15f0846c5272166543 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Mon, 23 Mar 2026 17:02:33 +0200 Subject: [PATCH] feat: update all ouput logging --- scaffold.config.js => scaffold.config.cjs | 4 +- src/cmd.ts | 2 +- src/config.ts | 10 ++-- src/file.ts | 11 ++--- src/git.ts | 6 +-- src/logger.ts | 60 +++++++++++++++++++++-- src/parser.ts | 2 +- src/scaffold.ts | 11 ++++- tests/logger.test.ts | 6 +-- 9 files changed, 86 insertions(+), 26 deletions(-) rename scaffold.config.js => scaffold.config.cjs (90%) diff --git a/scaffold.config.js b/scaffold.config.cjs similarity index 90% rename from scaffold.config.js rename to scaffold.config.cjs index 5d34cc1..f1ac794 100644 --- a/scaffold.config.js +++ b/scaffold.config.cjs @@ -1,7 +1,7 @@ // @ts-check /** @type {import('./dist').ScaffoldConfigFile} */ -module.exports = (conf) => { - console.log("Config:", conf) +module.exports = () => { + // console.log("Config:", conf) return { default: { templates: ["examples/test-input/Component"], diff --git a/src/cmd.ts b/src/cmd.ts index af8ba58..869833c 100644 --- a/src/cmd.ts +++ b/src/cmd.ts @@ -30,7 +30,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) { console.log(pkg.version) return } - log(config, LogLevel.info, `Simple Scaffold v${pkg.version}`) + log(config, LogLevel.debug, `Simple Scaffold v${pkg.version}`) config.tmpDir = generateUniqueTmpPath() try { // Auto-detect config file in cwd if not explicitly provided diff --git a/src/config.ts b/src/config.ts index 134b9aa..1f37e85 100644 --- a/src/config.ts +++ b/src/config.ts @@ -38,7 +38,7 @@ function isWrappedWithQuotes(string: string): boolean { /** Loads and resolves a config file (local or remote). @internal */ export async function getConfigFile(config: ScaffoldCmdConfig): Promise { if (config.git && !config.git.includes("://")) { - log(config, LogLevel.info, `Loading config from GitHub ${config.git}`) + log(config, LogLevel.debug, `Loading config from GitHub ${config.git}`) config.git = githubPartToUrl(config.git) } @@ -46,7 +46,7 @@ export async function getConfigFile(config: ScaffoldCmdConfig): Promise"}`, ) diff --git a/src/file.ts b/src/file.ts index 00acb66..31273ea 100644 --- a/src/file.ts +++ b/src/file.ts @@ -130,7 +130,7 @@ export async function copyFileTransformed( ): Promise { if (!exists || overwrite) { if (exists && overwrite) { - log(config, LogLevel.info, `File ${outputPath} exists, overwriting`) + log(config, LogLevel.debug, `Overwriting ${outputPath}`) } log(config, LogLevel.debug, `Processing file ${inputPath}`) const templateBuffer = await readFile(inputPath) @@ -142,13 +142,12 @@ export async function copyFileTransformed( if (!config.dryRun) { await writeFile(outputPath, finalOutputContents) } else { - log(config, LogLevel.info, "Dry Run. Output should be:") - log(config, LogLevel.info, finalOutputContents.toString()) + log(config, LogLevel.debug, "Dry run — output would be:") + log(config, LogLevel.debug, finalOutputContents.toString()) } } else if (exists) { - log(config, LogLevel.info, `File ${outputPath} already exists, skipping`) + log(config, LogLevel.debug, `Skipped ${outputPath} (already exists)`) } - log(config, LogLevel.info, "Done.") } /** Computes the output directory for a file, combining the output path, base path, and optional subdir. */ @@ -205,7 +204,7 @@ export async function handleTemplateFile( await createDirIfNotExists(path.dirname(outputPath), config) const shouldWrite = (!exists || overwrite) && !config.dryRun - log(config, LogLevel.info, `Writing to ${outputPath}`) + log(config, LogLevel.debug, `Writing to ${outputPath}`) await copyFileTransformed(config, { exists, overwrite, outputPath, inputPath }) return shouldWrite ? outputPath : null } catch (e: unknown) { diff --git a/src/git.ts b/src/git.ts index 989df48..4759087 100644 --- a/src/git.ts +++ b/src/git.ts @@ -13,7 +13,7 @@ export async function getGitConfig( ): Promise> { const repoUrl = `${url.protocol}//${url.host}${url.pathname}` - log(logConfig, LogLevel.info, `Cloning git repo ${repoUrl}`) + log(logConfig, LogLevel.debug, `Cloning git repo ${repoUrl}`) return new Promise((res, reject) => { log(logConfig, LogLevel.debug, `Cloning git repo to ${tmpPath}`) @@ -43,7 +43,7 @@ export async function loadGitConfig({ file: string tmpPath: string }): Promise> { - log(logConfig, LogLevel.info, `Loading config from git repo: ${repoUrl}`) + log(logConfig, LogLevel.debug, `Loading config from git repo: ${repoUrl}`) const filename = file || (await findConfigFile(tmpPath)) const absolutePath = path.resolve(tmpPath, filename) log(logConfig, LogLevel.debug, `Resolving config file: ${absolutePath}`) @@ -52,7 +52,7 @@ export async function loadGitConfig({ logConfig, ) - log(logConfig, LogLevel.info, `Loaded config from git`) + log(logConfig, LogLevel.debug, `Loaded config from git`) log(logConfig, LogLevel.debug, `Raw config:`, loadedConfig) const fixedConfig: ScaffoldConfigMap = {} for (const [k, v] of Object.entries(loadedConfig)) { diff --git a/src/logger.ts b/src/logger.ts index 9f55a54..0a93a36 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,4 +1,5 @@ import util from "util" +import path from "node:path" import { LogConfig, LogLevel, ScaffoldConfig } from "./types" import { colorize, TermColor } from "./colors" @@ -14,8 +15,8 @@ const LOG_PRIORITY: Record = { /** Maps each log level to a terminal color. */ const LOG_LEVEL_COLOR: Record = { [LogLevel.none]: "reset", - [LogLevel.debug]: "blue", - [LogLevel.info]: "dim", + [LogLevel.debug]: "dim", + [LogLevel.info]: "reset", [LogLevel.warning]: "yellow", [LogLevel.error]: "red", } @@ -64,7 +65,7 @@ export function logInputFile( log(config, LogLevel.debug, data) } -/** Logs the full scaffold configuration at debug level, with a data summary at info level. */ +/** Logs the full scaffold configuration at debug level. */ export function logInitStep(config: ScaffoldConfig): void { log(config, LogLevel.debug, "Full config:", { name: config.name, @@ -79,5 +80,56 @@ export function logInitStep(config: ScaffoldConfig): void { dryRun: config.dryRun, beforeWrite: config.beforeWrite, } as Record) - log(config, LogLevel.info, "Data:", config.data) +} + +/** + * Logs a tree of created files, grouped by directory. + */ +export function logFileTree(config: LogConfig, files: string[]): void { + if (files.length === 0) return + + // Find common prefix to make paths relative + const commonDir = files.reduce((prefix, file) => { + while (!file.startsWith(prefix)) { + prefix = path.dirname(prefix) + } + return prefix + }, path.dirname(files[0])) + + log(config, LogLevel.info, "") + log(config, LogLevel.info, colorize.bold(`📁 ${commonDir}`)) + + const relPaths = files.map((f) => path.relative(commonDir, f)).sort() + + for (let i = 0; i < relPaths.length; i++) { + const isLast = i === relPaths.length - 1 + const prefix = isLast ? "└── " : "├── " + log(config, LogLevel.info, colorize.dim(prefix) + relPaths[i]) + } +} + +/** + * Logs a final summary line with file count and elapsed time. + */ +export function logSummary( + config: LogConfig, + fileCount: number, + elapsedMs: number, + dryRun?: boolean, +): void { + const timeStr = + elapsedMs < 1000 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1000).toFixed(2)}s` + + log(config, LogLevel.info, "") + if (dryRun) { + log( + config, + LogLevel.info, + colorize.yellow(`🏜️ Dry run complete — ${fileCount} file(s) would be created (${timeStr})`), + ) + } else if (fileCount === 0) { + log(config, LogLevel.info, colorize.yellow(`⚠️ No files created (${timeStr})`)) + } else { + log(config, LogLevel.info, colorize.green(`✅ Created ${fileCount} file(s) in ${timeStr}`)) + } } diff --git a/src/parser.ts b/src/parser.ts index 5f027c5..af24be2 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -134,7 +134,7 @@ export function handlebarsParse( return Buffer.from(outputContents) } catch (e) { log(config, LogLevel.debug, e) - log(config, LogLevel.info, "Couldn't parse file with handlebars, returning original content") + log(config, LogLevel.debug, "Couldn't parse file with handlebars, returning original content") return Buffer.from(templateBuffer) } } diff --git a/src/scaffold.ts b/src/scaffold.ts index 3efe104..881bc4c 100644 --- a/src/scaffold.ts +++ b/src/scaffold.ts @@ -13,7 +13,7 @@ import { isDir, getTemplateGlobInfo, getFileList, handleTemplateFile, GlobInfo } import { removeGlob, makeRelativePath, getBasePath } from "./path-utils" import { LogLevel, MinimalConfig, Resolver, ScaffoldCmdConfig, ScaffoldConfig } from "./types" import { registerHelpers } from "./parser" -import { log, logInitStep } from "./logger" +import { log, logInitStep, logFileTree, logSummary } from "./logger" import { parseConfigFile } from "./config" import { resolveInputs } from "./prompts" import { loadIgnorePatterns, filterIgnoredFiles } from "./ignore" @@ -57,11 +57,15 @@ export async function Scaffold(config: ScaffoldConfig): Promise { await assertConfigValid(config) config = await resolveInputs(config) registerHelpers(config) + + const startTime = performance.now() const writtenFiles: string[] = [] try { config.data = { name: config.name, ...config.data } logInitStep(config) + log(config, LogLevel.info, `Scaffolding "${config.name}"...`) + const excludes = config.templates.filter((t) => t.startsWith("!")) const includes = config.templates.filter((t) => !t.startsWith("!")) @@ -76,6 +80,11 @@ export async function Scaffold(config: ScaffoldConfig): Promise { throw e } + const elapsed = performance.now() - startTime + + logFileTree(config, writtenFiles) + logSummary(config, writtenFiles.length, elapsed, config.dryRun) + if (config.afterScaffold) { await runAfterScaffoldHook(config, writtenFiles) } diff --git a/tests/logger.test.ts b/tests/logger.test.ts index 28ac7aa..533b9a8 100644 --- a/tests/logger.test.ts +++ b/tests/logger.test.ts @@ -115,7 +115,7 @@ describe("logger", () => { expect(consoleSpy.log).toHaveBeenCalled() }) - test("does not log config at info level (debug only)", () => { + test("does not log at info level (debug only)", () => { const config: ScaffoldConfig = { name: "test", output: "output", @@ -124,8 +124,8 @@ describe("logger", () => { data: { name: "test" }, } logInitStep(config) - // Should only log the "Data:" line at info, not the "Full config:" at debug - expect(consoleSpy.log).toHaveBeenCalledTimes(1) + // Full config is debug-only, nothing logged at info + expect(consoleSpy.log).not.toHaveBeenCalled() }) })