reifinator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +431 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +334 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { join, resolve, dirname } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import yaml from "js-yaml";
|
|
11
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
var DEFAULT_CONFIG_FILENAME = "reifinator.yaml";
|
|
13
|
+
function loadConfig(configPath) {
|
|
14
|
+
const path = configPath ?? join(process.cwd(), DEFAULT_CONFIG_FILENAME);
|
|
15
|
+
if (!existsSync(path)) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
const content = readFileSync(path, "utf-8");
|
|
19
|
+
return yaml.load(content) ?? {};
|
|
20
|
+
}
|
|
21
|
+
async function loadAdapterClass(adapterPath) {
|
|
22
|
+
if (!adapterPath.includes(":")) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Invalid adapter path '${adapterPath}'. Expected format: 'module.path:ClassName'`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const [modulePath, className] = adapterPath.split(":", 2);
|
|
28
|
+
let mod;
|
|
29
|
+
try {
|
|
30
|
+
mod = await import(modulePath);
|
|
31
|
+
} catch {
|
|
32
|
+
const localPath = modulePath.replace(/^reifinator\//, "./");
|
|
33
|
+
const resolved = resolve(__dirname, localPath + ".js");
|
|
34
|
+
mod = await import(resolved);
|
|
35
|
+
}
|
|
36
|
+
const cls = mod[className];
|
|
37
|
+
if (!cls) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Class '${className}' not found in module '${modulePath}'`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return cls;
|
|
43
|
+
}
|
|
44
|
+
async function loadContentGenerators(config, templateDir) {
|
|
45
|
+
const entries = config.content_generators ?? [];
|
|
46
|
+
const generators = [];
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (!entry.adapter) continue;
|
|
49
|
+
const AdapterCls = await loadAdapterClass(entry.adapter);
|
|
50
|
+
const options = {};
|
|
51
|
+
if (templateDir !== void 0) {
|
|
52
|
+
options.templateDir = templateDir;
|
|
53
|
+
}
|
|
54
|
+
if (entry.extension !== void 0) {
|
|
55
|
+
options.extension = entry.extension;
|
|
56
|
+
}
|
|
57
|
+
generators.push(new AdapterCls(options));
|
|
58
|
+
}
|
|
59
|
+
return generators;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/generator.ts
|
|
63
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
64
|
+
import { join as join3 } from "path";
|
|
65
|
+
|
|
66
|
+
// src/content.ts
|
|
67
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
68
|
+
var BaseContentGenerator = class {
|
|
69
|
+
};
|
|
70
|
+
var BuiltinInterpolator = class _BuiltinInterpolator extends BaseContentGenerator {
|
|
71
|
+
extension = ".tpl";
|
|
72
|
+
static PATTERN = /\{\{([\w.]+)\}\}/g;
|
|
73
|
+
generate(templatePath, context) {
|
|
74
|
+
const templateText = readFileSync2(templatePath, "utf-8");
|
|
75
|
+
const result = templateText.replace(
|
|
76
|
+
_BuiltinInterpolator.PATTERN,
|
|
77
|
+
(_match, expr) => this.resolve(expr, context)
|
|
78
|
+
);
|
|
79
|
+
return { content: result };
|
|
80
|
+
}
|
|
81
|
+
resolve(expression, context) {
|
|
82
|
+
const parts = expression.split(".");
|
|
83
|
+
let value = context;
|
|
84
|
+
for (const part of parts) {
|
|
85
|
+
if (typeof value === "object" && value !== null && part in value) {
|
|
86
|
+
value = value[part];
|
|
87
|
+
} else {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Cannot resolve expression '${expression}': '${part}' not found`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return String(value);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// src/context.ts
|
|
98
|
+
import { existsSync as existsSync2 } from "fs";
|
|
99
|
+
import { pathToFileURL } from "url";
|
|
100
|
+
var CONTEXT_SCRIPT_NAMES = ["_gen_context.js", "_gen_context.mjs"];
|
|
101
|
+
var OTHER_CONTEXT_SCRIPTS = ["_gen_context.py"];
|
|
102
|
+
async function loadContextScript(dirPath, scriptNames, parentContext) {
|
|
103
|
+
for (const name of scriptNames) {
|
|
104
|
+
const scriptPath = `${dirPath}/${name}`;
|
|
105
|
+
if (!existsSync2(scriptPath)) continue;
|
|
106
|
+
const fileUrl = pathToFileURL(scriptPath).href;
|
|
107
|
+
const mod = await import(fileUrl);
|
|
108
|
+
const fn = mod.getContexts ?? mod.get_contexts;
|
|
109
|
+
if (typeof fn === "function") {
|
|
110
|
+
return fn(parentContext);
|
|
111
|
+
}
|
|
112
|
+
return [parentContext];
|
|
113
|
+
}
|
|
114
|
+
return [parentContext];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/output.ts
|
|
118
|
+
import { copyFileSync, mkdirSync, writeFileSync } from "fs";
|
|
119
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
120
|
+
var OutputDirectory = class _OutputDirectory {
|
|
121
|
+
constructor(path) {
|
|
122
|
+
this.path = path;
|
|
123
|
+
}
|
|
124
|
+
createDir(name) {
|
|
125
|
+
const dirPath = join2(this.path, name);
|
|
126
|
+
mkdirSync(dirPath, { recursive: true });
|
|
127
|
+
return new _OutputDirectory(dirPath);
|
|
128
|
+
}
|
|
129
|
+
writeFile(name, content) {
|
|
130
|
+
const filePath = join2(this.path, name);
|
|
131
|
+
mkdirSync(dirname2(filePath), { recursive: true });
|
|
132
|
+
writeFileSync(filePath, content.content, {
|
|
133
|
+
encoding: content.encoding ?? "utf-8"
|
|
134
|
+
});
|
|
135
|
+
return filePath;
|
|
136
|
+
}
|
|
137
|
+
copyFile(name, source) {
|
|
138
|
+
const filePath = join2(this.path, name);
|
|
139
|
+
mkdirSync(dirname2(filePath), { recursive: true });
|
|
140
|
+
copyFileSync(source, filePath);
|
|
141
|
+
return filePath;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/resolution.ts
|
|
146
|
+
var BracketResolver = class _BracketResolver {
|
|
147
|
+
static PATTERN = /\[([\w.]+)\]/g;
|
|
148
|
+
resolve(filename, context) {
|
|
149
|
+
let resolutionFailed = false;
|
|
150
|
+
const expressionsFound = [];
|
|
151
|
+
const result = filename.replace(
|
|
152
|
+
_BracketResolver.PATTERN,
|
|
153
|
+
(match, expr) => {
|
|
154
|
+
expressionsFound.push(expr);
|
|
155
|
+
const parts = expr.split(".");
|
|
156
|
+
let value = context;
|
|
157
|
+
try {
|
|
158
|
+
for (const part of parts) {
|
|
159
|
+
if (value === null || value === void 0) {
|
|
160
|
+
resolutionFailed = true;
|
|
161
|
+
return match;
|
|
162
|
+
}
|
|
163
|
+
if (typeof value === "object" && part in value) {
|
|
164
|
+
value = value[part];
|
|
165
|
+
} else {
|
|
166
|
+
resolutionFailed = true;
|
|
167
|
+
return match;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return String(value);
|
|
171
|
+
} catch {
|
|
172
|
+
resolutionFailed = true;
|
|
173
|
+
return match;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
const resolved = resolutionFailed ? null : result;
|
|
178
|
+
return {
|
|
179
|
+
original: filename,
|
|
180
|
+
resolved,
|
|
181
|
+
expressions: expressionsFound,
|
|
182
|
+
success: !resolutionFailed,
|
|
183
|
+
wasSubstituted: !resolutionFailed && result !== filename
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var GenerationTracker = class {
|
|
188
|
+
expressionItems = /* @__PURE__ */ new Map();
|
|
189
|
+
resolvedItems = /* @__PURE__ */ new Set();
|
|
190
|
+
registerExpressionItem(itemPath, expressions) {
|
|
191
|
+
if (!this.expressionItems.has(itemPath)) {
|
|
192
|
+
this.expressionItems.set(itemPath, expressions);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
markResolved(itemPath) {
|
|
196
|
+
this.resolvedItems.add(itemPath);
|
|
197
|
+
}
|
|
198
|
+
getUnresolvedItems() {
|
|
199
|
+
const unresolved = [];
|
|
200
|
+
for (const [path, exprs] of this.expressionItems) {
|
|
201
|
+
if (!this.resolvedItems.has(path)) {
|
|
202
|
+
unresolved.push([path, exprs]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return unresolved;
|
|
206
|
+
}
|
|
207
|
+
raiseIfUnresolved() {
|
|
208
|
+
const unresolved = this.getUnresolvedItems();
|
|
209
|
+
if (unresolved.length > 0) {
|
|
210
|
+
throw new UnresolvedExpressionsError(unresolved);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
var UnresolvedExpressionsError = class extends Error {
|
|
215
|
+
unresolvedItems;
|
|
216
|
+
constructor(unresolvedItems) {
|
|
217
|
+
const itemsToShow = unresolvedItems.slice(0, 5);
|
|
218
|
+
const lines = [
|
|
219
|
+
"The following template items have expressions that could not be resolved:"
|
|
220
|
+
];
|
|
221
|
+
for (const [itemPath, expressions] of itemsToShow) {
|
|
222
|
+
const exprStr = expressions.map((e) => `[${e}]`).join(", ");
|
|
223
|
+
lines.push(` - ${itemPath}: ${exprStr}`);
|
|
224
|
+
}
|
|
225
|
+
if (unresolvedItems.length > 5) {
|
|
226
|
+
lines.push(` ... and ${unresolvedItems.length - 5} more`);
|
|
227
|
+
}
|
|
228
|
+
super(lines.join("\n"));
|
|
229
|
+
this.name = "UnresolvedExpressionsError";
|
|
230
|
+
this.unresolvedItems = unresolvedItems;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// src/generator.ts
|
|
235
|
+
var SKIP_NAMES = /* @__PURE__ */ new Set([
|
|
236
|
+
"__pycache__",
|
|
237
|
+
"node_modules",
|
|
238
|
+
".DS_Store"
|
|
239
|
+
]);
|
|
240
|
+
var Generator = class {
|
|
241
|
+
templateDir;
|
|
242
|
+
outputWriteDir;
|
|
243
|
+
outputDestDir;
|
|
244
|
+
contentGenerators;
|
|
245
|
+
placeholderResolver;
|
|
246
|
+
contextScriptNames;
|
|
247
|
+
debug;
|
|
248
|
+
constructor(options) {
|
|
249
|
+
this.templateDir = options.templateDir;
|
|
250
|
+
this.placeholderResolver = options.placeholderResolver ?? new BracketResolver();
|
|
251
|
+
this.contextScriptNames = options.contextScriptNames ?? CONTEXT_SCRIPT_NAMES;
|
|
252
|
+
this.debug = options.debug ?? false;
|
|
253
|
+
this.contentGenerators = /* @__PURE__ */ new Map();
|
|
254
|
+
const builtin = new BuiltinInterpolator();
|
|
255
|
+
this.contentGenerators.set(builtin.extension, builtin);
|
|
256
|
+
for (const gen of options.contentGenerators ?? []) {
|
|
257
|
+
this.contentGenerators.set(gen.extension, gen);
|
|
258
|
+
}
|
|
259
|
+
const [writeDir, destDir] = this.resolveOutputDirs(
|
|
260
|
+
options.outputDir,
|
|
261
|
+
options.outputStageDir,
|
|
262
|
+
options.outputDestDir
|
|
263
|
+
);
|
|
264
|
+
this.outputWriteDir = writeDir;
|
|
265
|
+
this.outputDestDir = destDir;
|
|
266
|
+
}
|
|
267
|
+
resolveOutputDirs(outputDir, outputStageDir, outputDestDir) {
|
|
268
|
+
const hasSingle = outputDir !== void 0;
|
|
269
|
+
const hasStage = outputStageDir !== void 0;
|
|
270
|
+
const hasDest = outputDestDir !== void 0;
|
|
271
|
+
if (hasSingle && (hasStage || hasDest)) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
"Cannot specify both outputDir and outputStageDir/outputDestDir. Use outputDir for 1-stage or outputStageDir + outputDestDir for 2-stage."
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (hasStage !== hasDest) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
"outputStageDir and outputDestDir must both be specified for 2-stage output."
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (!hasSingle && !hasStage) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
"No output directory configured. Specify outputDir (1-stage) or outputStageDir + outputDestDir (2-stage)."
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (hasSingle) {
|
|
287
|
+
return [outputDir, null];
|
|
288
|
+
}
|
|
289
|
+
return [outputStageDir, outputDestDir];
|
|
290
|
+
}
|
|
291
|
+
async run(context) {
|
|
292
|
+
if (!existsSync3(this.templateDir) || !statSync(this.templateDir).isDirectory()) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Template directory does not exist: ${this.templateDir}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const rootContext = context ?? {};
|
|
298
|
+
const tracker = new GenerationTracker();
|
|
299
|
+
const outputRoot = new OutputDirectory(this.outputWriteDir);
|
|
300
|
+
mkdirSync2(this.outputWriteDir, { recursive: true });
|
|
301
|
+
await this.processDirectory(
|
|
302
|
+
this.templateDir,
|
|
303
|
+
outputRoot,
|
|
304
|
+
rootContext,
|
|
305
|
+
tracker
|
|
306
|
+
);
|
|
307
|
+
tracker.raiseIfUnresolved();
|
|
308
|
+
}
|
|
309
|
+
async processDirectory(inputPath, outputDir, parentCtx, tracker) {
|
|
310
|
+
const contexts = await loadContextScript(
|
|
311
|
+
inputPath,
|
|
312
|
+
this.contextScriptNames,
|
|
313
|
+
parentCtx
|
|
314
|
+
);
|
|
315
|
+
for (let idx = 0; idx < contexts.length; idx++) {
|
|
316
|
+
const ctx = contexts[idx];
|
|
317
|
+
const mergedCtx = { ...parentCtx, ...ctx };
|
|
318
|
+
if (this.debug) {
|
|
319
|
+
this.writeContextLog(outputDir.path, idx, mergedCtx);
|
|
320
|
+
}
|
|
321
|
+
const items = readdirSync(inputPath);
|
|
322
|
+
for (const itemName of items) {
|
|
323
|
+
if (this.contextScriptNames.includes(itemName) || OTHER_CONTEXT_SCRIPTS.includes(itemName) || SKIP_NAMES.has(itemName)) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const itemPath = join3(inputPath, itemName);
|
|
327
|
+
const resolution = this.placeholderResolver.resolve(
|
|
328
|
+
itemName,
|
|
329
|
+
mergedCtx
|
|
330
|
+
);
|
|
331
|
+
if (resolution.expressions.length > 0) {
|
|
332
|
+
tracker.registerExpressionItem(itemPath, resolution.expressions);
|
|
333
|
+
if (resolution.success) {
|
|
334
|
+
tracker.markResolved(itemPath);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (!resolution.success) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const outputName = resolution.resolved;
|
|
341
|
+
const stat = statSync(itemPath);
|
|
342
|
+
if (stat.isDirectory()) {
|
|
343
|
+
const newOutputDir = outputDir.createDir(outputName);
|
|
344
|
+
await this.processDirectory(
|
|
345
|
+
itemPath,
|
|
346
|
+
newOutputDir,
|
|
347
|
+
mergedCtx,
|
|
348
|
+
tracker
|
|
349
|
+
);
|
|
350
|
+
} else if (this.isTemplateFile(itemName)) {
|
|
351
|
+
if (!resolution.wasSubstituted && idx > 0) continue;
|
|
352
|
+
const ext = this.getTemplateExtension(itemName);
|
|
353
|
+
const gen = this.contentGenerators.get(ext);
|
|
354
|
+
const content = gen.generate(itemPath, mergedCtx);
|
|
355
|
+
const finalName = outputName.slice(
|
|
356
|
+
0,
|
|
357
|
+
outputName.length - ext.length
|
|
358
|
+
);
|
|
359
|
+
outputDir.writeFile(finalName, content);
|
|
360
|
+
} else {
|
|
361
|
+
if (!resolution.wasSubstituted && idx > 0) continue;
|
|
362
|
+
outputDir.copyFile(outputName, itemPath);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
isTemplateFile(name) {
|
|
368
|
+
for (const ext of this.contentGenerators.keys()) {
|
|
369
|
+
if (name.endsWith(ext)) return true;
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
getTemplateExtension(name) {
|
|
374
|
+
for (const ext of this.contentGenerators.keys()) {
|
|
375
|
+
if (name.endsWith(ext)) return ext;
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
writeContextLog(outputPath, idx, context) {
|
|
380
|
+
const logContent = `# Merged context for iteration ${idx}
|
|
381
|
+
${JSON.stringify(context, null, 2)}`;
|
|
382
|
+
const logPath = join3(outputPath, `.gen_context_${idx}.log`);
|
|
383
|
+
writeFileSync2(logPath, logContent, "utf-8");
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// src/cli.ts
|
|
388
|
+
var program = new Command();
|
|
389
|
+
program.name("reify").description(
|
|
390
|
+
"Reifinator \u2014 template-structure-driven code and directory generator"
|
|
391
|
+
);
|
|
392
|
+
program.command("generate").description("Run code generation").option("--template-dir <path>", "Template directory (overrides config)").option(
|
|
393
|
+
"--output-dir <path>",
|
|
394
|
+
"Output directory for 1-stage output (overrides config)"
|
|
395
|
+
).option(
|
|
396
|
+
"--output-stage-dir <path>",
|
|
397
|
+
"Staging directory for 2-stage output"
|
|
398
|
+
).option(
|
|
399
|
+
"--output-dest-dir <path>",
|
|
400
|
+
"Destination directory for 2-stage output"
|
|
401
|
+
).option("--config <path>", "Config file path (default: ./reifinator.yaml)").option("--dry-run", "Show what would be generated without writing").option("--debug", "Write generation log files alongside output").action(async (opts) => {
|
|
402
|
+
const config = loadConfig(opts.config);
|
|
403
|
+
const templateDir = opts.templateDir ?? config.template_dir;
|
|
404
|
+
const outputDir = opts.outputDir ?? config.output_dir;
|
|
405
|
+
const outputStageDir = opts.outputStageDir ?? config.output_stage_dir;
|
|
406
|
+
const outputDestDir = opts.outputDestDir ?? config.output_dest_dir;
|
|
407
|
+
const debug = opts.debug ?? config.debug ?? false;
|
|
408
|
+
if (!templateDir) {
|
|
409
|
+
console.error(
|
|
410
|
+
"Error: template_dir is required (via --template-dir or config file)."
|
|
411
|
+
);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
const contentGenerators = await loadContentGenerators(config, templateDir);
|
|
415
|
+
if (opts.dryRun) {
|
|
416
|
+
console.log("Dry run mode \u2014 generation not yet implemented for dry run.");
|
|
417
|
+
process.exit(0);
|
|
418
|
+
}
|
|
419
|
+
const generator = new Generator({
|
|
420
|
+
templateDir,
|
|
421
|
+
outputDir,
|
|
422
|
+
outputStageDir,
|
|
423
|
+
outputDestDir,
|
|
424
|
+
contentGenerators,
|
|
425
|
+
debug
|
|
426
|
+
});
|
|
427
|
+
await generator.run();
|
|
428
|
+
console.log("Generation complete.");
|
|
429
|
+
});
|
|
430
|
+
program.parse();
|
|
431
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/config.ts","../src/generator.ts","../src/content.ts","../src/context.ts","../src/output.ts","../src/resolution.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * CLI entry point — the `reify` command.\n */\n\nimport { Command } from \"commander\";\nimport { loadConfig, loadContentGenerators } from \"./config.js\";\nimport { Generator } from \"./generator.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"reify\")\n .description(\n \"Reifinator — template-structure-driven code and directory generator\",\n );\n\nprogram\n .command(\"generate\")\n .description(\"Run code generation\")\n .option(\"--template-dir <path>\", \"Template directory (overrides config)\")\n .option(\n \"--output-dir <path>\",\n \"Output directory for 1-stage output (overrides config)\",\n )\n .option(\n \"--output-stage-dir <path>\",\n \"Staging directory for 2-stage output\",\n )\n .option(\n \"--output-dest-dir <path>\",\n \"Destination directory for 2-stage output\",\n )\n .option(\"--config <path>\", \"Config file path (default: ./reifinator.yaml)\")\n .option(\"--dry-run\", \"Show what would be generated without writing\")\n .option(\"--debug\", \"Write generation log files alongside output\")\n .action(async (opts) => {\n const config = loadConfig(opts.config);\n\n const templateDir =\n opts.templateDir ?? config.template_dir;\n const outputDir =\n opts.outputDir ?? config.output_dir;\n const outputStageDir =\n opts.outputStageDir ?? config.output_stage_dir;\n const outputDestDir =\n opts.outputDestDir ?? config.output_dest_dir;\n const debug = opts.debug ?? config.debug ?? false;\n\n if (!templateDir) {\n console.error(\n \"Error: template_dir is required (via --template-dir or config file).\",\n );\n process.exit(1);\n }\n\n // Load content generators from config\n const contentGenerators = await loadContentGenerators(config, templateDir);\n\n if (opts.dryRun) {\n console.log(\"Dry run mode — generation not yet implemented for dry run.\");\n process.exit(0);\n }\n\n const generator = new Generator({\n templateDir,\n outputDir,\n outputStageDir,\n outputDestDir,\n contentGenerators,\n debug,\n });\n\n await generator.run();\n console.log(\"Generation complete.\");\n });\n\nprogram.parse();\n","/**\n * Configuration loading from reifinator.yaml.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, resolve, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport yaml from \"js-yaml\";\nimport type { BaseContentGenerator } from \"./content.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport const DEFAULT_CONFIG_FILENAME = \"reifinator.yaml\";\n\nexport interface ContentGeneratorEntry {\n adapter: string;\n extension?: string;\n}\n\nexport interface ReifinatorConfig {\n template_dir?: string;\n output_dir?: string;\n output_stage_dir?: string;\n output_dest_dir?: string;\n content_generators?: ContentGeneratorEntry[];\n debug?: boolean;\n}\n\n/**\n * Load configuration from a YAML file.\n * If no path is given, looks for reifinator.yaml in the current directory.\n * Returns an empty object if the file doesn't exist.\n */\nexport function loadConfig(configPath?: string): ReifinatorConfig {\n const path = configPath ?? join(process.cwd(), DEFAULT_CONFIG_FILENAME);\n\n if (!existsSync(path)) {\n return {};\n }\n\n const content = readFileSync(path, \"utf-8\");\n return (yaml.load(content) as ReifinatorConfig) ?? {};\n}\n\n/**\n * Dynamically load an adapter class from a module:ClassName path string.\n *\n * Example: \"reifinator/adapters/eta:EtaContentGenerator\"\n */\nasync function loadAdapterClass(\n adapterPath: string,\n): Promise<new (options?: Record<string, unknown>) => BaseContentGenerator> {\n if (!adapterPath.includes(\":\")) {\n throw new Error(\n `Invalid adapter path '${adapterPath}'. Expected format: 'module.path:ClassName'`,\n );\n }\n\n const [modulePath, className] = adapterPath.split(\":\", 2);\n\n // Try loading the module: first as a package path, then relative to this package\n let mod: Record<string, unknown>;\n try {\n mod = (await import(modulePath)) as Record<string, unknown>;\n } catch {\n // If the module path starts with \"reifinator/\", resolve relative to this package's src/\n const localPath = modulePath.replace(/^reifinator\\//, \"./\");\n const resolved = resolve(__dirname, localPath + \".js\");\n mod = (await import(resolved)) as Record<string, unknown>;\n }\n\n const cls = mod[className] as new (\n options?: Record<string, unknown>,\n ) => BaseContentGenerator;\n\n if (!cls) {\n throw new Error(\n `Class '${className}' not found in module '${modulePath}'`,\n );\n }\n\n return cls;\n}\n\n/**\n * Load content generators from the config's content_generators list.\n *\n * Each entry must have an 'adapter' key. An optional 'extension' key\n * overrides the adapter's default extension.\n */\nexport async function loadContentGenerators(\n config: ReifinatorConfig,\n templateDir?: string,\n): Promise<BaseContentGenerator[]> {\n const entries = config.content_generators ?? [];\n const generators: BaseContentGenerator[] = [];\n\n for (const entry of entries) {\n if (!entry.adapter) continue;\n\n const AdapterCls = await loadAdapterClass(entry.adapter);\n const options: Record<string, unknown> = {};\n if (templateDir !== undefined) {\n options.templateDir = templateDir;\n }\n if (entry.extension !== undefined) {\n options.extension = entry.extension;\n }\n generators.push(new AdapterCls(options));\n }\n\n return generators;\n}\n","/**\n * Core generation engine.\n */\n\nimport { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport { BaseContentGenerator, BuiltinInterpolator } from \"./content.js\";\nimport { CONTEXT_SCRIPT_NAMES, OTHER_CONTEXT_SCRIPTS, loadContextScript } from \"./context.js\";\nimport type { ContextDict } from \"./context.js\";\nimport { OutputDirectory } from \"./output.js\";\nimport {\n BracketResolver,\n GenerationTracker,\n type PlaceholderResolver,\n} from \"./resolution.js\";\n\n/** Items to always skip during directory walking. */\nconst SKIP_NAMES = new Set([\n \"__pycache__\",\n \"node_modules\",\n \".DS_Store\",\n]);\n\nexport interface GeneratorOptions {\n templateDir: string;\n outputDir?: string;\n outputStageDir?: string;\n outputDestDir?: string;\n contentGenerators?: BaseContentGenerator[];\n placeholderResolver?: PlaceholderResolver;\n contextScriptNames?: string[];\n debug?: boolean;\n}\n\nexport class Generator {\n private readonly templateDir: string;\n private readonly outputWriteDir: string;\n private readonly outputDestDir: string | null;\n private readonly contentGenerators: Map<string, BaseContentGenerator>;\n private readonly placeholderResolver: PlaceholderResolver;\n private readonly contextScriptNames: string[];\n private readonly debug: boolean;\n\n constructor(options: GeneratorOptions) {\n this.templateDir = options.templateDir;\n this.placeholderResolver =\n options.placeholderResolver ?? new BracketResolver();\n this.contextScriptNames =\n options.contextScriptNames ?? CONTEXT_SCRIPT_NAMES;\n this.debug = options.debug ?? false;\n\n // Build extension -> generator mapping (built-in always included)\n this.contentGenerators = new Map<string, BaseContentGenerator>();\n const builtin = new BuiltinInterpolator();\n this.contentGenerators.set(builtin.extension, builtin);\n for (const gen of options.contentGenerators ?? []) {\n this.contentGenerators.set(gen.extension, gen);\n }\n\n // Resolve output directories\n const [writeDir, destDir] = this.resolveOutputDirs(\n options.outputDir,\n options.outputStageDir,\n options.outputDestDir,\n );\n this.outputWriteDir = writeDir;\n this.outputDestDir = destDir;\n }\n\n private resolveOutputDirs(\n outputDir?: string,\n outputStageDir?: string,\n outputDestDir?: string,\n ): [string, string | null] {\n const hasSingle = outputDir !== undefined;\n const hasStage = outputStageDir !== undefined;\n const hasDest = outputDestDir !== undefined;\n\n if (hasSingle && (hasStage || hasDest)) {\n throw new Error(\n \"Cannot specify both outputDir and outputStageDir/outputDestDir. \" +\n \"Use outputDir for 1-stage or outputStageDir + outputDestDir for 2-stage.\",\n );\n }\n if (hasStage !== hasDest) {\n throw new Error(\n \"outputStageDir and outputDestDir must both be specified for 2-stage output.\",\n );\n }\n if (!hasSingle && !hasStage) {\n throw new Error(\n \"No output directory configured. \" +\n \"Specify outputDir (1-stage) or outputStageDir + outputDestDir (2-stage).\",\n );\n }\n\n if (hasSingle) {\n return [outputDir!, null];\n }\n return [outputStageDir!, outputDestDir!];\n }\n\n async run(context?: ContextDict): Promise<void> {\n if (!existsSync(this.templateDir) || !statSync(this.templateDir).isDirectory()) {\n throw new Error(\n `Template directory does not exist: ${this.templateDir}`,\n );\n }\n\n const rootContext = context ?? {};\n const tracker = new GenerationTracker();\n const outputRoot = new OutputDirectory(this.outputWriteDir);\n mkdirSync(this.outputWriteDir, { recursive: true });\n\n await this.processDirectory(\n this.templateDir,\n outputRoot,\n rootContext,\n tracker,\n );\n tracker.raiseIfUnresolved();\n }\n\n private async processDirectory(\n inputPath: string,\n outputDir: OutputDirectory,\n parentCtx: ContextDict,\n tracker: GenerationTracker,\n ): Promise<void> {\n const contexts = await loadContextScript(\n inputPath,\n this.contextScriptNames,\n parentCtx,\n );\n\n for (let idx = 0; idx < contexts.length; idx++) {\n const ctx = contexts[idx];\n const mergedCtx = { ...parentCtx, ...ctx };\n\n if (this.debug) {\n this.writeContextLog(outputDir.path, idx, mergedCtx);\n }\n\n const items = readdirSync(inputPath);\n for (const itemName of items) {\n // Skip context scripts (own and other implementations), cache dirs\n if (\n this.contextScriptNames.includes(itemName) ||\n OTHER_CONTEXT_SCRIPTS.includes(itemName) ||\n SKIP_NAMES.has(itemName)\n ) {\n continue;\n }\n\n const itemPath = join(inputPath, itemName);\n const resolution = this.placeholderResolver.resolve(\n itemName,\n mergedCtx,\n );\n\n // Track items with expressions\n if (resolution.expressions.length > 0) {\n tracker.registerExpressionItem(itemPath, resolution.expressions);\n if (resolution.success) {\n tracker.markResolved(itemPath);\n }\n }\n\n // Skip if resolution failed\n if (!resolution.success) {\n continue;\n }\n\n const outputName = resolution.resolved!;\n const stat = statSync(itemPath);\n\n if (stat.isDirectory()) {\n const newOutputDir = outputDir.createDir(outputName);\n await this.processDirectory(\n itemPath,\n newOutputDir,\n mergedCtx,\n tracker,\n );\n } else if (this.isTemplateFile(itemName)) {\n // Duplicate prevention: skip non-substituted files in later iterations\n if (!resolution.wasSubstituted && idx > 0) continue;\n\n const ext = this.getTemplateExtension(itemName)!;\n const gen = this.contentGenerators.get(ext)!;\n const content = gen.generate(itemPath, mergedCtx);\n const finalName = outputName.slice(\n 0,\n outputName.length - ext.length,\n );\n outputDir.writeFile(finalName, content);\n } else {\n // Static file — copy as-is\n // Duplicate prevention for static files in iterated directories\n if (!resolution.wasSubstituted && idx > 0) continue;\n outputDir.copyFile(outputName, itemPath);\n }\n }\n }\n }\n\n private isTemplateFile(name: string): boolean {\n for (const ext of this.contentGenerators.keys()) {\n if (name.endsWith(ext)) return true;\n }\n return false;\n }\n\n private getTemplateExtension(name: string): string | null {\n for (const ext of this.contentGenerators.keys()) {\n if (name.endsWith(ext)) return ext;\n }\n return null;\n }\n\n private writeContextLog(\n outputPath: string,\n idx: number,\n context: ContextDict,\n ): void {\n const logContent = `# Merged context for iteration ${idx}\\n${JSON.stringify(context, null, 2)}`;\n const logPath = join(outputPath, `.gen_context_${idx}.log`);\n writeFileSync(logPath, logContent, \"utf-8\");\n }\n}\n","/**\n * Content generator interface and built-in string interpolator.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport type { Content } from \"./output.js\";\n\nexport abstract class BaseContentGenerator {\n abstract readonly extension: string;\n\n abstract generate(\n templatePath: string,\n context: Record<string, unknown>,\n ): Content;\n}\n\n/**\n * Minimal built-in content generator using {{expression}} substitution.\n * Supports dot-notation for nested property access. No control structures.\n */\nexport class BuiltinInterpolator extends BaseContentGenerator {\n readonly extension = \".tpl\";\n\n private static readonly PATTERN = /\\{\\{([\\w.]+)\\}\\}/g;\n\n generate(templatePath: string, context: Record<string, unknown>): Content {\n const templateText = readFileSync(templatePath, \"utf-8\");\n const result = templateText.replace(\n BuiltinInterpolator.PATTERN,\n (_match, expr: string) => this.resolve(expr, context),\n );\n return { content: result };\n }\n\n private resolve(expression: string, context: Record<string, unknown>): string {\n const parts = expression.split(\".\");\n let value: unknown = context;\n for (const part of parts) {\n if (typeof value === \"object\" && value !== null && part in value) {\n value = (value as Record<string, unknown>)[part];\n } else {\n throw new Error(\n `Cannot resolve expression '${expression}': '${part}' not found`,\n );\n }\n }\n return String(value);\n }\n}\n","/**\n * Context script loading.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { pathToFileURL } from \"node:url\";\n\n/** Default context script filenames by priority. */\nexport const CONTEXT_SCRIPT_NAMES = [\"_gen_context.js\", \"_gen_context.mjs\"];\n\n/** Context script filenames from other implementations (skipped, not loaded). */\nexport const OTHER_CONTEXT_SCRIPTS = [\"_gen_context.py\"];\n\nexport type ContextDict = Record<string, unknown>;\nexport type GetContextsFn = (parentContext: ContextDict) => ContextDict[];\n\n/**\n * Load a context script and execute its getContexts (or get_contexts) function.\n *\n * Returns a list of context dicts. If the script doesn't exist or doesn't\n * define the function, returns [parentContext] (single pass-through).\n */\nexport async function loadContextScript(\n dirPath: string,\n scriptNames: string[],\n parentContext: ContextDict,\n): Promise<ContextDict[]> {\n for (const name of scriptNames) {\n const scriptPath = `${dirPath}/${name}`;\n if (!existsSync(scriptPath)) continue;\n\n const fileUrl = pathToFileURL(scriptPath).href;\n const mod = (await import(fileUrl)) as Record<string, unknown>;\n\n // Support both camelCase and snake_case function names\n const fn = (mod.getContexts ?? mod.get_contexts) as\n | GetContextsFn\n | undefined;\n\n if (typeof fn === \"function\") {\n return fn(parentContext);\n }\n\n return [parentContext];\n }\n\n return [parentContext];\n}\n","/**\n * Output abstractions for creating directories and files.\n */\n\nimport { copyFileSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\n\nexport interface Content {\n content: string;\n encoding?: BufferEncoding;\n}\n\nexport class OutputDirectory {\n constructor(public readonly path: string) {}\n\n createDir(name: string): OutputDirectory {\n const dirPath = join(this.path, name);\n mkdirSync(dirPath, { recursive: true });\n return new OutputDirectory(dirPath);\n }\n\n writeFile(name: string, content: Content): string {\n const filePath = join(this.path, name);\n mkdirSync(dirname(filePath), { recursive: true });\n writeFileSync(filePath, content.content, {\n encoding: content.encoding ?? \"utf-8\",\n });\n return filePath;\n }\n\n copyFile(name: string, source: string): string {\n const filePath = join(this.path, name);\n mkdirSync(dirname(filePath), { recursive: true });\n copyFileSync(source, filePath);\n return filePath;\n }\n}\n","/**\n * Filename placeholder resolution and generation tracking.\n */\n\nexport interface FilenameResolution {\n original: string;\n resolved: string | null; // null if resolution failed\n expressions: string[]; // all expressions found in the filename\n success: boolean;\n wasSubstituted: boolean;\n}\n\nexport interface PlaceholderResolver {\n resolve(filename: string, context: Record<string, unknown>): FilenameResolution;\n}\n\n/**\n * Resolves [expression] placeholders using dot-notation property access.\n */\nexport class BracketResolver implements PlaceholderResolver {\n private static readonly PATTERN = /\\[([\\w.]+)\\]/g;\n\n resolve(\n filename: string,\n context: Record<string, unknown>,\n ): FilenameResolution {\n let resolutionFailed = false;\n const expressionsFound: string[] = [];\n\n const result = filename.replace(\n BracketResolver.PATTERN,\n (match, expr: string) => {\n expressionsFound.push(expr);\n const parts = expr.split(\".\");\n let value: unknown = context;\n\n try {\n for (const part of parts) {\n if (value === null || value === undefined) {\n resolutionFailed = true;\n return match;\n }\n if (typeof value === \"object\" && part in (value as object)) {\n value = (value as Record<string, unknown>)[part];\n } else {\n resolutionFailed = true;\n return match;\n }\n }\n return String(value);\n } catch {\n resolutionFailed = true;\n return match;\n }\n },\n );\n\n const resolved = resolutionFailed ? null : result;\n return {\n original: filename,\n resolved,\n expressions: expressionsFound,\n success: !resolutionFailed,\n wasSubstituted: !resolutionFailed && result !== filename,\n };\n }\n}\n\n/**\n * Tracks template items with expressions and whether they were resolved.\n */\nexport class GenerationTracker {\n private expressionItems = new Map<string, string[]>();\n private resolvedItems = new Set<string>();\n\n registerExpressionItem(itemPath: string, expressions: string[]): void {\n if (!this.expressionItems.has(itemPath)) {\n this.expressionItems.set(itemPath, expressions);\n }\n }\n\n markResolved(itemPath: string): void {\n this.resolvedItems.add(itemPath);\n }\n\n getUnresolvedItems(): Array<[string, string[]]> {\n const unresolved: Array<[string, string[]]> = [];\n for (const [path, exprs] of this.expressionItems) {\n if (!this.resolvedItems.has(path)) {\n unresolved.push([path, exprs]);\n }\n }\n return unresolved;\n }\n\n raiseIfUnresolved(): void {\n const unresolved = this.getUnresolvedItems();\n if (unresolved.length > 0) {\n throw new UnresolvedExpressionsError(unresolved);\n }\n }\n}\n\nexport class UnresolvedExpressionsError extends Error {\n public readonly unresolvedItems: Array<[string, string[]]>;\n\n constructor(unresolvedItems: Array<[string, string[]]>) {\n const itemsToShow = unresolvedItems.slice(0, 5);\n const lines = [\n \"The following template items have expressions that could not be resolved:\",\n ];\n for (const [itemPath, expressions] of itemsToShow) {\n const exprStr = expressions.map((e) => `[${e}]`).join(\", \");\n lines.push(` - ${itemPath}: ${exprStr}`);\n }\n if (unresolvedItems.length > 5) {\n lines.push(` ... and ${unresolvedItems.length - 5} more`);\n }\n super(lines.join(\"\\n\"));\n this.name = \"UnresolvedExpressionsError\";\n this.unresolvedItems = unresolvedItems;\n }\n}\n"],"mappings":"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","names":["existsSync","mkdirSync","writeFileSync","join","readFileSync","existsSync","join","dirname","existsSync","mkdirSync","join","writeFileSync"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output abstractions for creating directories and files.
|
|
3
|
+
*/
|
|
4
|
+
interface Content {
|
|
5
|
+
content: string;
|
|
6
|
+
encoding?: BufferEncoding;
|
|
7
|
+
}
|
|
8
|
+
declare class OutputDirectory {
|
|
9
|
+
readonly path: string;
|
|
10
|
+
constructor(path: string);
|
|
11
|
+
createDir(name: string): OutputDirectory;
|
|
12
|
+
writeFile(name: string, content: Content): string;
|
|
13
|
+
copyFile(name: string, source: string): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Content generator interface and built-in string interpolator.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
declare abstract class BaseContentGenerator {
|
|
21
|
+
abstract readonly extension: string;
|
|
22
|
+
abstract generate(templatePath: string, context: Record<string, unknown>): Content;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Minimal built-in content generator using {{expression}} substitution.
|
|
26
|
+
* Supports dot-notation for nested property access. No control structures.
|
|
27
|
+
*/
|
|
28
|
+
declare class BuiltinInterpolator extends BaseContentGenerator {
|
|
29
|
+
readonly extension = ".tpl";
|
|
30
|
+
private static readonly PATTERN;
|
|
31
|
+
generate(templatePath: string, context: Record<string, unknown>): Content;
|
|
32
|
+
private resolve;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type ContextDict = Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Filename placeholder resolution and generation tracking.
|
|
39
|
+
*/
|
|
40
|
+
interface FilenameResolution {
|
|
41
|
+
original: string;
|
|
42
|
+
resolved: string | null;
|
|
43
|
+
expressions: string[];
|
|
44
|
+
success: boolean;
|
|
45
|
+
wasSubstituted: boolean;
|
|
46
|
+
}
|
|
47
|
+
interface PlaceholderResolver {
|
|
48
|
+
resolve(filename: string, context: Record<string, unknown>): FilenameResolution;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolves [expression] placeholders using dot-notation property access.
|
|
52
|
+
*/
|
|
53
|
+
declare class BracketResolver implements PlaceholderResolver {
|
|
54
|
+
private static readonly PATTERN;
|
|
55
|
+
resolve(filename: string, context: Record<string, unknown>): FilenameResolution;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Tracks template items with expressions and whether they were resolved.
|
|
59
|
+
*/
|
|
60
|
+
declare class GenerationTracker {
|
|
61
|
+
private expressionItems;
|
|
62
|
+
private resolvedItems;
|
|
63
|
+
registerExpressionItem(itemPath: string, expressions: string[]): void;
|
|
64
|
+
markResolved(itemPath: string): void;
|
|
65
|
+
getUnresolvedItems(): Array<[string, string[]]>;
|
|
66
|
+
raiseIfUnresolved(): void;
|
|
67
|
+
}
|
|
68
|
+
declare class UnresolvedExpressionsError extends Error {
|
|
69
|
+
readonly unresolvedItems: Array<[string, string[]]>;
|
|
70
|
+
constructor(unresolvedItems: Array<[string, string[]]>);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Core generation engine.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
interface GeneratorOptions {
|
|
78
|
+
templateDir: string;
|
|
79
|
+
outputDir?: string;
|
|
80
|
+
outputStageDir?: string;
|
|
81
|
+
outputDestDir?: string;
|
|
82
|
+
contentGenerators?: BaseContentGenerator[];
|
|
83
|
+
placeholderResolver?: PlaceholderResolver;
|
|
84
|
+
contextScriptNames?: string[];
|
|
85
|
+
debug?: boolean;
|
|
86
|
+
}
|
|
87
|
+
declare class Generator {
|
|
88
|
+
private readonly templateDir;
|
|
89
|
+
private readonly outputWriteDir;
|
|
90
|
+
private readonly outputDestDir;
|
|
91
|
+
private readonly contentGenerators;
|
|
92
|
+
private readonly placeholderResolver;
|
|
93
|
+
private readonly contextScriptNames;
|
|
94
|
+
private readonly debug;
|
|
95
|
+
constructor(options: GeneratorOptions);
|
|
96
|
+
private resolveOutputDirs;
|
|
97
|
+
run(context?: ContextDict): Promise<void>;
|
|
98
|
+
private processDirectory;
|
|
99
|
+
private isTemplateFile;
|
|
100
|
+
private getTemplateExtension;
|
|
101
|
+
private writeContextLog;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { BaseContentGenerator, BracketResolver, BuiltinInterpolator, type Content, type FilenameResolution, GenerationTracker, Generator, type GeneratorOptions, OutputDirectory, type PlaceholderResolver, UnresolvedExpressionsError };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// src/generator.ts
|
|
2
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
3
|
+
import { join as join2 } from "path";
|
|
4
|
+
|
|
5
|
+
// src/content.ts
|
|
6
|
+
import { readFileSync } from "fs";
|
|
7
|
+
var BaseContentGenerator = class {
|
|
8
|
+
};
|
|
9
|
+
var BuiltinInterpolator = class _BuiltinInterpolator extends BaseContentGenerator {
|
|
10
|
+
extension = ".tpl";
|
|
11
|
+
static PATTERN = /\{\{([\w.]+)\}\}/g;
|
|
12
|
+
generate(templatePath, context) {
|
|
13
|
+
const templateText = readFileSync(templatePath, "utf-8");
|
|
14
|
+
const result = templateText.replace(
|
|
15
|
+
_BuiltinInterpolator.PATTERN,
|
|
16
|
+
(_match, expr) => this.resolve(expr, context)
|
|
17
|
+
);
|
|
18
|
+
return { content: result };
|
|
19
|
+
}
|
|
20
|
+
resolve(expression, context) {
|
|
21
|
+
const parts = expression.split(".");
|
|
22
|
+
let value = context;
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
if (typeof value === "object" && value !== null && part in value) {
|
|
25
|
+
value = value[part];
|
|
26
|
+
} else {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Cannot resolve expression '${expression}': '${part}' not found`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return String(value);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// src/context.ts
|
|
37
|
+
import { existsSync } from "fs";
|
|
38
|
+
import { pathToFileURL } from "url";
|
|
39
|
+
var CONTEXT_SCRIPT_NAMES = ["_gen_context.js", "_gen_context.mjs"];
|
|
40
|
+
var OTHER_CONTEXT_SCRIPTS = ["_gen_context.py"];
|
|
41
|
+
async function loadContextScript(dirPath, scriptNames, parentContext) {
|
|
42
|
+
for (const name of scriptNames) {
|
|
43
|
+
const scriptPath = `${dirPath}/${name}`;
|
|
44
|
+
if (!existsSync(scriptPath)) continue;
|
|
45
|
+
const fileUrl = pathToFileURL(scriptPath).href;
|
|
46
|
+
const mod = await import(fileUrl);
|
|
47
|
+
const fn = mod.getContexts ?? mod.get_contexts;
|
|
48
|
+
if (typeof fn === "function") {
|
|
49
|
+
return fn(parentContext);
|
|
50
|
+
}
|
|
51
|
+
return [parentContext];
|
|
52
|
+
}
|
|
53
|
+
return [parentContext];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/output.ts
|
|
57
|
+
import { copyFileSync, mkdirSync, writeFileSync } from "fs";
|
|
58
|
+
import { join, dirname } from "path";
|
|
59
|
+
var OutputDirectory = class _OutputDirectory {
|
|
60
|
+
constructor(path) {
|
|
61
|
+
this.path = path;
|
|
62
|
+
}
|
|
63
|
+
createDir(name) {
|
|
64
|
+
const dirPath = join(this.path, name);
|
|
65
|
+
mkdirSync(dirPath, { recursive: true });
|
|
66
|
+
return new _OutputDirectory(dirPath);
|
|
67
|
+
}
|
|
68
|
+
writeFile(name, content) {
|
|
69
|
+
const filePath = join(this.path, name);
|
|
70
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
71
|
+
writeFileSync(filePath, content.content, {
|
|
72
|
+
encoding: content.encoding ?? "utf-8"
|
|
73
|
+
});
|
|
74
|
+
return filePath;
|
|
75
|
+
}
|
|
76
|
+
copyFile(name, source) {
|
|
77
|
+
const filePath = join(this.path, name);
|
|
78
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
79
|
+
copyFileSync(source, filePath);
|
|
80
|
+
return filePath;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/resolution.ts
|
|
85
|
+
var BracketResolver = class _BracketResolver {
|
|
86
|
+
static PATTERN = /\[([\w.]+)\]/g;
|
|
87
|
+
resolve(filename, context) {
|
|
88
|
+
let resolutionFailed = false;
|
|
89
|
+
const expressionsFound = [];
|
|
90
|
+
const result = filename.replace(
|
|
91
|
+
_BracketResolver.PATTERN,
|
|
92
|
+
(match, expr) => {
|
|
93
|
+
expressionsFound.push(expr);
|
|
94
|
+
const parts = expr.split(".");
|
|
95
|
+
let value = context;
|
|
96
|
+
try {
|
|
97
|
+
for (const part of parts) {
|
|
98
|
+
if (value === null || value === void 0) {
|
|
99
|
+
resolutionFailed = true;
|
|
100
|
+
return match;
|
|
101
|
+
}
|
|
102
|
+
if (typeof value === "object" && part in value) {
|
|
103
|
+
value = value[part];
|
|
104
|
+
} else {
|
|
105
|
+
resolutionFailed = true;
|
|
106
|
+
return match;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return String(value);
|
|
110
|
+
} catch {
|
|
111
|
+
resolutionFailed = true;
|
|
112
|
+
return match;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
const resolved = resolutionFailed ? null : result;
|
|
117
|
+
return {
|
|
118
|
+
original: filename,
|
|
119
|
+
resolved,
|
|
120
|
+
expressions: expressionsFound,
|
|
121
|
+
success: !resolutionFailed,
|
|
122
|
+
wasSubstituted: !resolutionFailed && result !== filename
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var GenerationTracker = class {
|
|
127
|
+
expressionItems = /* @__PURE__ */ new Map();
|
|
128
|
+
resolvedItems = /* @__PURE__ */ new Set();
|
|
129
|
+
registerExpressionItem(itemPath, expressions) {
|
|
130
|
+
if (!this.expressionItems.has(itemPath)) {
|
|
131
|
+
this.expressionItems.set(itemPath, expressions);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
markResolved(itemPath) {
|
|
135
|
+
this.resolvedItems.add(itemPath);
|
|
136
|
+
}
|
|
137
|
+
getUnresolvedItems() {
|
|
138
|
+
const unresolved = [];
|
|
139
|
+
for (const [path, exprs] of this.expressionItems) {
|
|
140
|
+
if (!this.resolvedItems.has(path)) {
|
|
141
|
+
unresolved.push([path, exprs]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return unresolved;
|
|
145
|
+
}
|
|
146
|
+
raiseIfUnresolved() {
|
|
147
|
+
const unresolved = this.getUnresolvedItems();
|
|
148
|
+
if (unresolved.length > 0) {
|
|
149
|
+
throw new UnresolvedExpressionsError(unresolved);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var UnresolvedExpressionsError = class extends Error {
|
|
154
|
+
unresolvedItems;
|
|
155
|
+
constructor(unresolvedItems) {
|
|
156
|
+
const itemsToShow = unresolvedItems.slice(0, 5);
|
|
157
|
+
const lines = [
|
|
158
|
+
"The following template items have expressions that could not be resolved:"
|
|
159
|
+
];
|
|
160
|
+
for (const [itemPath, expressions] of itemsToShow) {
|
|
161
|
+
const exprStr = expressions.map((e) => `[${e}]`).join(", ");
|
|
162
|
+
lines.push(` - ${itemPath}: ${exprStr}`);
|
|
163
|
+
}
|
|
164
|
+
if (unresolvedItems.length > 5) {
|
|
165
|
+
lines.push(` ... and ${unresolvedItems.length - 5} more`);
|
|
166
|
+
}
|
|
167
|
+
super(lines.join("\n"));
|
|
168
|
+
this.name = "UnresolvedExpressionsError";
|
|
169
|
+
this.unresolvedItems = unresolvedItems;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/generator.ts
|
|
174
|
+
var SKIP_NAMES = /* @__PURE__ */ new Set([
|
|
175
|
+
"__pycache__",
|
|
176
|
+
"node_modules",
|
|
177
|
+
".DS_Store"
|
|
178
|
+
]);
|
|
179
|
+
var Generator = class {
|
|
180
|
+
templateDir;
|
|
181
|
+
outputWriteDir;
|
|
182
|
+
outputDestDir;
|
|
183
|
+
contentGenerators;
|
|
184
|
+
placeholderResolver;
|
|
185
|
+
contextScriptNames;
|
|
186
|
+
debug;
|
|
187
|
+
constructor(options) {
|
|
188
|
+
this.templateDir = options.templateDir;
|
|
189
|
+
this.placeholderResolver = options.placeholderResolver ?? new BracketResolver();
|
|
190
|
+
this.contextScriptNames = options.contextScriptNames ?? CONTEXT_SCRIPT_NAMES;
|
|
191
|
+
this.debug = options.debug ?? false;
|
|
192
|
+
this.contentGenerators = /* @__PURE__ */ new Map();
|
|
193
|
+
const builtin = new BuiltinInterpolator();
|
|
194
|
+
this.contentGenerators.set(builtin.extension, builtin);
|
|
195
|
+
for (const gen of options.contentGenerators ?? []) {
|
|
196
|
+
this.contentGenerators.set(gen.extension, gen);
|
|
197
|
+
}
|
|
198
|
+
const [writeDir, destDir] = this.resolveOutputDirs(
|
|
199
|
+
options.outputDir,
|
|
200
|
+
options.outputStageDir,
|
|
201
|
+
options.outputDestDir
|
|
202
|
+
);
|
|
203
|
+
this.outputWriteDir = writeDir;
|
|
204
|
+
this.outputDestDir = destDir;
|
|
205
|
+
}
|
|
206
|
+
resolveOutputDirs(outputDir, outputStageDir, outputDestDir) {
|
|
207
|
+
const hasSingle = outputDir !== void 0;
|
|
208
|
+
const hasStage = outputStageDir !== void 0;
|
|
209
|
+
const hasDest = outputDestDir !== void 0;
|
|
210
|
+
if (hasSingle && (hasStage || hasDest)) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
"Cannot specify both outputDir and outputStageDir/outputDestDir. Use outputDir for 1-stage or outputStageDir + outputDestDir for 2-stage."
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (hasStage !== hasDest) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
"outputStageDir and outputDestDir must both be specified for 2-stage output."
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (!hasSingle && !hasStage) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
"No output directory configured. Specify outputDir (1-stage) or outputStageDir + outputDestDir (2-stage)."
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (hasSingle) {
|
|
226
|
+
return [outputDir, null];
|
|
227
|
+
}
|
|
228
|
+
return [outputStageDir, outputDestDir];
|
|
229
|
+
}
|
|
230
|
+
async run(context) {
|
|
231
|
+
if (!existsSync2(this.templateDir) || !statSync(this.templateDir).isDirectory()) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Template directory does not exist: ${this.templateDir}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const rootContext = context ?? {};
|
|
237
|
+
const tracker = new GenerationTracker();
|
|
238
|
+
const outputRoot = new OutputDirectory(this.outputWriteDir);
|
|
239
|
+
mkdirSync2(this.outputWriteDir, { recursive: true });
|
|
240
|
+
await this.processDirectory(
|
|
241
|
+
this.templateDir,
|
|
242
|
+
outputRoot,
|
|
243
|
+
rootContext,
|
|
244
|
+
tracker
|
|
245
|
+
);
|
|
246
|
+
tracker.raiseIfUnresolved();
|
|
247
|
+
}
|
|
248
|
+
async processDirectory(inputPath, outputDir, parentCtx, tracker) {
|
|
249
|
+
const contexts = await loadContextScript(
|
|
250
|
+
inputPath,
|
|
251
|
+
this.contextScriptNames,
|
|
252
|
+
parentCtx
|
|
253
|
+
);
|
|
254
|
+
for (let idx = 0; idx < contexts.length; idx++) {
|
|
255
|
+
const ctx = contexts[idx];
|
|
256
|
+
const mergedCtx = { ...parentCtx, ...ctx };
|
|
257
|
+
if (this.debug) {
|
|
258
|
+
this.writeContextLog(outputDir.path, idx, mergedCtx);
|
|
259
|
+
}
|
|
260
|
+
const items = readdirSync(inputPath);
|
|
261
|
+
for (const itemName of items) {
|
|
262
|
+
if (this.contextScriptNames.includes(itemName) || OTHER_CONTEXT_SCRIPTS.includes(itemName) || SKIP_NAMES.has(itemName)) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const itemPath = join2(inputPath, itemName);
|
|
266
|
+
const resolution = this.placeholderResolver.resolve(
|
|
267
|
+
itemName,
|
|
268
|
+
mergedCtx
|
|
269
|
+
);
|
|
270
|
+
if (resolution.expressions.length > 0) {
|
|
271
|
+
tracker.registerExpressionItem(itemPath, resolution.expressions);
|
|
272
|
+
if (resolution.success) {
|
|
273
|
+
tracker.markResolved(itemPath);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!resolution.success) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const outputName = resolution.resolved;
|
|
280
|
+
const stat = statSync(itemPath);
|
|
281
|
+
if (stat.isDirectory()) {
|
|
282
|
+
const newOutputDir = outputDir.createDir(outputName);
|
|
283
|
+
await this.processDirectory(
|
|
284
|
+
itemPath,
|
|
285
|
+
newOutputDir,
|
|
286
|
+
mergedCtx,
|
|
287
|
+
tracker
|
|
288
|
+
);
|
|
289
|
+
} else if (this.isTemplateFile(itemName)) {
|
|
290
|
+
if (!resolution.wasSubstituted && idx > 0) continue;
|
|
291
|
+
const ext = this.getTemplateExtension(itemName);
|
|
292
|
+
const gen = this.contentGenerators.get(ext);
|
|
293
|
+
const content = gen.generate(itemPath, mergedCtx);
|
|
294
|
+
const finalName = outputName.slice(
|
|
295
|
+
0,
|
|
296
|
+
outputName.length - ext.length
|
|
297
|
+
);
|
|
298
|
+
outputDir.writeFile(finalName, content);
|
|
299
|
+
} else {
|
|
300
|
+
if (!resolution.wasSubstituted && idx > 0) continue;
|
|
301
|
+
outputDir.copyFile(outputName, itemPath);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
isTemplateFile(name) {
|
|
307
|
+
for (const ext of this.contentGenerators.keys()) {
|
|
308
|
+
if (name.endsWith(ext)) return true;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
getTemplateExtension(name) {
|
|
313
|
+
for (const ext of this.contentGenerators.keys()) {
|
|
314
|
+
if (name.endsWith(ext)) return ext;
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
writeContextLog(outputPath, idx, context) {
|
|
319
|
+
const logContent = `# Merged context for iteration ${idx}
|
|
320
|
+
${JSON.stringify(context, null, 2)}`;
|
|
321
|
+
const logPath = join2(outputPath, `.gen_context_${idx}.log`);
|
|
322
|
+
writeFileSync2(logPath, logContent, "utf-8");
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
export {
|
|
326
|
+
BaseContentGenerator,
|
|
327
|
+
BracketResolver,
|
|
328
|
+
BuiltinInterpolator,
|
|
329
|
+
GenerationTracker,
|
|
330
|
+
Generator,
|
|
331
|
+
OutputDirectory,
|
|
332
|
+
UnresolvedExpressionsError
|
|
333
|
+
};
|
|
334
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/generator.ts","../src/content.ts","../src/context.ts","../src/output.ts","../src/resolution.ts"],"sourcesContent":["/**\n * Core generation engine.\n */\n\nimport { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport { BaseContentGenerator, BuiltinInterpolator } from \"./content.js\";\nimport { CONTEXT_SCRIPT_NAMES, OTHER_CONTEXT_SCRIPTS, loadContextScript } from \"./context.js\";\nimport type { ContextDict } from \"./context.js\";\nimport { OutputDirectory } from \"./output.js\";\nimport {\n BracketResolver,\n GenerationTracker,\n type PlaceholderResolver,\n} from \"./resolution.js\";\n\n/** Items to always skip during directory walking. */\nconst SKIP_NAMES = new Set([\n \"__pycache__\",\n \"node_modules\",\n \".DS_Store\",\n]);\n\nexport interface GeneratorOptions {\n templateDir: string;\n outputDir?: string;\n outputStageDir?: string;\n outputDestDir?: string;\n contentGenerators?: BaseContentGenerator[];\n placeholderResolver?: PlaceholderResolver;\n contextScriptNames?: string[];\n debug?: boolean;\n}\n\nexport class Generator {\n private readonly templateDir: string;\n private readonly outputWriteDir: string;\n private readonly outputDestDir: string | null;\n private readonly contentGenerators: Map<string, BaseContentGenerator>;\n private readonly placeholderResolver: PlaceholderResolver;\n private readonly contextScriptNames: string[];\n private readonly debug: boolean;\n\n constructor(options: GeneratorOptions) {\n this.templateDir = options.templateDir;\n this.placeholderResolver =\n options.placeholderResolver ?? new BracketResolver();\n this.contextScriptNames =\n options.contextScriptNames ?? CONTEXT_SCRIPT_NAMES;\n this.debug = options.debug ?? false;\n\n // Build extension -> generator mapping (built-in always included)\n this.contentGenerators = new Map<string, BaseContentGenerator>();\n const builtin = new BuiltinInterpolator();\n this.contentGenerators.set(builtin.extension, builtin);\n for (const gen of options.contentGenerators ?? []) {\n this.contentGenerators.set(gen.extension, gen);\n }\n\n // Resolve output directories\n const [writeDir, destDir] = this.resolveOutputDirs(\n options.outputDir,\n options.outputStageDir,\n options.outputDestDir,\n );\n this.outputWriteDir = writeDir;\n this.outputDestDir = destDir;\n }\n\n private resolveOutputDirs(\n outputDir?: string,\n outputStageDir?: string,\n outputDestDir?: string,\n ): [string, string | null] {\n const hasSingle = outputDir !== undefined;\n const hasStage = outputStageDir !== undefined;\n const hasDest = outputDestDir !== undefined;\n\n if (hasSingle && (hasStage || hasDest)) {\n throw new Error(\n \"Cannot specify both outputDir and outputStageDir/outputDestDir. \" +\n \"Use outputDir for 1-stage or outputStageDir + outputDestDir for 2-stage.\",\n );\n }\n if (hasStage !== hasDest) {\n throw new Error(\n \"outputStageDir and outputDestDir must both be specified for 2-stage output.\",\n );\n }\n if (!hasSingle && !hasStage) {\n throw new Error(\n \"No output directory configured. \" +\n \"Specify outputDir (1-stage) or outputStageDir + outputDestDir (2-stage).\",\n );\n }\n\n if (hasSingle) {\n return [outputDir!, null];\n }\n return [outputStageDir!, outputDestDir!];\n }\n\n async run(context?: ContextDict): Promise<void> {\n if (!existsSync(this.templateDir) || !statSync(this.templateDir).isDirectory()) {\n throw new Error(\n `Template directory does not exist: ${this.templateDir}`,\n );\n }\n\n const rootContext = context ?? {};\n const tracker = new GenerationTracker();\n const outputRoot = new OutputDirectory(this.outputWriteDir);\n mkdirSync(this.outputWriteDir, { recursive: true });\n\n await this.processDirectory(\n this.templateDir,\n outputRoot,\n rootContext,\n tracker,\n );\n tracker.raiseIfUnresolved();\n }\n\n private async processDirectory(\n inputPath: string,\n outputDir: OutputDirectory,\n parentCtx: ContextDict,\n tracker: GenerationTracker,\n ): Promise<void> {\n const contexts = await loadContextScript(\n inputPath,\n this.contextScriptNames,\n parentCtx,\n );\n\n for (let idx = 0; idx < contexts.length; idx++) {\n const ctx = contexts[idx];\n const mergedCtx = { ...parentCtx, ...ctx };\n\n if (this.debug) {\n this.writeContextLog(outputDir.path, idx, mergedCtx);\n }\n\n const items = readdirSync(inputPath);\n for (const itemName of items) {\n // Skip context scripts (own and other implementations), cache dirs\n if (\n this.contextScriptNames.includes(itemName) ||\n OTHER_CONTEXT_SCRIPTS.includes(itemName) ||\n SKIP_NAMES.has(itemName)\n ) {\n continue;\n }\n\n const itemPath = join(inputPath, itemName);\n const resolution = this.placeholderResolver.resolve(\n itemName,\n mergedCtx,\n );\n\n // Track items with expressions\n if (resolution.expressions.length > 0) {\n tracker.registerExpressionItem(itemPath, resolution.expressions);\n if (resolution.success) {\n tracker.markResolved(itemPath);\n }\n }\n\n // Skip if resolution failed\n if (!resolution.success) {\n continue;\n }\n\n const outputName = resolution.resolved!;\n const stat = statSync(itemPath);\n\n if (stat.isDirectory()) {\n const newOutputDir = outputDir.createDir(outputName);\n await this.processDirectory(\n itemPath,\n newOutputDir,\n mergedCtx,\n tracker,\n );\n } else if (this.isTemplateFile(itemName)) {\n // Duplicate prevention: skip non-substituted files in later iterations\n if (!resolution.wasSubstituted && idx > 0) continue;\n\n const ext = this.getTemplateExtension(itemName)!;\n const gen = this.contentGenerators.get(ext)!;\n const content = gen.generate(itemPath, mergedCtx);\n const finalName = outputName.slice(\n 0,\n outputName.length - ext.length,\n );\n outputDir.writeFile(finalName, content);\n } else {\n // Static file — copy as-is\n // Duplicate prevention for static files in iterated directories\n if (!resolution.wasSubstituted && idx > 0) continue;\n outputDir.copyFile(outputName, itemPath);\n }\n }\n }\n }\n\n private isTemplateFile(name: string): boolean {\n for (const ext of this.contentGenerators.keys()) {\n if (name.endsWith(ext)) return true;\n }\n return false;\n }\n\n private getTemplateExtension(name: string): string | null {\n for (const ext of this.contentGenerators.keys()) {\n if (name.endsWith(ext)) return ext;\n }\n return null;\n }\n\n private writeContextLog(\n outputPath: string,\n idx: number,\n context: ContextDict,\n ): void {\n const logContent = `# Merged context for iteration ${idx}\\n${JSON.stringify(context, null, 2)}`;\n const logPath = join(outputPath, `.gen_context_${idx}.log`);\n writeFileSync(logPath, logContent, \"utf-8\");\n }\n}\n","/**\n * Content generator interface and built-in string interpolator.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport type { Content } from \"./output.js\";\n\nexport abstract class BaseContentGenerator {\n abstract readonly extension: string;\n\n abstract generate(\n templatePath: string,\n context: Record<string, unknown>,\n ): Content;\n}\n\n/**\n * Minimal built-in content generator using {{expression}} substitution.\n * Supports dot-notation for nested property access. No control structures.\n */\nexport class BuiltinInterpolator extends BaseContentGenerator {\n readonly extension = \".tpl\";\n\n private static readonly PATTERN = /\\{\\{([\\w.]+)\\}\\}/g;\n\n generate(templatePath: string, context: Record<string, unknown>): Content {\n const templateText = readFileSync(templatePath, \"utf-8\");\n const result = templateText.replace(\n BuiltinInterpolator.PATTERN,\n (_match, expr: string) => this.resolve(expr, context),\n );\n return { content: result };\n }\n\n private resolve(expression: string, context: Record<string, unknown>): string {\n const parts = expression.split(\".\");\n let value: unknown = context;\n for (const part of parts) {\n if (typeof value === \"object\" && value !== null && part in value) {\n value = (value as Record<string, unknown>)[part];\n } else {\n throw new Error(\n `Cannot resolve expression '${expression}': '${part}' not found`,\n );\n }\n }\n return String(value);\n }\n}\n","/**\n * Context script loading.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { pathToFileURL } from \"node:url\";\n\n/** Default context script filenames by priority. */\nexport const CONTEXT_SCRIPT_NAMES = [\"_gen_context.js\", \"_gen_context.mjs\"];\n\n/** Context script filenames from other implementations (skipped, not loaded). */\nexport const OTHER_CONTEXT_SCRIPTS = [\"_gen_context.py\"];\n\nexport type ContextDict = Record<string, unknown>;\nexport type GetContextsFn = (parentContext: ContextDict) => ContextDict[];\n\n/**\n * Load a context script and execute its getContexts (or get_contexts) function.\n *\n * Returns a list of context dicts. If the script doesn't exist or doesn't\n * define the function, returns [parentContext] (single pass-through).\n */\nexport async function loadContextScript(\n dirPath: string,\n scriptNames: string[],\n parentContext: ContextDict,\n): Promise<ContextDict[]> {\n for (const name of scriptNames) {\n const scriptPath = `${dirPath}/${name}`;\n if (!existsSync(scriptPath)) continue;\n\n const fileUrl = pathToFileURL(scriptPath).href;\n const mod = (await import(fileUrl)) as Record<string, unknown>;\n\n // Support both camelCase and snake_case function names\n const fn = (mod.getContexts ?? mod.get_contexts) as\n | GetContextsFn\n | undefined;\n\n if (typeof fn === \"function\") {\n return fn(parentContext);\n }\n\n return [parentContext];\n }\n\n return [parentContext];\n}\n","/**\n * Output abstractions for creating directories and files.\n */\n\nimport { copyFileSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\n\nexport interface Content {\n content: string;\n encoding?: BufferEncoding;\n}\n\nexport class OutputDirectory {\n constructor(public readonly path: string) {}\n\n createDir(name: string): OutputDirectory {\n const dirPath = join(this.path, name);\n mkdirSync(dirPath, { recursive: true });\n return new OutputDirectory(dirPath);\n }\n\n writeFile(name: string, content: Content): string {\n const filePath = join(this.path, name);\n mkdirSync(dirname(filePath), { recursive: true });\n writeFileSync(filePath, content.content, {\n encoding: content.encoding ?? \"utf-8\",\n });\n return filePath;\n }\n\n copyFile(name: string, source: string): string {\n const filePath = join(this.path, name);\n mkdirSync(dirname(filePath), { recursive: true });\n copyFileSync(source, filePath);\n return filePath;\n }\n}\n","/**\n * Filename placeholder resolution and generation tracking.\n */\n\nexport interface FilenameResolution {\n original: string;\n resolved: string | null; // null if resolution failed\n expressions: string[]; // all expressions found in the filename\n success: boolean;\n wasSubstituted: boolean;\n}\n\nexport interface PlaceholderResolver {\n resolve(filename: string, context: Record<string, unknown>): FilenameResolution;\n}\n\n/**\n * Resolves [expression] placeholders using dot-notation property access.\n */\nexport class BracketResolver implements PlaceholderResolver {\n private static readonly PATTERN = /\\[([\\w.]+)\\]/g;\n\n resolve(\n filename: string,\n context: Record<string, unknown>,\n ): FilenameResolution {\n let resolutionFailed = false;\n const expressionsFound: string[] = [];\n\n const result = filename.replace(\n BracketResolver.PATTERN,\n (match, expr: string) => {\n expressionsFound.push(expr);\n const parts = expr.split(\".\");\n let value: unknown = context;\n\n try {\n for (const part of parts) {\n if (value === null || value === undefined) {\n resolutionFailed = true;\n return match;\n }\n if (typeof value === \"object\" && part in (value as object)) {\n value = (value as Record<string, unknown>)[part];\n } else {\n resolutionFailed = true;\n return match;\n }\n }\n return String(value);\n } catch {\n resolutionFailed = true;\n return match;\n }\n },\n );\n\n const resolved = resolutionFailed ? null : result;\n return {\n original: filename,\n resolved,\n expressions: expressionsFound,\n success: !resolutionFailed,\n wasSubstituted: !resolutionFailed && result !== filename,\n };\n }\n}\n\n/**\n * Tracks template items with expressions and whether they were resolved.\n */\nexport class GenerationTracker {\n private expressionItems = new Map<string, string[]>();\n private resolvedItems = new Set<string>();\n\n registerExpressionItem(itemPath: string, expressions: string[]): void {\n if (!this.expressionItems.has(itemPath)) {\n this.expressionItems.set(itemPath, expressions);\n }\n }\n\n markResolved(itemPath: string): void {\n this.resolvedItems.add(itemPath);\n }\n\n getUnresolvedItems(): Array<[string, string[]]> {\n const unresolved: Array<[string, string[]]> = [];\n for (const [path, exprs] of this.expressionItems) {\n if (!this.resolvedItems.has(path)) {\n unresolved.push([path, exprs]);\n }\n }\n return unresolved;\n }\n\n raiseIfUnresolved(): void {\n const unresolved = this.getUnresolvedItems();\n if (unresolved.length > 0) {\n throw new UnresolvedExpressionsError(unresolved);\n }\n }\n}\n\nexport class UnresolvedExpressionsError extends Error {\n public readonly unresolvedItems: Array<[string, string[]]>;\n\n constructor(unresolvedItems: Array<[string, string[]]>) {\n const itemsToShow = unresolvedItems.slice(0, 5);\n const lines = [\n \"The following template items have expressions that could not be resolved:\",\n ];\n for (const [itemPath, expressions] of itemsToShow) {\n const exprStr = expressions.map((e) => `[${e}]`).join(\", \");\n lines.push(` - ${itemPath}: ${exprStr}`);\n }\n if (unresolvedItems.length > 5) {\n lines.push(` ... and ${unresolvedItems.length - 5} more`);\n }\n super(lines.join(\"\\n\"));\n this.name = \"UnresolvedExpressionsError\";\n this.unresolvedItems = unresolvedItems;\n }\n}\n"],"mappings":"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","names":["existsSync","mkdirSync","writeFileSync","join","existsSync","mkdirSync","join","writeFileSync"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reifinator",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Template-structure-driven code and directory generator",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"reify": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup",
|
|
13
|
+
"dev": "tsx src/cli.ts",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"lint": "eslint src/ tests/"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"commander": "^14.0.0",
|
|
20
|
+
"js-yaml": "^4.1.0"
|
|
21
|
+
},
|
|
22
|
+
"optionalDependencies": {
|
|
23
|
+
"eta": "^3.5.0",
|
|
24
|
+
"nunjucks": "^3.2.4"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/js-yaml": "^4.0.9",
|
|
28
|
+
"@types/nunjucks": "^3.2.6",
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"tsup": "^8.4.0",
|
|
31
|
+
"tsx": "^4.21.0",
|
|
32
|
+
"typescript": "^5.7.0",
|
|
33
|
+
"vitest": "^3.0.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20.0.0"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"author": "Paul Steyn <if.paul.then@gmail.com>",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/if-paul-then/reifinator.git"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/if-paul-then/reifinator",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/if-paul-then/reifinator/issues"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"code-generation",
|
|
53
|
+
"template",
|
|
54
|
+
"scaffolding",
|
|
55
|
+
"generator",
|
|
56
|
+
"eta",
|
|
57
|
+
"nunjucks"
|
|
58
|
+
]
|
|
59
|
+
}
|