@jakubmazanec/template 1.0.3 → 1.1.0-next.5d8abeb

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 CHANGED
@@ -28,6 +28,7 @@ Write templates using [EJS](https://ejs.co/) with YAML front matter:
28
28
 
29
29
  ```
30
30
  ---
31
+ extends: './base.ejs'
31
32
  to: <%- attributes.variables.path %>
32
33
  if: <%- attributes.variables.enabled %>
33
34
  variables:
@@ -90,6 +91,11 @@ Because `variables` is an array, we can expect two renders; but because one of t
90
91
  `enabled` is used for rendering the value of `if` attribute and is set to `false` in the second
91
92
  case, only one render is actually returned.
92
93
 
94
+ You can extend another template using `extends` attribute that represents path to the template to
95
+ extend, relative to the current template. The extended template is read using the same options
96
+ object. The current template attributes are recursively merged with the extended template
97
+ attributes. If the current template has no content, the content from the extended template is used.
98
+
93
99
  ## Documentation
94
100
 
95
101
  See [API reference](./docs) for auto-generated documentation.
@@ -67,7 +67,7 @@ export declare class Template<A extends AnyZodObject | undefined = undefined, D
67
67
  readonly attributesSchema: A extends AnyZodObject ? A : null;
68
68
  /** Zod schema for template data. */
69
69
  readonly dataSchema: D extends AnyZodObject ? D : null;
70
- constructor({ path, content, attributes, attributesSchema, dataSchema }: TemplateOptions<A, D>);
70
+ constructor({ path: templatePath, content, attributes, attributesSchema, dataSchema, }: TemplateOptions<A, D>);
71
71
  /**
72
72
  * Reads a template from a file.
73
73
  *
package/dist/Template.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import ejs from 'ejs';
2
2
  import fs from 'fs-extra';
3
3
  import matter from 'gray-matter';
4
+ import merge from 'lodash.merge';
4
5
  import omit from 'lodash.omit';
6
+ import path from 'node:path';
5
7
  import prettier from 'prettier';
6
8
  import yaml from 'yaml';
7
9
  import { NO_INFERRED_PARSER_REGEXP } from './constants.js';
@@ -31,9 +33,9 @@ import { TemplateError } from './TemplateError.js';
31
33
  /** Template function. */ fn;
32
34
  /** Zod schema for custom template attributes. */ attributesSchema;
33
35
  /** Zod schema for template data. */ dataSchema;
34
- constructor({ path, content, attributes, attributesSchema, dataSchema }){
35
- if (path) {
36
- this.path = path;
36
+ constructor({ path: templatePath, content, attributes, attributesSchema, dataSchema }){
37
+ if (templatePath) {
38
+ this.path = templatePath;
37
39
  }
38
40
  this.content = content;
39
41
  this.attributes = attributes;
@@ -54,9 +56,19 @@ import { TemplateError } from './TemplateError.js';
54
56
  *
55
57
  * @param templatePath Path to the template file.
56
58
  */ static async read(templatePath, options) {
57
- let { data: attributes, content } = matter(await fs.readFile(templatePath, 'utf8'));
59
+ let { data: rawAttributes, content } = matter(await fs.readFile(templatePath, 'utf8'));
60
+ // extending another template
61
+ if (typeof rawAttributes.extends === 'string' && rawAttributes.extends) {
62
+ let extendedTemplate = await Template.read(path.join(path.dirname(templatePath), rawAttributes.extends), options); // you can point to any template, but it will be parsed with the same options, including attribute or data schema
63
+ // attributes are recursively merged
64
+ rawAttributes = merge(extendedTemplate.attributes, rawAttributes);
65
+ // if content is empty, use content from the extended template
66
+ if (!content.trim()) {
67
+ content = extendedTemplate.content;
68
+ }
69
+ }
58
70
  let attributesSchema = options?.attributesSchema ? templateAttributesSchema.merge(options.attributesSchema) : templateAttributesSchema;
59
- let attributesParseResult = attributesSchema.safeParse(attributes);
71
+ let attributesParseResult = attributesSchema.safeParse(rawAttributes);
60
72
  if (!attributesParseResult.success) {
61
73
  throw new TemplateError('INVALID_ATTRIBUTES', {
62
74
  cause: attributesParseResult.error,
@@ -66,6 +78,7 @@ import { TemplateError } from './TemplateError.js';
66
78
  }
67
79
  });
68
80
  }
81
+ let attributes = attributesParseResult.data;
69
82
  return new this({
70
83
  path: templatePath,
71
84
  attributes,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/Template.ts"],"sourcesContent":["import ejs from 'ejs';\nimport fs from 'fs-extra';\nimport matter from 'gray-matter';\nimport omit from 'lodash.omit';\nimport prettier from 'prettier';\nimport yaml from 'yaml';\nimport {type z, type AnyZodObject} from 'zod';\n\nimport {NO_INFERRED_PARSER_REGEXP} from './constants.js';\nimport {templateAttributesSchema, type TemplateAttributes} from './TemplateAttributes.js';\nimport {TemplateError} from './TemplateError.js';\nimport {type TemplateFunction} from './TemplateFunction.js';\nimport {type TemplateRender} from './TemplateRender.js';\nimport {type TemplateRenders} from './TemplateRenders.js';\n\nexport type TemplateOptions<\n A extends AnyZodObject | undefined = undefined,\n D extends AnyZodObject | undefined = undefined,\n> = {\n /** Template path. */\n path?: string;\n\n /** Template content. */\n content: string;\n\n /** Template attributes. */\n attributes: A extends undefined\n ? TemplateAttributes\n : TemplateAttributes & z.infer<NonNullable<A>>;\n\n /** Zod schema for custom template attributes. */\n attributesSchema?: A | undefined;\n\n /** Zod schema for template data. */\n dataSchema?: D | undefined;\n};\n\nexport type TemplateReadOptions<\n A extends AnyZodObject | undefined = undefined,\n D extends AnyZodObject | undefined = undefined,\n> = {\n /** Zod schema for custom template attributes. */\n attributesSchema?: A | undefined;\n\n /** Zod schema for template data. */\n dataSchema?: D | undefined;\n};\n\nexport type TemplateReadAndRenderOptions<\n A extends AnyZodObject | undefined = undefined,\n D extends AnyZodObject | undefined = undefined,\n> = {\n /** Prettier options. */\n prettierOptions?: prettier.Options | undefined;\n\n /** Disable formatting the rendered template using Prettier. */\n disablePrettier?: boolean | undefined;\n\n /** Zod schema for custom template attributes. */\n attributesSchema?: A | undefined;\n\n /** Zod schema for template data. */\n dataSchema?: D | undefined;\n};\n\nexport type TemplateRenderOptions = {\n /** Prettier options. */\n prettierOptions?: prettier.Options | undefined;\n\n /** Disable formatting the rendered template using Prettier. */\n disablePrettier?: boolean | undefined;\n};\n\n/**\n * The `Template` object stores all information about a template and has methods for reading and rendering.\n *\n * ```TypeScript\n * let template = new Template({\n * content: '<%- value %>',\n * attributes: {\n * to: 'value.txt',\n * },\n * dataSchema: z.object({value: z.number()}),\n * });\n *\n * console.log(await template.render({value: 42})); // -> [{attributes: {to: 'value.txt'}, data: {value: 42}, content: '42'}]\n * ```\n *\n * @typeParam A A Zod schema type used to define the custom template attributes type.\n * @typeParam D A Zod schema type used to define the template datatype.\n */\nexport class Template<\n A extends AnyZodObject | undefined = undefined,\n D extends AnyZodObject | undefined = undefined,\n> {\n /** Template path. */\n path?: string;\n\n /** Template content. Templates are written in [EJS](https://ejs.co/). */\n readonly content: string;\n\n /** Template attributes. These control how the template is rendered. Some attributes can be written in [EJS](https://ejs.co/) and are rendered before the template content is rendered. It is expected that attribute `if` renders to a string that can be parsed as YAML value; if that value is `false`, the rendering is cancelled. Attribute `to` is also rendered and it represents the path of file the resulting template render is supposed to be written into (by the user). Attribute `variables` is an object that is merged with the data object before rendering. If attribute `variables` is an array of objects, that leads to multiple renders, each with different variables object merged with the data. Each variable that is a string is also rendered. */\n readonly attributes: A extends undefined\n ? TemplateAttributes\n : TemplateAttributes & z.infer<NonNullable<A>>;\n\n /** Template function. */\n private readonly fn: TemplateFunction;\n\n /** Zod schema for custom template attributes. */\n readonly attributesSchema: A extends AnyZodObject ? A : null;\n\n /** Zod schema for template data. */\n readonly dataSchema: D extends AnyZodObject ? D : null;\n\n constructor({path, content, attributes, attributesSchema, dataSchema}: TemplateOptions<A, D>) {\n if (path) {\n this.path = path;\n }\n\n this.content = content;\n this.attributes = attributes;\n this.fn = ejs.compile(this.content);\n\n if (attributesSchema) {\n this.attributesSchema = attributesSchema as A extends AnyZodObject ? A : null;\n } else {\n this.attributesSchema = null as A extends AnyZodObject ? A : null;\n }\n\n if (dataSchema) {\n this.dataSchema = dataSchema as D extends AnyZodObject ? D : null;\n } else {\n this.dataSchema = null as D extends AnyZodObject ? D : null;\n }\n }\n\n /**\n * Reads a template from a file.\n *\n * @param templatePath Path to the template file.\n */\n static async read<\n A extends AnyZodObject | undefined = undefined,\n D extends AnyZodObject | undefined = undefined,\n >(templatePath: string, options?: TemplateReadOptions<A, D>): Promise<Template<A, D>> {\n let {data: attributes, content} = matter(await fs.readFile(templatePath, 'utf8'));\n let attributesSchema = options?.attributesSchema\n ? templateAttributesSchema.merge(options.attributesSchema)\n : templateAttributesSchema;\n let attributesParseResult = attributesSchema.safeParse(attributes);\n\n if (!attributesParseResult.success) {\n throw new TemplateError('INVALID_ATTRIBUTES', {\n cause: attributesParseResult.error,\n data: {\n path: templatePath,\n zodIssues: attributesParseResult.error.issues,\n },\n });\n }\n\n return new this({\n path: templatePath,\n attributes,\n content,\n attributesSchema: options?.attributesSchema,\n dataSchema: options?.dataSchema,\n } as TemplateOptions<A, D>);\n }\n\n /**\n * Reads a template from a file and then renders it.\n *\n * @param templatePath Path to the template file.\n * @param data Data used for rendering the template.\n * @param options Options object.\n * @return Template renders.\n */\n static async readAndRender<\n A extends AnyZodObject | undefined = undefined,\n D extends AnyZodObject | undefined = undefined,\n >(\n templatePath: string,\n data: D extends undefined ? null : z.infer<NonNullable<D>>,\n options?: TemplateReadAndRenderOptions<A, D>,\n ) {\n let template = await Template.read<A, D>(templatePath, {\n attributesSchema: options?.attributesSchema,\n dataSchema: options?.dataSchema,\n });\n\n return template.render(data, {\n prettierOptions: options?.prettierOptions,\n disablePrettier: options?.disablePrettier,\n });\n }\n\n /**\n * Renders the template using data.\n *\n * @param data Data used for rendering the template.\n * @param options Options object.\n * @return Template renders.\n */\n async render(\n data: D extends undefined ? null : z.infer<NonNullable<D>>,\n options?: TemplateRenderOptions,\n ): Promise<TemplateRenders<A, D>> {\n let renders: TemplateRenders<A, D> = [];\n\n for (let variables of Array.isArray(this.attributes.variables)\n ? this.attributes.variables\n : [this.attributes.variables]) {\n let renderedAttributes = omit(this.attributes, ['if']);\n let renderedVariables = variables;\n\n if (renderedVariables) {\n for (let [variableName, variableValue] of Object.entries(renderedVariables)) {\n if (typeof variableValue === 'string') {\n renderedVariables[variableName] = ejs.render(variableValue, {...data});\n } else {\n renderedVariables[variableName] = variableValue;\n }\n }\n\n renderedAttributes.variables = renderedVariables;\n }\n\n let dataWithAttributes = {...data, attributes: renderedAttributes};\n\n if (this.attributes.if) {\n let renderedIf = ejs.render(this.attributes.if, dataWithAttributes);\n let parsedIf = yaml.parse(renderedIf) as unknown;\n\n if (parsedIf === false) {\n continue;\n }\n }\n\n renderedAttributes.to = ejs.render(renderedAttributes.to, dataWithAttributes);\n dataWithAttributes.attributes = renderedAttributes;\n\n let content;\n\n try {\n content = this.fn(dataWithAttributes);\n } catch (error: unknown) {\n throw new TemplateError('FAILED_RENDER', {cause: error});\n }\n\n if (!options?.disablePrettier) {\n try {\n content = await prettier.format(content, {\n ...options?.prettierOptions,\n filepath: renderedAttributes.to,\n });\n } catch (error: unknown) {\n if (!NO_INFERRED_PARSER_REGEXP.test((error as Error).message)) {\n throw new TemplateError('FAILED_FORMAT', {cause: error});\n }\n }\n }\n\n renders.push({\n attributes: renderedAttributes,\n data: data ?? null,\n content,\n } as TemplateRender<A, D>);\n }\n\n return renders;\n }\n}\n"],"names":["ejs","fs","matter","omit","prettier","yaml","NO_INFERRED_PARSER_REGEXP","templateAttributesSchema","TemplateError","Template","path","content","attributes","fn","attributesSchema","dataSchema","constructor","compile","read","templatePath","options","data","readFile","merge","attributesParseResult","safeParse","success","cause","error","zodIssues","issues","readAndRender","template","render","prettierOptions","disablePrettier","renders","variables","Array","isArray","renderedAttributes","renderedVariables","variableName","variableValue","Object","entries","dataWithAttributes","if","renderedIf","parsedIf","parse","to","format","filepath","test","message","push"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/Template.ts"],"sourcesContent":["import ejs from 'ejs';\nimport fs from 'fs-extra';\nimport matter from 'gray-matter';\nimport merge from 'lodash.merge';\nimport omit from 'lodash.omit';\nimport path from 'node:path';\nimport prettier from 'prettier';\nimport yaml from 'yaml';\nimport {type z, type AnyZodObject} from 'zod';\n\nimport {NO_INFERRED_PARSER_REGEXP} from './constants.js';\nimport {templateAttributesSchema, type TemplateAttributes} from './TemplateAttributes.js';\nimport {TemplateError} from './TemplateError.js';\nimport {type TemplateFunction} from './TemplateFunction.js';\nimport {type TemplateRender} from './TemplateRender.js';\nimport {type TemplateRenders} from './TemplateRenders.js';\n\nexport type TemplateOptions<\n A extends AnyZodObject | undefined = undefined,\n D extends AnyZodObject | undefined = undefined,\n> = {\n /** Template path. */\n path?: string;\n\n /** Template content. */\n content: string;\n\n /** Template attributes. */\n attributes: A extends undefined\n ? TemplateAttributes\n : TemplateAttributes & z.infer<NonNullable<A>>;\n\n /** Zod schema for custom template attributes. */\n attributesSchema?: A | undefined;\n\n /** Zod schema for template data. */\n dataSchema?: D | undefined;\n};\n\nexport type TemplateReadOptions<\n A extends AnyZodObject | undefined = undefined,\n D extends AnyZodObject | undefined = undefined,\n> = {\n /** Zod schema for custom template attributes. */\n attributesSchema?: A | undefined;\n\n /** Zod schema for template data. */\n dataSchema?: D | undefined;\n};\n\nexport type TemplateReadAndRenderOptions<\n A extends AnyZodObject | undefined = undefined,\n D extends AnyZodObject | undefined = undefined,\n> = {\n /** Prettier options. */\n prettierOptions?: prettier.Options | undefined;\n\n /** Disable formatting the rendered template using Prettier. */\n disablePrettier?: boolean | undefined;\n\n /** Zod schema for custom template attributes. */\n attributesSchema?: A | undefined;\n\n /** Zod schema for template data. */\n dataSchema?: D | undefined;\n};\n\nexport type TemplateRenderOptions = {\n /** Prettier options. */\n prettierOptions?: prettier.Options | undefined;\n\n /** Disable formatting the rendered template using Prettier. */\n disablePrettier?: boolean | undefined;\n};\n\n/**\n * The `Template` object stores all information about a template and has methods for reading and rendering.\n *\n * ```TypeScript\n * let template = new Template({\n * content: '<%- value %>',\n * attributes: {\n * to: 'value.txt',\n * },\n * dataSchema: z.object({value: z.number()}),\n * });\n *\n * console.log(await template.render({value: 42})); // -> [{attributes: {to: 'value.txt'}, data: {value: 42}, content: '42'}]\n * ```\n *\n * @typeParam A A Zod schema type used to define the custom template attributes type.\n * @typeParam D A Zod schema type used to define the template datatype.\n */\nexport class Template<\n A extends AnyZodObject | undefined = undefined,\n D extends AnyZodObject | undefined = undefined,\n> {\n /** Template path. */\n path?: string;\n\n /** Template content. Templates are written in [EJS](https://ejs.co/). */\n readonly content: string;\n\n /** Template attributes. These control how the template is rendered. Some attributes can be written in [EJS](https://ejs.co/) and are rendered before the template content is rendered. It is expected that attribute `if` renders to a string that can be parsed as YAML value; if that value is `false`, the rendering is cancelled. Attribute `to` is also rendered and it represents the path of file the resulting template render is supposed to be written into (by the user). Attribute `variables` is an object that is merged with the data object before rendering. If attribute `variables` is an array of objects, that leads to multiple renders, each with different variables object merged with the data. Each variable that is a string is also rendered. */\n readonly attributes: A extends undefined\n ? TemplateAttributes\n : TemplateAttributes & z.infer<NonNullable<A>>;\n\n /** Template function. */\n private readonly fn: TemplateFunction;\n\n /** Zod schema for custom template attributes. */\n readonly attributesSchema: A extends AnyZodObject ? A : null;\n\n /** Zod schema for template data. */\n readonly dataSchema: D extends AnyZodObject ? D : null;\n\n constructor({\n path: templatePath,\n content,\n attributes,\n attributesSchema,\n dataSchema,\n }: TemplateOptions<A, D>) {\n if (templatePath) {\n this.path = templatePath;\n }\n\n this.content = content;\n this.attributes = attributes;\n this.fn = ejs.compile(this.content);\n\n if (attributesSchema) {\n this.attributesSchema = attributesSchema as A extends AnyZodObject ? A : null;\n } else {\n this.attributesSchema = null as A extends AnyZodObject ? A : null;\n }\n\n if (dataSchema) {\n this.dataSchema = dataSchema as D extends AnyZodObject ? D : null;\n } else {\n this.dataSchema = null as D extends AnyZodObject ? D : null;\n }\n }\n\n /**\n * Reads a template from a file.\n *\n * @param templatePath Path to the template file.\n */\n static async read<\n A extends AnyZodObject | undefined = undefined,\n D extends AnyZodObject | undefined = undefined,\n >(templatePath: string, options?: TemplateReadOptions<A, D>): Promise<Template<A, D>> {\n let {data: rawAttributes, content} = matter(await fs.readFile(templatePath, 'utf8'));\n\n // extending another template\n if (typeof rawAttributes.extends === 'string' && rawAttributes.extends) {\n let extendedTemplate = await Template.read(\n path.join(path.dirname(templatePath), rawAttributes.extends),\n options,\n ); // you can point to any template, but it will be parsed with the same options, including attribute or data schema\n\n // attributes are recursively merged\n rawAttributes = merge(extendedTemplate.attributes, rawAttributes);\n\n // if content is empty, use content from the extended template\n if (!content.trim()) {\n content = extendedTemplate.content;\n }\n }\n\n let attributesSchema = options?.attributesSchema\n ? templateAttributesSchema.merge(options.attributesSchema)\n : templateAttributesSchema;\n let attributesParseResult = attributesSchema.safeParse(rawAttributes);\n\n if (!attributesParseResult.success) {\n throw new TemplateError('INVALID_ATTRIBUTES', {\n cause: attributesParseResult.error,\n data: {\n path: templatePath,\n zodIssues: attributesParseResult.error.issues,\n },\n });\n }\n\n let attributes = attributesParseResult.data;\n\n return new this({\n path: templatePath,\n attributes,\n content,\n attributesSchema: options?.attributesSchema,\n dataSchema: options?.dataSchema,\n } as TemplateOptions<A, D>);\n }\n\n /**\n * Reads a template from a file and then renders it.\n *\n * @param templatePath Path to the template file.\n * @param data Data used for rendering the template.\n * @param options Options object.\n * @return Template renders.\n */\n static async readAndRender<\n A extends AnyZodObject | undefined = undefined,\n D extends AnyZodObject | undefined = undefined,\n >(\n templatePath: string,\n data: D extends undefined ? null : z.infer<NonNullable<D>>,\n options?: TemplateReadAndRenderOptions<A, D>,\n ) {\n let template = await Template.read<A, D>(templatePath, {\n attributesSchema: options?.attributesSchema,\n dataSchema: options?.dataSchema,\n });\n\n return template.render(data, {\n prettierOptions: options?.prettierOptions,\n disablePrettier: options?.disablePrettier,\n });\n }\n\n /**\n * Renders the template using data.\n *\n * @param data Data used for rendering the template.\n * @param options Options object.\n * @return Template renders.\n */\n async render(\n data: D extends undefined ? null : z.infer<NonNullable<D>>,\n options?: TemplateRenderOptions,\n ): Promise<TemplateRenders<A, D>> {\n let renders: TemplateRenders<A, D> = [];\n\n for (let variables of Array.isArray(this.attributes.variables)\n ? this.attributes.variables\n : [this.attributes.variables]) {\n let renderedAttributes = omit(this.attributes, ['if']);\n let renderedVariables = variables;\n\n if (renderedVariables) {\n for (let [variableName, variableValue] of Object.entries(renderedVariables)) {\n if (typeof variableValue === 'string') {\n renderedVariables[variableName] = ejs.render(variableValue, {...data});\n } else {\n renderedVariables[variableName] = variableValue;\n }\n }\n\n renderedAttributes.variables = renderedVariables;\n }\n\n let dataWithAttributes = {...data, attributes: renderedAttributes};\n\n if (this.attributes.if) {\n let renderedIf = ejs.render(this.attributes.if, dataWithAttributes);\n let parsedIf = yaml.parse(renderedIf) as unknown;\n\n if (parsedIf === false) {\n continue;\n }\n }\n\n renderedAttributes.to = ejs.render(renderedAttributes.to, dataWithAttributes);\n dataWithAttributes.attributes = renderedAttributes;\n\n let content;\n\n try {\n content = this.fn(dataWithAttributes);\n } catch (error: unknown) {\n throw new TemplateError('FAILED_RENDER', {cause: error});\n }\n\n if (!options?.disablePrettier) {\n try {\n content = await prettier.format(content, {\n ...options?.prettierOptions,\n filepath: renderedAttributes.to,\n });\n } catch (error: unknown) {\n if (!NO_INFERRED_PARSER_REGEXP.test((error as Error).message)) {\n throw new TemplateError('FAILED_FORMAT', {cause: error});\n }\n }\n }\n\n renders.push({\n attributes: renderedAttributes,\n data: data ?? null,\n content,\n } as TemplateRender<A, D>);\n }\n\n return renders;\n }\n}\n"],"names":["ejs","fs","matter","merge","omit","path","prettier","yaml","NO_INFERRED_PARSER_REGEXP","templateAttributesSchema","TemplateError","Template","content","attributes","fn","attributesSchema","dataSchema","constructor","templatePath","compile","read","options","data","rawAttributes","readFile","extends","extendedTemplate","join","dirname","trim","attributesParseResult","safeParse","success","cause","error","zodIssues","issues","readAndRender","template","render","prettierOptions","disablePrettier","renders","variables","Array","isArray","renderedAttributes","renderedVariables","variableName","variableValue","Object","entries","dataWithAttributes","if","renderedIf","parsedIf","parse","to","format","filepath","test","message","push"],"mappings":"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"}
@@ -3,15 +3,18 @@ import { z } from 'zod';
3
3
  * Zod schema for {@link TemplateAttributes}.
4
4
  */
5
5
  export declare const templateAttributesSchema: z.ZodObject<{
6
+ extends: z.ZodOptional<z.ZodString>;
6
7
  to: z.ZodString;
7
8
  if: z.ZodOptional<z.ZodString>;
8
9
  variables: z.ZodOptional<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">]>>;
9
10
  }, "strict", z.ZodTypeAny, {
10
11
  to: string;
12
+ extends?: string | undefined;
11
13
  if?: string | undefined;
12
14
  variables?: Record<string, unknown> | Record<string, unknown>[] | undefined;
13
15
  }, {
14
16
  to: string;
17
+ extends?: string | undefined;
15
18
  if?: string | undefined;
16
19
  variables?: Record<string, unknown> | Record<string, unknown>[] | undefined;
17
20
  }>;
@@ -3,6 +3,7 @@ import { templateVariablesSchema } from './TemplateVariables.js';
3
3
  /**
4
4
  * Zod schema for {@link TemplateAttributes}.
5
5
  */ export const templateAttributesSchema = z.object({
6
+ extends: z.string().optional(),
6
7
  to: z.string(),
7
8
  if: z.string().optional(),
8
9
  variables: templateVariablesSchema.optional()
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/TemplateAttributes.ts"],"sourcesContent":["import {z} from 'zod';\n\nimport {templateVariablesSchema} from './TemplateVariables.js';\n\n/**\n * Zod schema for {@link TemplateAttributes}.\n */\nexport const templateAttributesSchema = z\n .object({\n to: z.string(),\n if: z.string().optional(),\n variables: templateVariablesSchema.optional(),\n })\n .strict();\n\n/**\n * Type representing template attributes.\n */\nexport type TemplateAttributes = z.infer<typeof templateAttributesSchema>;\n"],"names":["z","templateVariablesSchema","templateAttributesSchema","object","to","string","if","optional","variables","strict"],"mappings":"AAAA,SAAQA,CAAC,QAAO,MAAM;AAEtB,SAAQC,uBAAuB,QAAO,yBAAyB;AAE/D;;CAEC,GACD,OAAO,MAAMC,2BAA2BF,EACrCG,MAAM,CAAC;IACNC,IAAIJ,EAAEK,MAAM;IACZC,IAAIN,EAAEK,MAAM,GAAGE,QAAQ;IACvBC,WAAWP,wBAAwBM,QAAQ;AAC7C,GACCE,MAAM,GAAG"}
1
+ {"version":3,"sources":["../src/TemplateAttributes.ts"],"sourcesContent":["import {z} from 'zod';\n\nimport {templateVariablesSchema} from './TemplateVariables.js';\n\n/**\n * Zod schema for {@link TemplateAttributes}.\n */\nexport const templateAttributesSchema = z\n .object({\n extends: z.string().optional(),\n to: z.string(),\n if: z.string().optional(),\n variables: templateVariablesSchema.optional(),\n })\n .strict();\n\n/**\n * Type representing template attributes.\n */\nexport type TemplateAttributes = z.infer<typeof templateAttributesSchema>;\n"],"names":["z","templateVariablesSchema","templateAttributesSchema","object","extends","string","optional","to","if","variables","strict"],"mappings":"AAAA,SAAQA,CAAC,QAAO,MAAM;AAEtB,SAAQC,uBAAuB,QAAO,yBAAyB;AAE/D;;CAEC,GACD,OAAO,MAAMC,2BAA2BF,EACrCG,MAAM,CAAC;IACNC,SAASJ,EAAEK,MAAM,GAAGC,QAAQ;IAC5BC,IAAIP,EAAEK,MAAM;IACZG,IAAIR,EAAEK,MAAM,GAAGC,QAAQ;IACvBG,WAAWR,wBAAwBK,QAAQ;AAC7C,GACCI,MAAM,GAAG"}
@@ -4,7 +4,7 @@ import { createCustomErrorWithData } from '@jakubmazanec/error';
4
4
  */ export const TemplateError = createCustomErrorWithData('TemplateError', {
5
5
  INVALID_ATTRIBUTES: 'Invalid template attributes.',
6
6
  FAILED_FORMAT: 'Template render formatting has failed.',
7
- FAILED_RENDER: 'Rendering has failed.'
7
+ FAILED_RENDER: 'Template rendering has failed.'
8
8
  }, class TemplateErrorData {
9
9
  template;
10
10
  path;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/TemplateError.ts"],"sourcesContent":["import {createCustomErrorWithData} from '@jakubmazanec/error';\nimport {type ZodIssue} from 'zod';\n\nimport {type Template} from './Template.js';\n\n/**\n * A subclass of `Error` that indicates a template failure.\n */\nexport const TemplateError = createCustomErrorWithData(\n 'TemplateError',\n {\n INVALID_ATTRIBUTES: 'Invalid template attributes.',\n FAILED_FORMAT: 'Template render formatting has failed.',\n FAILED_RENDER: 'Rendering has failed.',\n },\n class TemplateErrorData {\n template?: Template;\n path?: string;\n zodIssues?: ZodIssue[];\n },\n);\n"],"names":["createCustomErrorWithData","TemplateError","INVALID_ATTRIBUTES","FAILED_FORMAT","FAILED_RENDER","TemplateErrorData","template","path","zodIssues"],"mappings":"AAAA,SAAQA,yBAAyB,QAAO,sBAAsB;AAK9D;;CAEC,GACD,OAAO,MAAMC,gBAAgBD,0BAC3B,iBACA;IACEE,oBAAoB;IACpBC,eAAe;IACfC,eAAe;AACjB,GACA,MAAMC;IACJC,SAAoB;IACpBC,KAAc;IACdC,UAAuB;AACzB,GACA"}
1
+ {"version":3,"sources":["../src/TemplateError.ts"],"sourcesContent":["import {createCustomErrorWithData} from '@jakubmazanec/error';\nimport {type ZodIssue} from 'zod';\n\nimport {type Template} from './Template.js';\n\n/**\n * A subclass of `Error` that indicates a template failure.\n */\nexport const TemplateError = createCustomErrorWithData(\n 'TemplateError',\n {\n INVALID_ATTRIBUTES: 'Invalid template attributes.',\n FAILED_FORMAT: 'Template render formatting has failed.',\n FAILED_RENDER: 'Template rendering has failed.',\n },\n class TemplateErrorData {\n template?: Template;\n path?: string;\n zodIssues?: ZodIssue[];\n },\n);\n"],"names":["createCustomErrorWithData","TemplateError","INVALID_ATTRIBUTES","FAILED_FORMAT","FAILED_RENDER","TemplateErrorData","template","path","zodIssues"],"mappings":"AAAA,SAAQA,yBAAyB,QAAO,sBAAsB;AAK9D;;CAEC,GACD,OAAO,MAAMC,gBAAgBD,0BAC3B,iBACA;IACEE,oBAAoB;IACpBC,eAAe;IACfC,eAAe;AACjB,GACA,MAAMC;IACJC,SAAoB;IACpBC,KAAc;IACdC,UAAuB;AACzB,GACA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jakubmazanec/template",
3
- "version": "1.0.3",
3
+ "version": "1.1.0-next.5d8abeb",
4
4
  "description": "TypeScript-first library for working with templates using EJS and Zod.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,6 +30,7 @@
30
30
  "ejs": "^3.1.9",
31
31
  "fs-extra": "^11.1.1",
32
32
  "gray-matter": "^4.0.3",
33
+ "lodash.merge": "^4.6.2",
33
34
  "lodash.omit": "^4.5.0",
34
35
  "prettier": "^3.0.0",
35
36
  "yaml": "^2.2.1",
@@ -39,6 +40,7 @@
39
40
  "@jest/globals": "^29.5.0",
40
41
  "@types/ejs": "^3.1.2",
41
42
  "@types/fs-extra": "^11.0.1",
43
+ "@types/lodash.merge": "^4.6.7",
42
44
  "@types/lodash.omit": "^4.5.7",
43
45
  "@types/node": "^16.18.3"
44
46
  },
package/src/Template.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import ejs from 'ejs';
2
2
  import fs from 'fs-extra';
3
3
  import matter from 'gray-matter';
4
+ import merge from 'lodash.merge';
4
5
  import omit from 'lodash.omit';
6
+ import path from 'node:path';
5
7
  import prettier from 'prettier';
6
8
  import yaml from 'yaml';
7
9
  import {type z, type AnyZodObject} from 'zod';
@@ -113,9 +115,15 @@ export class Template<
113
115
  /** Zod schema for template data. */
114
116
  readonly dataSchema: D extends AnyZodObject ? D : null;
115
117
 
116
- constructor({path, content, attributes, attributesSchema, dataSchema}: TemplateOptions<A, D>) {
117
- if (path) {
118
- this.path = path;
118
+ constructor({
119
+ path: templatePath,
120
+ content,
121
+ attributes,
122
+ attributesSchema,
123
+ dataSchema,
124
+ }: TemplateOptions<A, D>) {
125
+ if (templatePath) {
126
+ this.path = templatePath;
119
127
  }
120
128
 
121
129
  this.content = content;
@@ -144,11 +152,28 @@ export class Template<
144
152
  A extends AnyZodObject | undefined = undefined,
145
153
  D extends AnyZodObject | undefined = undefined,
146
154
  >(templatePath: string, options?: TemplateReadOptions<A, D>): Promise<Template<A, D>> {
147
- let {data: attributes, content} = matter(await fs.readFile(templatePath, 'utf8'));
155
+ let {data: rawAttributes, content} = matter(await fs.readFile(templatePath, 'utf8'));
156
+
157
+ // extending another template
158
+ if (typeof rawAttributes.extends === 'string' && rawAttributes.extends) {
159
+ let extendedTemplate = await Template.read(
160
+ path.join(path.dirname(templatePath), rawAttributes.extends),
161
+ options,
162
+ ); // you can point to any template, but it will be parsed with the same options, including attribute or data schema
163
+
164
+ // attributes are recursively merged
165
+ rawAttributes = merge(extendedTemplate.attributes, rawAttributes);
166
+
167
+ // if content is empty, use content from the extended template
168
+ if (!content.trim()) {
169
+ content = extendedTemplate.content;
170
+ }
171
+ }
172
+
148
173
  let attributesSchema = options?.attributesSchema
149
174
  ? templateAttributesSchema.merge(options.attributesSchema)
150
175
  : templateAttributesSchema;
151
- let attributesParseResult = attributesSchema.safeParse(attributes);
176
+ let attributesParseResult = attributesSchema.safeParse(rawAttributes);
152
177
 
153
178
  if (!attributesParseResult.success) {
154
179
  throw new TemplateError('INVALID_ATTRIBUTES', {
@@ -160,6 +185,8 @@ export class Template<
160
185
  });
161
186
  }
162
187
 
188
+ let attributes = attributesParseResult.data;
189
+
163
190
  return new this({
164
191
  path: templatePath,
165
192
  attributes,
@@ -7,6 +7,7 @@ import {templateVariablesSchema} from './TemplateVariables.js';
7
7
  */
8
8
  export const templateAttributesSchema = z
9
9
  .object({
10
+ extends: z.string().optional(),
10
11
  to: z.string(),
11
12
  if: z.string().optional(),
12
13
  variables: templateVariablesSchema.optional(),
@@ -11,7 +11,7 @@ export const TemplateError = createCustomErrorWithData(
11
11
  {
12
12
  INVALID_ATTRIBUTES: 'Invalid template attributes.',
13
13
  FAILED_FORMAT: 'Template render formatting has failed.',
14
- FAILED_RENDER: 'Rendering has failed.',
14
+ FAILED_RENDER: 'Template rendering has failed.',
15
15
  },
16
16
  class TemplateErrorData {
17
17
  template?: Template;