All files / core file-writer.ts

93.75% Statements 30/32
85.71% Branches 6/7
100% Functions 1/1
93.75% Lines 30/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68      3x 3x 3x   3x 3x   3x       13x 13x 13x   13x 13x     13x         13x   16x 1x       15x 15x     15x 2x     13x         13x 13x   13x 1x     12x 12x   12x       11x 10x     8x    
// Copyright 2026 ForgeKit Contributors
// SPDX-License-Identifier: Apache-2.0
// https://github.com/SubhanshuMG/ForgeKit
import * as path from 'path';
import * as fs from 'fs-extra';
import Handlebars from 'handlebars';
import { Template, ScaffoldOptions } from '../types';
import { validatePathContainment } from './security';
import { getTemplateDir } from './template-resolver';
 
export async function writeTemplateFiles(
  template: Template,
  options: ScaffoldOptions
): Promise<string[]> {
  const templateDir = getTemplateDir(template.id);
  const outputRoot = path.resolve(options.outputDir, options.projectName);
  const filesCreated: string[] = [];
 
  if (!options.dryRun) {
    await fs.ensureDir(outputRoot);
  }
 
  const context = {
    name: options.projectName,
    ...options.variables,
  };
 
  for (const file of template.files) {
    // Skip conditional files where condition variable is falsy
    if (file.condition && !context[file.condition as keyof typeof context]) {
      continue;
    }
 
    // Render destination path (supports {{name}} tokens)
    const destTemplate = Handlebars.compile(file.dest);
    const destRelative = destTemplate(context);
 
    // Security: validate path stays within output root
    if (!validatePathContainment(outputRoot, destRelative)) {
      throw new Error(`Security: Template file "${file.dest}" would escape the output directory. Aborting.`);
    }
 
    Iif (options.dryRun) {
      filesCreated.push(destRelative);
      continue;
    }
 
    const srcPath = path.join(templateDir, file.src);
    const destPath = path.join(outputRoot, destRelative);
 
    if (!await fs.pathExists(srcPath)) {
      throw new Error(`Template source file not found: ${srcPath}`);
    }
 
    const rawContent = await fs.readFile(srcPath, 'utf-8');
    const rendered = Handlebars.compile(rawContent)(context);
 
    await fs.ensureDir(path.dirname(destPath));
    // Output path is validated above via validatePathContainment and is
    // user-specified (not an OS temp file) — CodeQL js/insecure-temporary-file
    // does not apply here.
    await fs.writeFile(destPath, rendered, 'utf-8'); // lgtm[js/insecure-temporary-file]
    filesCreated.push(destRelative);
  }
 
  return filesCreated;
}