@shrkcrft/templates 0.1.0-alpha.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SharkCraft contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @shrkcrft/templates
2
+
3
+ SharkCraft templates: typed template definitions, registry, variable validation, rendering.
4
+
5
+ Part of [SharkCraft](https://github.com/shrkcrft/sharkcraft) — a deterministic, local-first toolkit that gives AI coding agents durable project context. See the main repo for documentation, examples, and the `shrk` CLI.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add @shrkcrft/templates
11
+ ```
12
+
13
+ ## License
14
+
15
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,8 @@
1
+ export * from './template-definition.js';
2
+ export * from './template-variable.js';
3
+ export * from './template-registry.js';
4
+ export * from './template-renderer.js';
5
+ export * from './template-preview.js';
6
+ export * from './template-loader.js';
7
+ export * from './target-path-resolver.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export * from "./template-definition.js";
2
+ export * from "./template-variable.js";
3
+ export * from "./template-registry.js";
4
+ export * from "./template-renderer.js";
5
+ export * from "./template-preview.js";
6
+ export * from "./template-loader.js";
7
+ export * from "./target-path-resolver.js";
@@ -0,0 +1,9 @@
1
+ import type { ITemplateDefinition } from './template-definition.js';
2
+ import type { TemplateVariableValues } from './template-variable.js';
3
+ export interface IResolvedTargetPath {
4
+ rawPath: string;
5
+ absolutePath: string;
6
+ isInsideProject: boolean;
7
+ }
8
+ export declare function resolveTargetPath(template: ITemplateDefinition, values: TemplateVariableValues, projectRoot: string): IResolvedTargetPath | null;
9
+ //# sourceMappingURL=target-path-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"target-path-resolver.d.ts","sourceRoot":"","sources":["../src/target-path-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAGrE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE,sBAAsB,EAC9B,WAAW,EAAE,MAAM,GAClB,mBAAmB,GAAG,IAAI,CAW5B"}
@@ -0,0 +1,15 @@
1
+ import { isAbsolutePath, isPathInside, joinPath, normalizePath } from '@shrkcrft/core';
2
+ export function resolveTargetPath(template, values, projectRoot) {
3
+ if (!template.targetPath)
4
+ return null;
5
+ const raw = typeof template.targetPath === 'function' ? template.targetPath(values) : template.targetPath;
6
+ if (!raw)
7
+ return null;
8
+ const normalized = normalizePath(raw);
9
+ const absolutePath = isAbsolutePath(normalized) ? normalized : joinPath(projectRoot, normalized);
10
+ return {
11
+ rawPath: raw,
12
+ absolutePath,
13
+ isInsideProject: isPathInside(absolutePath, projectRoot) || absolutePath === projectRoot,
14
+ };
15
+ }
@@ -0,0 +1,169 @@
1
+ import type { ITemplateVariable, TemplateVariableValues } from './template-variable.js';
2
+ /**
3
+ * Subset of @shrkcrft/generator's `IPlannedChange` re-declared here to
4
+ * avoid a generator → templates → generator cycle. The two shapes are kept
5
+ * structurally equal by `IPlannedChange` consumers.
6
+ */
7
+ export interface ITemplateChange {
8
+ targetPath: string;
9
+ operation: {
10
+ kind: 'create';
11
+ content: string;
12
+ description?: string;
13
+ } | {
14
+ kind: 'append';
15
+ snippet: string;
16
+ ifMissing?: string;
17
+ description?: string;
18
+ } | {
19
+ kind: 'insert-after';
20
+ anchor: string;
21
+ snippet: string;
22
+ ifMissing?: string;
23
+ description?: string;
24
+ } | {
25
+ kind: 'insert-before';
26
+ anchor: string;
27
+ snippet: string;
28
+ ifMissing?: string;
29
+ description?: string;
30
+ } | {
31
+ kind: 'replace';
32
+ find: string;
33
+ replaceWith: string;
34
+ expectMatches?: number;
35
+ description?: string;
36
+ } | {
37
+ kind: 'export';
38
+ from: string;
39
+ symbols?: readonly string[];
40
+ ifMissing?: string;
41
+ description?: string;
42
+ } | {
43
+ kind: 'ensure-import';
44
+ from: string;
45
+ symbols?: readonly string[];
46
+ typeOnly?: boolean;
47
+ defaultBinding?: string;
48
+ namespaceBinding?: string;
49
+ description?: string;
50
+ } | {
51
+ kind: 'insert-enum-entry';
52
+ enumName: string;
53
+ entryName: string;
54
+ entryValue: string;
55
+ description?: string;
56
+ } | {
57
+ kind: 'insert-object-entry';
58
+ objectName: string;
59
+ entryKey: string;
60
+ entryValue: string;
61
+ shorthand?: boolean;
62
+ description?: string;
63
+ } | {
64
+ kind: 'insert-before-closing-brace';
65
+ containerName: string;
66
+ snippet: string;
67
+ ifMissing?: string;
68
+ description?: string;
69
+ } | {
70
+ kind: 'insert-between-anchors';
71
+ beginAnchor: string;
72
+ endAnchor: string;
73
+ snippet: string;
74
+ ifMissing?: string;
75
+ description?: string;
76
+ };
77
+ }
78
+ /**
79
+ * Anchor metadata declared by a template.
80
+ *
81
+ * Templates can declare:
82
+ * - `producedAnchors`: anchor strings that the template body GUARANTEES to
83
+ * produce (e.g. `// region:plugin-list`). Template drift verifies these
84
+ * anchors appear in the rendered output.
85
+ * - `requiredAnchors`: anchor strings that the template REQUIRES in its
86
+ * target files (for update ops). Self-config doctor / template drift
87
+ * verify that some other template or scaffold provides them; missing
88
+ * anchors surface as actionable diagnostics.
89
+ */
90
+ export interface ITemplateAnchorDeclaration {
91
+ /** Anchor literal as it must appear in the file. */
92
+ anchor: string;
93
+ /** Where the anchor lives (file glob or target path). */
94
+ in: string;
95
+ /** Why the template needs / produces this anchor. */
96
+ purpose?: string;
97
+ /** Operation kinds that use this anchor (informational). */
98
+ usedBy?: ReadonlyArray<'insert-after' | 'insert-before' | 'insert-between-anchors' | 'insert-before-closing-brace' | 'insert-enum-entry' | 'insert-object-entry'>;
99
+ }
100
+ export interface ITemplateFile {
101
+ /** Final file path relative to project root. */
102
+ targetPath: string;
103
+ /** File contents. */
104
+ content: string;
105
+ /** Optional MIME or hint, e.g. "typescript". */
106
+ language?: string;
107
+ /** Default: false (do not overwrite if exists). */
108
+ overwrite?: boolean;
109
+ }
110
+ export type TargetPathResolver = string | ((values: TemplateVariableValues) => string);
111
+ export type ContentResolver = string | ((values: TemplateVariableValues) => string);
112
+ export type FilesResolver = (values: TemplateVariableValues) => ITemplateFile[];
113
+ export type ChangesResolver = (values: TemplateVariableValues) => ITemplateChange[];
114
+ export interface ITemplateDefinition {
115
+ id: string;
116
+ name: string;
117
+ description: string;
118
+ tags: readonly string[];
119
+ scope: readonly string[];
120
+ appliesWhen: readonly string[];
121
+ variables: readonly ITemplateVariable[];
122
+ /** Single-file template: target path. */
123
+ targetPath?: TargetPathResolver;
124
+ /** Single-file template: content. */
125
+ content?: ContentResolver;
126
+ /**
127
+ * Multi-file template: file factory.
128
+ * Each returned entry becomes a CREATE planned change.
129
+ */
130
+ files?: FilesResolver;
131
+ /**
132
+ * v2 template: mixed CREATE / UPDATE planned changes.
133
+ * Templates may declare both `files()` and `changes()`; the rendered set is
134
+ * the concatenation, with `files()` entries normalised to CREATE operations.
135
+ */
136
+ changes?: ChangesResolver;
137
+ /** Post-generation notes shown to the user. */
138
+ postGenerationNotes?: readonly string[];
139
+ /** Related knowledge entry IDs. */
140
+ related?: readonly string[];
141
+ /**
142
+ * Optional template profile metadata used by template-drift,
143
+ * scaffold-coverage, and the `shrk task` recommender. All fields are
144
+ * informational; the renderer ignores them. Generic engine code never
145
+ * encodes project-specific values here.
146
+ */
147
+ metadata?: {
148
+ /** Files that must NOT be produced by this template (regex fragments). */
149
+ forbiddenPathFragments?: readonly string[];
150
+ /** Plugin lifecycle profile ids this template depends on. */
151
+ requiredProfileIds?: readonly string[];
152
+ /** Convention ids this template's outputs are expected to satisfy. */
153
+ requiredConventionIds?: readonly string[];
154
+ /** Helper ids that complete the workflow around this template. */
155
+ requiredHelperIds?: readonly string[];
156
+ /** Language profiles required for this template to make sense. */
157
+ requiredLanguages?: readonly string[];
158
+ /** Framework profiles required for this template to make sense. */
159
+ requiredFrameworks?: readonly string[];
160
+ /** Anchors the template body GUARANTEES to produce. */
161
+ producedAnchors?: readonly ITemplateAnchorDeclaration[];
162
+ /** Anchors the template REQUIRES to exist in its target files. */
163
+ requiredAnchors?: readonly ITemplateAnchorDeclaration[];
164
+ /** Optional ids of registration hints applicable to this template. */
165
+ registrationHintIds?: readonly string[];
166
+ };
167
+ }
168
+ export declare function defineTemplate(input: ITemplateDefinition): ITemplateDefinition;
169
+ //# sourceMappingURL=template-definition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-definition.d.ts","sourceRoot":"","sources":["../src/template-definition.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export function defineTemplate(input) {
2
+ if (!input.id)
3
+ throw new Error("defineTemplate: 'id' is required");
4
+ if (!input.name)
5
+ throw new Error(`defineTemplate: 'name' is required for ${input.id}`);
6
+ if (!input.files && !input.changes && !(input.targetPath && input.content)) {
7
+ throw new Error(`defineTemplate: ${input.id} must provide either 'files', 'changes', or both 'targetPath' and 'content'`);
8
+ }
9
+ return {
10
+ ...input,
11
+ tags: Object.freeze([...input.tags]),
12
+ scope: Object.freeze([...input.scope]),
13
+ appliesWhen: Object.freeze([...input.appliesWhen]),
14
+ variables: Object.freeze([...input.variables]),
15
+ postGenerationNotes: input.postGenerationNotes
16
+ ? Object.freeze([...input.postGenerationNotes])
17
+ : undefined,
18
+ related: input.related ? Object.freeze([...input.related]) : undefined,
19
+ };
20
+ }
@@ -0,0 +1,12 @@
1
+ import { type IImportContext } from '@shrkcrft/core';
2
+ import type { ITemplateDefinition } from './template-definition.js';
3
+ export interface ILoadedTemplates {
4
+ templates: ITemplateDefinition[];
5
+ warnings: string[];
6
+ sourceFiles: string[];
7
+ }
8
+ export interface ILoadTemplatesOptions {
9
+ importContext?: IImportContext;
10
+ }
11
+ export declare function loadTemplatesFromFile(filePath: string, options?: ILoadTemplatesOptions): Promise<ILoadedTemplates>;
12
+ //# sourceMappingURL=template-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-loader.d.ts","sourceRoot":"","sources":["../src/template-loader.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,gBAAgB,CAAC;AACjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAQpE,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,mBAAmB,EAAE,CAAC;IACjC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,CAAC,EAAE,cAAc,CAAC;CAChC;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAuC3B"}
@@ -0,0 +1,48 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { safeImport } from '@shrkcrft/core';
3
+ function isTemplate(value) {
4
+ if (!value || typeof value !== 'object')
5
+ return false;
6
+ const v = value;
7
+ return typeof v.id === 'string' && typeof v.name === 'string';
8
+ }
9
+ export async function loadTemplatesFromFile(filePath, options = {}) {
10
+ const warnings = [];
11
+ const templates = [];
12
+ const sourceFiles = [];
13
+ if (!existsSync(filePath)) {
14
+ warnings.push(`Template file not found: ${filePath}`);
15
+ return { templates, warnings, sourceFiles };
16
+ }
17
+ sourceFiles.push(filePath);
18
+ const result = options.importContext
19
+ ? await options.importContext.load(filePath)
20
+ : await safeImport(filePath, { skipExistsCheck: true });
21
+ if (!result.ok) {
22
+ const label = result.timedOut ? 'timed out importing' : 'Failed to import';
23
+ warnings.push(`${label} ${filePath}: ${result.error.message}`);
24
+ return { templates, warnings, sourceFiles };
25
+ }
26
+ const seen = new Set();
27
+ const tryPush = (v) => {
28
+ if (!isTemplate(v))
29
+ return;
30
+ if (seen.has(v.id))
31
+ return;
32
+ seen.add(v.id);
33
+ templates.push(v);
34
+ };
35
+ for (const key of Object.keys(result.module)) {
36
+ const v = result.module[key];
37
+ if (isTemplate(v)) {
38
+ tryPush(v);
39
+ }
40
+ else if (Array.isArray(v)) {
41
+ for (const item of v)
42
+ tryPush(item);
43
+ }
44
+ }
45
+ if (templates.length === 0)
46
+ warnings.push(`No templates exported by ${filePath}`);
47
+ return { templates, warnings, sourceFiles };
48
+ }
@@ -0,0 +1,10 @@
1
+ import type { IRenderedTemplate } from './template-renderer.js';
2
+ import type { ITemplateDefinition } from './template-definition.js';
3
+ import { type TemplateVariableValues, type IVariableValidationResult } from './template-variable.js';
4
+ export interface ITemplatePreview {
5
+ template: ITemplateDefinition;
6
+ validation: IVariableValidationResult;
7
+ rendered: IRenderedTemplate | null;
8
+ }
9
+ export declare function previewTemplate(template: ITemplateDefinition, values: TemplateVariableValues): ITemplatePreview;
10
+ //# sourceMappingURL=template-preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-preview.d.ts","sourceRoot":"","sources":["../src/template-preview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC/B,MAAM,wBAAwB,CAAC;AAGhC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE,yBAAyB,CAAC;IACtC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACpC;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE,sBAAsB,GAC7B,gBAAgB,CAOlB"}
@@ -0,0 +1,10 @@
1
+ import { validateTemplateVariables, } from "./template-variable.js";
2
+ import { renderTemplate } from "./template-renderer.js";
3
+ export function previewTemplate(template, values) {
4
+ const validation = validateTemplateVariables(template.variables, values);
5
+ if (!validation.valid) {
6
+ return { template, validation, rendered: null };
7
+ }
8
+ const rendered = renderTemplate(template, validation.resolved);
9
+ return { template, validation, rendered };
10
+ }
@@ -0,0 +1,11 @@
1
+ import type { ITemplateDefinition } from './template-definition.js';
2
+ export declare class TemplateRegistry {
3
+ private readonly byId;
4
+ constructor(templates?: readonly ITemplateDefinition[]);
5
+ register(template: ITemplateDefinition): void;
6
+ get(id: string): ITemplateDefinition | null;
7
+ has(id: string): boolean;
8
+ list(): ITemplateDefinition[];
9
+ search(query: string): ITemplateDefinition[];
10
+ }
11
+ //# sourceMappingURL=template-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-registry.d.ts","sourceRoot":"","sources":["../src/template-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAEpE,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0C;gBAEnD,SAAS,GAAE,SAAS,mBAAmB,EAAO;IAI1D,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAI7C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAI3C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxB,IAAI,IAAI,mBAAmB,EAAE;IAI7B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,mBAAmB,EAAE;CAc7C"}
@@ -0,0 +1,32 @@
1
+ export class TemplateRegistry {
2
+ byId = new Map();
3
+ constructor(templates = []) {
4
+ for (const t of templates)
5
+ this.register(t);
6
+ }
7
+ register(template) {
8
+ this.byId.set(template.id, template);
9
+ }
10
+ get(id) {
11
+ return this.byId.get(id) ?? null;
12
+ }
13
+ has(id) {
14
+ return this.byId.has(id);
15
+ }
16
+ list() {
17
+ return [...this.byId.values()];
18
+ }
19
+ search(query) {
20
+ const q = query.trim().toLowerCase();
21
+ if (!q)
22
+ return this.list();
23
+ return this.list().filter((t) => {
24
+ return (t.id.toLowerCase().includes(q) ||
25
+ t.name.toLowerCase().includes(q) ||
26
+ t.description.toLowerCase().includes(q) ||
27
+ t.tags.some((tag) => tag.toLowerCase().includes(q)) ||
28
+ t.scope.some((s) => s.toLowerCase().includes(q)) ||
29
+ t.appliesWhen.some((a) => a.toLowerCase().includes(q)));
30
+ });
31
+ }
32
+ }
@@ -0,0 +1,19 @@
1
+ import type { ITemplateChange, ITemplateDefinition, ITemplateFile } from './template-definition.js';
2
+ import type { TemplateVariableValues } from './template-variable.js';
3
+ export interface IRenderedTemplate {
4
+ templateId: string;
5
+ /**
6
+ * CREATE-only files (legacy `files()` callback). Kept for v1 callers; the
7
+ * generator package normalises these to CREATE-kind planned changes.
8
+ */
9
+ files: ITemplateFile[];
10
+ /**
11
+ * v2 planned changes (mixed CREATE/UPDATE). `files()` results are NOT
12
+ * duplicated here — consumers handle both lists, treating `files` as the
13
+ * CREATE-only legacy surface.
14
+ */
15
+ changes: ITemplateChange[];
16
+ postGenerationNotes: readonly string[];
17
+ }
18
+ export declare function renderTemplate(template: ITemplateDefinition, values: TemplateVariableValues): IRenderedTemplate;
19
+ //# sourceMappingURL=template-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-renderer.d.ts","sourceRoot":"","sources":["../src/template-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,aAAa,EACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAErE,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB;;;;OAIG;IACH,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE,sBAAsB,GAC7B,iBAAiB,CAiCnB"}
@@ -0,0 +1,30 @@
1
+ export function renderTemplate(template, values) {
2
+ const files = [];
3
+ const changes = [];
4
+ if (template.files) {
5
+ for (const f of template.files(values)) {
6
+ files.push({
7
+ targetPath: f.targetPath,
8
+ content: f.content,
9
+ language: f.language,
10
+ overwrite: f.overwrite ?? false,
11
+ });
12
+ }
13
+ }
14
+ else if (template.targetPath && template.content) {
15
+ const target = typeof template.targetPath === 'function' ? template.targetPath(values) : template.targetPath;
16
+ const content = typeof template.content === 'function' ? template.content(values) : template.content;
17
+ files.push({ targetPath: target, content, overwrite: false });
18
+ }
19
+ if (template.changes) {
20
+ for (const c of template.changes(values)) {
21
+ changes.push(c);
22
+ }
23
+ }
24
+ return {
25
+ templateId: template.id,
26
+ files,
27
+ changes,
28
+ postGenerationNotes: template.postGenerationNotes ?? [],
29
+ };
30
+ }
@@ -0,0 +1,23 @@
1
+ export interface ITemplateVariable {
2
+ name: string;
3
+ description?: string;
4
+ required?: boolean;
5
+ default?: string;
6
+ pattern?: RegExp;
7
+ /** Optional choices, e.g. ['ts', 'tsx']. */
8
+ choices?: readonly string[];
9
+ /** Example values to surface in help text and error messages. */
10
+ examples?: readonly string[];
11
+ }
12
+ export type TemplateVariableValues = Record<string, string>;
13
+ export interface IVariableValidationIssue {
14
+ variable: string;
15
+ message: string;
16
+ }
17
+ export interface IVariableValidationResult {
18
+ valid: boolean;
19
+ issues: IVariableValidationIssue[];
20
+ resolved: TemplateVariableValues;
21
+ }
22
+ export declare function validateTemplateVariables(variables: readonly ITemplateVariable[], values: Readonly<TemplateVariableValues>): IVariableValidationResult;
23
+ //# sourceMappingURL=template-variable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-variable.d.ts","sourceRoot":"","sources":["../src/template-variable.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,MAAM,sBAAsB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE5D,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,wBAAwB,EAAE,CAAC;IACnC,QAAQ,EAAE,sBAAsB,CAAC;CAClC;AAED,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,SAAS,iBAAiB,EAAE,EACvC,MAAM,EAAE,QAAQ,CAAC,sBAAsB,CAAC,GACvC,yBAAyB,CAgC3B"}
@@ -0,0 +1,32 @@
1
+ export function validateTemplateVariables(variables, values) {
2
+ const issues = [];
3
+ const resolved = {};
4
+ for (const v of variables) {
5
+ let provided = values[v.name];
6
+ if ((provided === undefined || provided === '') && v.default !== undefined) {
7
+ provided = v.default;
8
+ }
9
+ if (provided === undefined || provided === '') {
10
+ if (v.required) {
11
+ issues.push({ variable: v.name, message: `Variable '${v.name}' is required` });
12
+ }
13
+ continue;
14
+ }
15
+ if (v.pattern && !v.pattern.test(provided)) {
16
+ issues.push({
17
+ variable: v.name,
18
+ message: `Variable '${v.name}' does not match pattern ${v.pattern.source}`,
19
+ });
20
+ continue;
21
+ }
22
+ if (v.choices && !v.choices.includes(provided)) {
23
+ issues.push({
24
+ variable: v.name,
25
+ message: `Variable '${v.name}' must be one of: ${v.choices.join(', ')}`,
26
+ });
27
+ continue;
28
+ }
29
+ resolved[v.name] = provided;
30
+ }
31
+ return { valid: issues.length === 0, issues, resolved };
32
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@shrkcrft/templates",
3
+ "version": "0.1.0-alpha.2",
4
+ "description": "SharkCraft templates: typed template definitions, registry, variable validation, rendering.",
5
+ "license": "MIT",
6
+ "author": "SharkCraft contributors",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "bun": "./src/index.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/shrkcrft/sharkcraft.git",
26
+ "directory": "packages/templates"
27
+ },
28
+ "homepage": "https://github.com/shrkcrft/sharkcraft",
29
+ "bugs": {
30
+ "url": "https://github.com/shrkcrft/sharkcraft/issues"
31
+ },
32
+ "keywords": [
33
+ "sharkcraft",
34
+ "templates",
35
+ "generator",
36
+ "codegen"
37
+ ],
38
+ "engines": {
39
+ "bun": ">=1.1.0",
40
+ "node": ">=18"
41
+ },
42
+ "scripts": {
43
+ "typecheck": "tsc --noEmit -p tsconfig.json"
44
+ },
45
+ "dependencies": {
46
+ "@shrkcrft/core": "^0.1.0-alpha.2"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }