render.mjs 1.52 KB
// lib/render.mjs — literal-safe template render (replaces scope-lock/render.sh)
//
// 用法(CLI):node lib/render.mjs <tplPath> <varsJsonPath> <outPath>
// 程序内:import { render } from './render.mjs'
//
// 核心要求:{{key}} 占位替换;值中含 $、{、}、}} 不被二次解释(字面插入);
// 先剥离 HTML 注释(模板引导文本);缺少变量则 throw(不静默留空)。
export function render(template, vars) {
  const withoutComments = template.replace(/<!--[\s\S]*?-->/g, '')
  return withoutComments.replace(/\{\{(\w+)\}\}/g, (_, key) => {
    // 用 Object.hasOwn 而非 `key in vars`:避免 {{constructor}} / {{toString}} 等
    // 沿原型链命中继承属性、静默渲染出垃圾(应按"缺变量"抛错)。
    if (!Object.hasOwn(vars, key)) throw new Error(`render: missing var "${key}"`)
    return String(vars[key]) // 字面插入,不二次解释 $ 或 {}
  })
}

// 入口判定用 pathToFileURL 规范化 process.argv[1],使其与 import.meta.url 编码一致
// (路径含空格/非 ASCII/Windows 反斜杠时,字面 `file://${argv[1]}` 比较会失配)。
const { pathToFileURL } = await import('node:url')
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  const { readFileSync, writeFileSync } = await import('node:fs')
  const [tplPath, jsonPath, outPath] = process.argv.slice(2)
  const tpl = readFileSync(tplPath, 'utf8')
  const vars = JSON.parse(readFileSync(jsonPath, 'utf8'))
  writeFileSync(outPath, render(tpl, vars))
}