@canonical/summon 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/README.md +439 -0
- package/generators/example/hello/index.ts +132 -0
- package/generators/example/hello/templates/README.md.ejs +20 -0
- package/generators/example/hello/templates/index.ts.ejs +9 -0
- package/generators/example/webapp/index.ts +509 -0
- package/generators/example/webapp/templates/ARCHITECTURE.md.ejs +180 -0
- package/generators/example/webapp/templates/App.tsx.ejs +86 -0
- package/generators/example/webapp/templates/README.md.ejs +154 -0
- package/generators/example/webapp/templates/app.test.ts.ejs +63 -0
- package/generators/example/webapp/templates/app.ts.ejs +132 -0
- package/generators/example/webapp/templates/feature.ts.ejs +264 -0
- package/generators/example/webapp/templates/index.html.ejs +20 -0
- package/generators/example/webapp/templates/main.tsx.ejs +43 -0
- package/generators/example/webapp/templates/styles.css.ejs +135 -0
- package/generators/init/index.ts +124 -0
- package/generators/init/templates/generator.ts.ejs +85 -0
- package/generators/init/templates/template-index.ts.ejs +9 -0
- package/generators/init/templates/template-test.ts.ejs +8 -0
- package/package.json +64 -0
- package/src/__tests__/combinators.test.ts +895 -0
- package/src/__tests__/dry-run.test.ts +927 -0
- package/src/__tests__/effect.test.ts +816 -0
- package/src/__tests__/interpreter.test.ts +673 -0
- package/src/__tests__/primitives.test.ts +970 -0
- package/src/__tests__/task.test.ts +929 -0
- package/src/__tests__/template.test.ts +666 -0
- package/src/cli-format.ts +165 -0
- package/src/cli-types.ts +53 -0
- package/src/cli.tsx +1322 -0
- package/src/combinators.ts +294 -0
- package/src/completion.ts +488 -0
- package/src/components/App.tsx +960 -0
- package/src/components/ExecutionProgress.tsx +205 -0
- package/src/components/FileTreePreview.tsx +97 -0
- package/src/components/PromptSequence.tsx +483 -0
- package/src/components/Spinner.tsx +36 -0
- package/src/components/index.ts +16 -0
- package/src/dry-run.ts +434 -0
- package/src/effect.ts +224 -0
- package/src/index.ts +266 -0
- package/src/interpreter.ts +463 -0
- package/src/primitives.ts +442 -0
- package/src/task.ts +245 -0
- package/src/template.ts +537 -0
- package/src/types.ts +453 -0
package/src/template.ts
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Engine
|
|
3
|
+
*
|
|
4
|
+
* This module provides template rendering utilities for generators.
|
|
5
|
+
* Templates are a key part of code generation, allowing dynamic file creation.
|
|
6
|
+
*
|
|
7
|
+
* By default uses EJS, but supports custom templating engines via the
|
|
8
|
+
* TemplatingEngine interface.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import * as ejs from "ejs";
|
|
13
|
+
import { sequence_ } from "./combinators.js";
|
|
14
|
+
import { glob, mkdir, readFile, writeFile } from "./primitives.js";
|
|
15
|
+
import { task } from "./task.js";
|
|
16
|
+
import type { Task } from "./types.js";
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Templating Engine Interface
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Abstract interface for templating engines.
|
|
24
|
+
*
|
|
25
|
+
* Implement this interface to use alternative template engines
|
|
26
|
+
* (e.g., Handlebars, Mustache, Nunjucks) with the summon generator.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const handlebarsEngine: TemplatingEngine = {
|
|
31
|
+
* render: (tpl, vars) => Handlebars.compile(tpl)(vars),
|
|
32
|
+
* renderAsync: async (tpl, vars) => Handlebars.compile(tpl)(vars),
|
|
33
|
+
* renderFile: async (path, vars) => {
|
|
34
|
+
* const tpl = await fs.readFile(path, "utf-8");
|
|
35
|
+
* return Handlebars.compile(tpl)(vars);
|
|
36
|
+
* },
|
|
37
|
+
* };
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export interface TemplatingEngine {
|
|
41
|
+
/**
|
|
42
|
+
* Render a template string with variables (synchronous).
|
|
43
|
+
* Used for quick inline rendering like destination paths.
|
|
44
|
+
*/
|
|
45
|
+
render(template: string, vars: Record<string, unknown>): string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Render a template string with variables (asynchronous).
|
|
49
|
+
* Useful when templates may include async operations.
|
|
50
|
+
*/
|
|
51
|
+
renderAsync(template: string, vars: Record<string, unknown>): Promise<string>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Render a template file with variables (asynchronous).
|
|
55
|
+
* Implementations may leverage caching or streaming.
|
|
56
|
+
*/
|
|
57
|
+
renderFile(
|
|
58
|
+
templatePath: string,
|
|
59
|
+
vars: Record<string, unknown>,
|
|
60
|
+
): Promise<string>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// EJS Engine Implementation
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Default EJS templating engine implementation.
|
|
69
|
+
*/
|
|
70
|
+
export const ejsEngine: TemplatingEngine = {
|
|
71
|
+
render(template, vars) {
|
|
72
|
+
return ejs.render(template, vars, { async: false });
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async renderAsync(template, vars) {
|
|
76
|
+
return ejs.render(template, vars, { async: true }) as Promise<string>;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async renderFile(templatePath, vars) {
|
|
80
|
+
return ejs.renderFile(templatePath, vars, { async: true });
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// =============================================================================
|
|
85
|
+
// Stamp Options
|
|
86
|
+
// =============================================================================
|
|
87
|
+
|
|
88
|
+
export interface StampOptions {
|
|
89
|
+
/** Generator name (e.g., "@canonical/summon-package") */
|
|
90
|
+
generator: string;
|
|
91
|
+
/** Generator version (e.g., "0.1.0") */
|
|
92
|
+
version: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// Template Options
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
export interface TemplateOptions {
|
|
100
|
+
/** Path to the template file */
|
|
101
|
+
source: string;
|
|
102
|
+
/** Destination path (can contain template variables) */
|
|
103
|
+
dest: string;
|
|
104
|
+
/** Variables to pass to the template */
|
|
105
|
+
vars: Record<string, unknown>;
|
|
106
|
+
/** Templating engine to use (defaults to EJS) */
|
|
107
|
+
engine?: TemplatingEngine;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface TemplateDirOptions {
|
|
111
|
+
/** Source directory containing templates */
|
|
112
|
+
source: string;
|
|
113
|
+
/** Destination directory */
|
|
114
|
+
dest: string;
|
|
115
|
+
/** Variables to pass to all templates */
|
|
116
|
+
vars: Record<string, unknown>;
|
|
117
|
+
/** File rename mappings (original -> new name, supports template vars) */
|
|
118
|
+
rename?: Record<string, string>;
|
|
119
|
+
/** Glob patterns to ignore */
|
|
120
|
+
ignore?: string[];
|
|
121
|
+
/** Content transformers by file extension or name */
|
|
122
|
+
transform?: Record<string, (content: string) => string>;
|
|
123
|
+
/** Templating engine to use (defaults to EJS) */
|
|
124
|
+
engine?: TemplatingEngine;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// =============================================================================
|
|
128
|
+
// String Rendering
|
|
129
|
+
// =============================================================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Render a template string with variables.
|
|
133
|
+
* @param engine - Templating engine to use (defaults to EJS)
|
|
134
|
+
*/
|
|
135
|
+
export const renderString = (
|
|
136
|
+
template: string,
|
|
137
|
+
vars: Record<string, unknown>,
|
|
138
|
+
engine: TemplatingEngine = ejsEngine,
|
|
139
|
+
): string => {
|
|
140
|
+
return engine.render(template, vars);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Render a template string asynchronously.
|
|
145
|
+
* @param engine - Templating engine to use (defaults to EJS)
|
|
146
|
+
*/
|
|
147
|
+
export const renderStringAsync = async (
|
|
148
|
+
template: string,
|
|
149
|
+
vars: Record<string, unknown>,
|
|
150
|
+
engine: TemplatingEngine = ejsEngine,
|
|
151
|
+
): Promise<string> => {
|
|
152
|
+
return engine.renderAsync(template, vars);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// File Rendering
|
|
157
|
+
// =============================================================================
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Render a template file with variables.
|
|
161
|
+
* @param engine - Templating engine to use (defaults to EJS)
|
|
162
|
+
*/
|
|
163
|
+
export const renderFile = async (
|
|
164
|
+
templatePath: string,
|
|
165
|
+
vars: Record<string, unknown>,
|
|
166
|
+
engine: TemplatingEngine = ejsEngine,
|
|
167
|
+
): Promise<string> => {
|
|
168
|
+
return engine.renderFile(templatePath, vars);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// =============================================================================
|
|
172
|
+
// Template Tasks
|
|
173
|
+
// =============================================================================
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Render a single template file to a destination.
|
|
177
|
+
*/
|
|
178
|
+
export const template = (options: TemplateOptions): Task<void> => {
|
|
179
|
+
const engine = options.engine ?? ejsEngine;
|
|
180
|
+
|
|
181
|
+
// Render destination path with variables
|
|
182
|
+
const destPath = renderString(options.dest, options.vars, engine);
|
|
183
|
+
const destDir = path.dirname(destPath);
|
|
184
|
+
|
|
185
|
+
return task(mkdir(destDir))
|
|
186
|
+
.chain(() => task(readFile(options.source)))
|
|
187
|
+
.map((content) => renderString(content, options.vars, engine))
|
|
188
|
+
.chain((rendered) => task(writeFile(destPath, rendered)))
|
|
189
|
+
.unwrap();
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Render a directory of templates to a destination.
|
|
194
|
+
*/
|
|
195
|
+
export const templateDir = (options: TemplateDirOptions): Task<void> => {
|
|
196
|
+
const engine = options.engine ?? ejsEngine;
|
|
197
|
+
|
|
198
|
+
return task(glob("**/*", options.source))
|
|
199
|
+
.chain((files) => {
|
|
200
|
+
const tasks = files
|
|
201
|
+
.filter((file) => {
|
|
202
|
+
// Filter out ignored patterns
|
|
203
|
+
if (options.ignore) {
|
|
204
|
+
return !options.ignore.some((pattern) => minimatch(file, pattern));
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
})
|
|
208
|
+
.map((file) => {
|
|
209
|
+
const sourcePath = path.join(options.source, file);
|
|
210
|
+
|
|
211
|
+
// Remove .ejs extension if present
|
|
212
|
+
let destFile = file.replace(/\.ejs$/, "");
|
|
213
|
+
|
|
214
|
+
// Apply renames
|
|
215
|
+
if (options.rename?.[destFile]) {
|
|
216
|
+
destFile = options.rename[destFile];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Render destination path with variables
|
|
220
|
+
destFile = renderString(destFile, options.vars, engine);
|
|
221
|
+
const destPath = path.join(options.dest, destFile);
|
|
222
|
+
|
|
223
|
+
return template({
|
|
224
|
+
source: sourcePath,
|
|
225
|
+
dest: destPath,
|
|
226
|
+
vars: options.vars,
|
|
227
|
+
engine,
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return task(sequence_(tasks));
|
|
232
|
+
})
|
|
233
|
+
.unwrap();
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Simple minimatch implementation for common patterns.
|
|
238
|
+
*/
|
|
239
|
+
const minimatch = (filepath: string, pattern: string): boolean => {
|
|
240
|
+
// Convert glob pattern to regex
|
|
241
|
+
const regex = pattern
|
|
242
|
+
.replace(/\./g, "\\.")
|
|
243
|
+
.replace(/\*\*/g, "<<GLOBSTAR>>")
|
|
244
|
+
.replace(/\*/g, "[^/]*")
|
|
245
|
+
.replace(/<<GLOBSTAR>>/g, ".*");
|
|
246
|
+
|
|
247
|
+
return new RegExp(`^${regex}$`).test(filepath);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// =============================================================================
|
|
251
|
+
// Template Helpers
|
|
252
|
+
// =============================================================================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Common template helpers that can be passed to templates.
|
|
256
|
+
*/
|
|
257
|
+
export const templateHelpers = {
|
|
258
|
+
/**
|
|
259
|
+
* Convert a string to camelCase.
|
|
260
|
+
*/
|
|
261
|
+
camelCase: (str: string): string => {
|
|
262
|
+
return str
|
|
263
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
|
|
264
|
+
.replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Convert a string to PascalCase.
|
|
269
|
+
*/
|
|
270
|
+
pascalCase: (str: string): string => {
|
|
271
|
+
const camel = templateHelpers.camelCase(str);
|
|
272
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Convert a string to kebab-case.
|
|
277
|
+
*/
|
|
278
|
+
kebabCase: (str: string): string => {
|
|
279
|
+
return str
|
|
280
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
281
|
+
.replace(/[\s_]+/g, "-")
|
|
282
|
+
.toLowerCase();
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Convert a string to snake_case.
|
|
287
|
+
*/
|
|
288
|
+
snakeCase: (str: string): string => {
|
|
289
|
+
return str
|
|
290
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
291
|
+
.replace(/[\s-]+/g, "_")
|
|
292
|
+
.toLowerCase();
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Convert a string to CONSTANT_CASE.
|
|
297
|
+
*/
|
|
298
|
+
constantCase: (str: string): string => {
|
|
299
|
+
return templateHelpers.snakeCase(str).toUpperCase();
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Capitalize the first letter of a string.
|
|
304
|
+
*/
|
|
305
|
+
capitalize: (str: string): string => {
|
|
306
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get the current date in ISO format.
|
|
311
|
+
*/
|
|
312
|
+
isoDate: (): string => new Date().toISOString(),
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get the current year.
|
|
316
|
+
*/
|
|
317
|
+
year: (): number => new Date().getFullYear(),
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Indent a multi-line string.
|
|
321
|
+
*/
|
|
322
|
+
indent: (str: string, spaces: number): string => {
|
|
323
|
+
const pad = " ".repeat(spaces);
|
|
324
|
+
return str
|
|
325
|
+
.split("\n")
|
|
326
|
+
.map((line) => pad + line)
|
|
327
|
+
.join("\n");
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Join array items with a separator.
|
|
332
|
+
*/
|
|
333
|
+
join: (arr: unknown[], separator = ", "): string => {
|
|
334
|
+
return arr.map(String).join(separator);
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Pluralize a word based on count.
|
|
339
|
+
*/
|
|
340
|
+
pluralize: (word: string, count: number): string => {
|
|
341
|
+
return count === 1 ? word : `${word}s`;
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Create a vars object with helpers included.
|
|
347
|
+
*/
|
|
348
|
+
export const withHelpers = (
|
|
349
|
+
vars: Record<string, unknown>,
|
|
350
|
+
): Record<string, unknown> => ({
|
|
351
|
+
...templateHelpers,
|
|
352
|
+
...vars,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// =============================================================================
|
|
356
|
+
// Generator Metadata Comment
|
|
357
|
+
// =============================================================================
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Generate a metadata comment for generated files.
|
|
361
|
+
*/
|
|
362
|
+
export const generatorComment = (
|
|
363
|
+
generatorName: string,
|
|
364
|
+
options?: {
|
|
365
|
+
version?: string;
|
|
366
|
+
timestamp?: boolean;
|
|
367
|
+
},
|
|
368
|
+
): string => {
|
|
369
|
+
const parts = [`Generated by ${generatorName}`];
|
|
370
|
+
|
|
371
|
+
if (options?.version) {
|
|
372
|
+
parts.push(`v${options.version}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (options?.timestamp) {
|
|
376
|
+
parts.push(`on ${new Date().toISOString()}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return `// ${parts.join(" ")}`;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Generate a metadata comment as an HTML comment.
|
|
384
|
+
*/
|
|
385
|
+
export const generatorHtmlComment = (
|
|
386
|
+
generatorName: string,
|
|
387
|
+
options?: {
|
|
388
|
+
version?: string;
|
|
389
|
+
timestamp?: boolean;
|
|
390
|
+
},
|
|
391
|
+
): string => {
|
|
392
|
+
const parts = [`Generated by ${generatorName}`];
|
|
393
|
+
|
|
394
|
+
if (options?.version) {
|
|
395
|
+
parts.push(`v${options.version}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (options?.timestamp) {
|
|
399
|
+
parts.push(`on ${new Date().toISOString()}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return `<!-- ${parts.join(" ")} -->`;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// =============================================================================
|
|
406
|
+
// Generated File Stamp
|
|
407
|
+
// =============================================================================
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Comment style configuration for different file types.
|
|
411
|
+
*/
|
|
412
|
+
interface CommentStyle {
|
|
413
|
+
/** Single line comment prefix (e.g., "//") */
|
|
414
|
+
single?: string;
|
|
415
|
+
/** Block comment start (e.g., slash-star) */
|
|
416
|
+
blockStart?: string;
|
|
417
|
+
/** Block comment end (e.g., star-slash) */
|
|
418
|
+
blockEnd?: string;
|
|
419
|
+
/** Use block style even for single line (e.g., for CSS) */
|
|
420
|
+
preferBlock?: boolean;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Map of file extensions to comment styles.
|
|
425
|
+
*/
|
|
426
|
+
const COMMENT_STYLES: Record<string, CommentStyle> = {
|
|
427
|
+
// JavaScript/TypeScript family
|
|
428
|
+
".ts": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
429
|
+
".tsx": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
430
|
+
".js": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
431
|
+
".jsx": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
432
|
+
".mjs": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
433
|
+
".cjs": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
434
|
+
|
|
435
|
+
// CSS family (prefer block comments)
|
|
436
|
+
".css": { blockStart: "/*", blockEnd: "*/", preferBlock: true },
|
|
437
|
+
".scss": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
438
|
+
".sass": { single: "//" },
|
|
439
|
+
".less": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
440
|
+
|
|
441
|
+
// HTML/XML family
|
|
442
|
+
".html": { blockStart: "<!--", blockEnd: "-->" },
|
|
443
|
+
".htm": { blockStart: "<!--", blockEnd: "-->" },
|
|
444
|
+
".xml": { blockStart: "<!--", blockEnd: "-->" },
|
|
445
|
+
".svg": { blockStart: "<!--", blockEnd: "-->" },
|
|
446
|
+
".vue": { blockStart: "<!--", blockEnd: "-->" },
|
|
447
|
+
".svelte": { blockStart: "<!--", blockEnd: "-->" },
|
|
448
|
+
|
|
449
|
+
// Config files
|
|
450
|
+
".json": {}, // JSON doesn't support comments - skip stamp
|
|
451
|
+
".yaml": { single: "#" },
|
|
452
|
+
".yml": { single: "#" },
|
|
453
|
+
".toml": { single: "#" },
|
|
454
|
+
|
|
455
|
+
// Shell/scripting
|
|
456
|
+
".sh": { single: "#" },
|
|
457
|
+
".bash": { single: "#" },
|
|
458
|
+
".zsh": { single: "#" },
|
|
459
|
+
".fish": { single: "#" },
|
|
460
|
+
".py": { single: "#" },
|
|
461
|
+
".rb": { single: "#" },
|
|
462
|
+
".pl": { single: "#" },
|
|
463
|
+
|
|
464
|
+
// Other languages
|
|
465
|
+
".go": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
466
|
+
".rs": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
467
|
+
".java": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
468
|
+
".kt": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
469
|
+
".swift": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
470
|
+
".c": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
471
|
+
".cpp": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
472
|
+
".h": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
473
|
+
".hpp": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
474
|
+
".php": { single: "//", blockStart: "/*", blockEnd: "*/" },
|
|
475
|
+
|
|
476
|
+
// Documentation
|
|
477
|
+
".md": { blockStart: "<!--", blockEnd: "-->" },
|
|
478
|
+
".mdx": { blockStart: "{/*", blockEnd: "*/}" },
|
|
479
|
+
|
|
480
|
+
// SQL
|
|
481
|
+
".sql": { single: "--", blockStart: "/*", blockEnd: "*/" },
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Get the comment style for a given file path.
|
|
486
|
+
*/
|
|
487
|
+
export const getCommentStyle = (filePath: string): CommentStyle | null => {
|
|
488
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
489
|
+
return COMMENT_STYLES[ext] ?? null;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Generate a stamp comment for a generated file.
|
|
494
|
+
* Returns null if the file type doesn't support comments.
|
|
495
|
+
*/
|
|
496
|
+
export const generateStamp = (
|
|
497
|
+
filePath: string,
|
|
498
|
+
options: StampOptions,
|
|
499
|
+
): string | null => {
|
|
500
|
+
const style = getCommentStyle(filePath);
|
|
501
|
+
if (!style) return null;
|
|
502
|
+
|
|
503
|
+
// Skip if no comment syntax available
|
|
504
|
+
if (!style.single && !style.blockStart) return null;
|
|
505
|
+
|
|
506
|
+
const stampText = `Generated by ${options.generator} v${options.version}`;
|
|
507
|
+
|
|
508
|
+
// Use block style if preferred or single not available
|
|
509
|
+
if (style.preferBlock || (!style.single && style.blockStart)) {
|
|
510
|
+
return `${style.blockStart} ${stampText} ${style.blockEnd}`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Use single line style
|
|
514
|
+
if (style.single) {
|
|
515
|
+
return `${style.single} ${stampText}`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return null;
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Prepend a stamp to file content.
|
|
523
|
+
* Handles shebang lines (#!/...) by placing stamp after them.
|
|
524
|
+
*/
|
|
525
|
+
export const prependStamp = (content: string, stamp: string): string => {
|
|
526
|
+
// Check for shebang
|
|
527
|
+
if (content.startsWith("#!")) {
|
|
528
|
+
const firstNewline = content.indexOf("\n");
|
|
529
|
+
if (firstNewline !== -1) {
|
|
530
|
+
const shebang = content.slice(0, firstNewline + 1);
|
|
531
|
+
const rest = content.slice(firstNewline + 1);
|
|
532
|
+
return `${shebang}${stamp}\n${rest}`;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return `${stamp}\n${content}`;
|
|
537
|
+
};
|