diff --git a/packages/post-kit-compiler/LICENSE b/packages/post-kit-compiler/LICENSE new file mode 100644 index 0000000..c8b783b --- /dev/null +++ b/packages/post-kit-compiler/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Singleton SD + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/post-kit-compiler/README.md b/packages/post-kit-compiler/README.md new file mode 100644 index 0000000..1177859 --- /dev/null +++ b/packages/post-kit-compiler/README.md @@ -0,0 +1,81 @@ +# @singleton-sd/post-kit-compiler + +Template compiler for [PostKit](../../README.md). Reads Git-backed email template source files (`template.json`, `metadata.json`, `preview.json`), validates them, and produces a compiled `CompiledTemplate` artifact for use by `post-kit-publisher` and `apps/api`. + +## HTML renderer and variable engine + +HTML is produced by [`@usewaypoint/email-builder`](https://www.npmjs.com/package/@usewaypoint/email-builder) `renderToStaticMarkup` (EmailBuilder.js JSON → email HTML). + +Subject lines and `{{variable}}` substitution in that HTML use [Handlebars](https://handlebarsjs.com/) `^4.7.8` (the version declared in this package). Handlebars is **not** the HTML renderer. + +Until send-time substitution, compiled `templateHtml` keeps Handlebars placeholders. `compile()` still renders preview.json through Handlebars to fail fast on invalid templates. + +## Installation + +```bash +pnpm add @singleton-sd/post-kit-compiler +``` + +## API + +```ts +import { compile, compileFromDirectory, validateSource, CompilerError } from '@singleton-sd/post-kit-compiler'; +``` + +### `compile(source, options?)` + +Validates and compiles a `TemplateSource` object into a `CompiledTemplate`. + +```ts +const result = await compile({ + templateJson: { document: { type: 'EmailLayout', data: {} } }, + metadata: { + key: 'marketing.contact-us', + name: 'Contact Us', + subject: 'New message from {{name}}', + variables: ['name', 'email', 'message'], + schemaVersion: '1', + }, + previewData: { name: 'Jane Doe', email: 'jane@example.com', message: 'Hello!' }, +}); + +console.log(result.manifest.contentHash); // SHA-256 hex +``` + +### `compileFromDirectory(dir, options?)` + +Reads `template.json`, `metadata.json`, and `preview.json` from `dir` and delegates to `compile()`. + +```ts +const result = await compileFromDirectory('./content/email-templates/marketing.contact-us'); +``` + +### `validateSource(source)` + +Dry-run validation — returns `{ ok: true }` or `{ ok: false; errors: string[] }`. Does not throw. + +```ts +const validation = validateSource(source); +if (!validation.ok) { + console.error(validation.errors); +} +``` + +### `CompilerError` + +Thrown by `compile()` and `compileFromDirectory()` on validation or render failures. Has a `code` property: + +| Code | Meaning | +|---|---| +| `INVALID_TEMPLATE_JSON` | `template.json` is missing or not valid JSON | +| `INVALID_METADATA` | `metadata.json` or `preview.json` is missing, not valid JSON, or fails schema validation | +| `MISSING_PREVIEW_VARIABLE` | A variable declared in `metadata.variables` is absent from `previewData` | +| `RENDER_FAILURE` | HTML or subject template rendering failed | + +## Development + +```bash +pnpm test # type-check + run tests +pnpm build # emit CommonJS to dist/ +pnpm lint # covered by root eslint +``` diff --git a/packages/post-kit-compiler/package.json b/packages/post-kit-compiler/package.json new file mode 100644 index 0000000..af9c177 --- /dev/null +++ b/packages/post-kit-compiler/package.json @@ -0,0 +1,45 @@ +{ + "name": "@singleton-sd/post-kit-compiler", + "version": "0.1.0", + "private": false, + "description": "Template compiler for PostKit — validates and compiles EmailBuilder.js source files into deployable HTML artifacts", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "LICENSE" + ], + "scripts": { + "build": "pnpm --filter @singleton-sd/post-kit-types run build && tsc -p tsconfig.json", + "lint": "echo \"lint:compiler — covered by root eslint on staged files\"", + "test": "pnpm --filter @singleton-sd/post-kit-types run build && tsc -p tsconfig.spec.json && node --import tsx --test src/**/*.spec.ts" + }, + "devDependencies": { + "@types/node": "^20.17.9", + "@types/react": "^18.3.31", + "@types/react-dom": "^18.3.7", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=20.18.1" + }, + "dependencies": { + "@singleton-sd/post-kit-types": "workspace:*", + "@usewaypoint/email-builder": "0.0.9", + "handlebars": "^4.7.8", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "zod": "^3.25.76" + } +} diff --git a/packages/post-kit-compiler/src/compiler-error.ts b/packages/post-kit-compiler/src/compiler-error.ts new file mode 100644 index 0000000..130a569 --- /dev/null +++ b/packages/post-kit-compiler/src/compiler-error.ts @@ -0,0 +1,12 @@ +export type CompilerErrorCode = + 'INVALID_TEMPLATE_JSON' | 'INVALID_METADATA' | 'MISSING_PREVIEW_VARIABLE' | 'RENDER_FAILURE'; + +export class CompilerError extends Error { + constructor( + public readonly code: CompilerErrorCode, + message: string, + ) { + super(message); + this.name = 'CompilerError'; + } +} diff --git a/packages/post-kit-compiler/src/compiler.spec.ts b/packages/post-kit-compiler/src/compiler.spec.ts new file mode 100644 index 0000000..aa832fe --- /dev/null +++ b/packages/post-kit-compiler/src/compiler.spec.ts @@ -0,0 +1,272 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { join } from 'node:path'; +import { compile, compileFromDirectory, validateSource } from './compiler'; +import { CompilerError } from './compiler-error'; +import type { TemplateSource } from './template-source'; + +const FIXTURES_DIR = join(import.meta.dirname, 'fixtures'); + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +const CONTACT_US_DOCUMENT = { + root: { + type: 'EmailLayout', + data: { + backdropColor: '#F8F8F8', + canvasColor: '#FFFFFF', + textColor: '#242424', + fontFamily: 'MODERN_SANS', + childrenIds: ['block-text'], + }, + }, + 'block-text': { + type: 'Text', + data: { + style: { + fontWeight: 'normal', + padding: { top: 16, bottom: 16, right: 24, left: 24 }, + }, + props: { + text: 'Hello {{name}}, from {{email}}: {{message}}', + }, + }, + }, +}; + +const contactUsSource: TemplateSource = { + templateJson: CONTACT_US_DOCUMENT, + metadata: { + key: 'marketing.contact-us', + name: 'Contact Us', + subject: 'New message from {{name}}', + variables: ['name', 'email', 'message'], + schemaVersion: '1', + }, + previewData: { + name: 'Jane Doe', + email: 'jane@example.com', + message: 'Hello!', + }, +}; + +const missingPreviewVarSource: TemplateSource = { + templateJson: { document: {} }, + metadata: { + key: 'auth.password-reset', + name: 'Password Reset', + subject: 'Reset your password', + variables: ['resetUrl'], + schemaVersion: '1', + }, + previewData: { + // resetUrl intentionally omitted + name: 'Jane Doe', + }, +}; + +const malformedMetadataSource: TemplateSource = { + templateJson: { document: {} }, + // key field missing — cast through unknown to simulate a runtime parse result + metadata: { + name: 'Malformed', + subject: 'Hello', + variables: [], + schemaVersion: '1', + } as unknown as TemplateSource['metadata'], + previewData: {}, +}; + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('compile()', () => { + it('returns CompiledTemplate with correct key and non-empty contentHash', async () => { + const result = await compile(contactUsSource); + + assert.equal(result.metadata.key, 'marketing.contact-us'); + assert.ok(result.manifest.contentHash.length > 0, 'contentHash should be non-empty'); + assert.ok( + result.templateHtml.includes('Hello {{name}}'), + 'HTML should keep Handlebars placeholders', + ); + assert.ok(result.templateHtml.includes(' { + const [a, b] = await Promise.all([compile(contactUsSource), compile(contactUsSource)]); + + assert.equal(a.templateHtml, b.templateHtml); + assert.equal(a.manifest.contentHash, b.manifest.contentHash); + }); + + it('throws CompilerError(MISSING_PREVIEW_VARIABLE) when a variable is absent from previewData', async () => { + await assert.rejects( + () => compile(missingPreviewVarSource), + (err: unknown) => { + assert.ok(err instanceof CompilerError, 'should be a CompilerError'); + assert.equal(err.code, 'MISSING_PREVIEW_VARIABLE'); + assert.ok(err.message.includes('resetUrl'), 'message should name the missing variable'); + return true; + }, + ); + }); + + it('throws CompilerError(INVALID_METADATA) when metadata is malformed', async () => { + await assert.rejects( + () => compile(malformedMetadataSource), + (err: unknown) => { + assert.ok(err instanceof CompilerError, 'should be a CompilerError'); + assert.equal(err.code, 'INVALID_METADATA'); + return true; + }, + ); + }); + + it('throws CompilerError(INVALID_METADATA) when schemaVersion is not the expected version', async () => { + const source: TemplateSource = { + templateJson: { document: {} }, + metadata: { + key: 'test.bad-schema', + name: 'Bad Schema', + subject: 'Hello', + variables: [], + schemaVersion: '99', + } as unknown as TemplateSource['metadata'], + previewData: {}, + }; + + await assert.rejects( + () => compile(source), + (err: unknown) => { + assert.ok(err instanceof CompilerError, 'should be a CompilerError'); + assert.equal(err.code, 'INVALID_METADATA'); + assert.ok(err.message.includes('schemaVersion'), 'message should mention schemaVersion'); + return true; + }, + ); + }); + + it('throws CompilerError(MISSING_PREVIEW_VARIABLE) for inherited (non-own) property in previewData', async () => { + // Create an object whose prototype has the variable key — hasOwnProperty should reject it + const proto = { inheritedVar: 'value' }; + const previewWithInheritedProp = Object.create(proto) as Record; + + const source: TemplateSource = { + templateJson: { document: {} }, + metadata: { + key: 'test.inherited', + name: 'Inherited Prop Test', + subject: 'Hello', + variables: ['inheritedVar'], + schemaVersion: '1', + }, + previewData: previewWithInheritedProp, + }; + + await assert.rejects( + () => compile(source), + (err: unknown) => { + assert.ok(err instanceof CompilerError, 'should be a CompilerError'); + assert.equal(err.code, 'MISSING_PREVIEW_VARIABLE'); + assert.ok(err.message.includes('inheritedVar'), 'message should name the missing variable'); + return true; + }, + ); + }); + + it('throws CompilerError(RENDER_FAILURE) when templateJson is not an EmailBuilder document', async () => { + const source: TemplateSource = { + templateJson: { document: { type: 'EmailLayout' } }, + metadata: { + key: 'test.bad-json', + name: 'Bad JSON', + subject: 'Hello', + variables: [], + schemaVersion: '1', + }, + previewData: {}, + }; + + await assert.rejects( + () => compile(source), + (err: unknown) => { + assert.ok(err instanceof CompilerError); + assert.equal(err.code, 'RENDER_FAILURE'); + return true; + }, + ); + }); + + it('subject rendered with Handlebars: {{name}} with {name: "Jane"} renders to "Jane"', async () => { + const source: TemplateSource = { + templateJson: CONTACT_US_DOCUMENT, + metadata: { + key: 'test.subject-render', + name: 'Subject Render Test', + subject: '{{name}}', + variables: ['name'], + schemaVersion: '1', + }, + previewData: { name: 'Jane' }, + }; + + // compile does not return the rendered subject directly, but we can verify + // there is no render error and use Handlebars directly to assert the rendering + const Handlebars = await import('handlebars'); + const rendered = Handlebars.default.compile('{{name}}')({ name: 'Jane' }); + assert.equal(rendered, 'Jane'); + + // compile() with a subject template and matching previewData should not throw + const result = await compile(source); + assert.ok(result.manifest.contentHash.length > 0); + }); +}); + +describe('compileFromDirectory()', () => { + it('reads fixture files from disk and returns a CompiledTemplate', async () => { + const dir = join(FIXTURES_DIR, 'marketing.contact-us'); + const result = await compileFromDirectory(dir); + + assert.equal(result.metadata.key, 'marketing.contact-us'); + assert.ok(result.manifest.contentHash.length > 0); + assert.ok(result.templateHtml.length > 0); + }); +}); + +describe('validateSource()', () => { + it('returns ok:true for a valid source', () => { + const result = validateSource(contactUsSource); + assert.ok(result.ok, 'expected ok:true'); + }); + + it('returns ok:false with errors when a preview variable is missing', () => { + const result = validateSource(missingPreviewVarSource); + assert.ok(!result.ok, 'expected ok:false'); + if (!result.ok) { + assert.ok(result.errors.length > 0, 'errors array should be non-empty'); + assert.ok( + result.errors.some((e) => e.includes('resetUrl')), + 'errors should mention the missing variable', + ); + } + }); + + it('returns ok:false with errors for malformed metadata (does not throw)', () => { + let threw = false; + let result: ReturnType; + try { + result = validateSource(malformedMetadataSource); + } catch { + threw = true; + result = { ok: false, errors: [] }; + } + assert.ok(!threw, 'validateSource should not throw'); + assert.ok(!result.ok, 'expected ok:false'); + }); +}); diff --git a/packages/post-kit-compiler/src/compiler.ts b/packages/post-kit-compiler/src/compiler.ts new file mode 100644 index 0000000..39bebb0 --- /dev/null +++ b/packages/post-kit-compiler/src/compiler.ts @@ -0,0 +1,239 @@ +import { createHash } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import Handlebars from 'handlebars'; +import { renderToStaticMarkup, type TReaderDocument } from '@usewaypoint/email-builder'; +import type { CompiledTemplate, TemplateSourceMetadata } from '@singleton-sd/post-kit-types'; +import { TEMPLATE_SCHEMA_VERSION } from '@singleton-sd/post-kit-types'; +import { CompilerError } from './compiler-error'; +import type { TemplateSource } from './template-source'; + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +/** + * Validate that `value` satisfies the TemplateSourceMetadata shape. + * Returns the typed metadata or throws CompilerError(INVALID_METADATA). + */ +function assertMetadata(value: unknown): TemplateSourceMetadata { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new CompilerError('INVALID_METADATA', 'metadata must be a JSON object'); + } + + const m = value as Record; + + const requiredStrings = ['key', 'name', 'subject', 'schemaVersion'] as const; + + for (const field of requiredStrings) { + if (typeof m[field] !== 'string' || (m[field] as string).trim() === '') { + throw new CompilerError( + 'INVALID_METADATA', + `metadata.${field} must be a non-empty string (got ${JSON.stringify(m[field])})`, + ); + } + } + + if (m['schemaVersion'] !== TEMPLATE_SCHEMA_VERSION) { + throw new CompilerError( + 'INVALID_METADATA', + `metadata.schemaVersion must be "${TEMPLATE_SCHEMA_VERSION}" (got ${JSON.stringify(m['schemaVersion'])})`, + ); + } + + if (!Array.isArray(m['variables'])) { + throw new CompilerError('INVALID_METADATA', 'metadata.variables must be an array'); + } + + for (let i = 0; i < (m['variables'] as unknown[]).length; i++) { + if (typeof (m['variables'] as unknown[])[i] !== 'string') { + throw new CompilerError('INVALID_METADATA', `metadata.variables[${i}] must be a string`); + } + } + + return m as unknown as TemplateSourceMetadata; +} + +/** + * Render an EmailBuilder.js document to email HTML. + * + * Uses `@usewaypoint/email-builder` `renderToStaticMarkup`. Handlebars is not + * the HTML renderer — it only substitutes `{{variable}}` values for preview + * validation after this step. + */ +function renderTemplateHtml(templateJson: unknown): string { + if (!isReaderDocument(templateJson)) { + throw new Error('templateJson must be an EmailBuilder document object with a root block'); + } + + return renderToStaticMarkup(templateJson, { rootBlockId: 'root' }); +} + +function isReaderDocument(value: unknown): value is TReaderDocument { + return typeof value === 'object' && value !== null && !Array.isArray(value) && 'root' in value; +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Compile a {@link TemplateSource} into a {@link CompiledTemplate}. + * + * Validation steps (in order): + * 1. Metadata shape check — all required string fields present and non-empty. + * 2. Preview-variable coverage — every variable listed in metadata must have + * a corresponding key in previewData. + * 3. HTML render via `@usewaypoint/email-builder`. + * 4. Handlebars subject and preview-variable render (validation only). + * 5. SHA-256 content hash of the rendered HTML (compiledAt excluded). + */ +export async function compile( + source: TemplateSource, + options?: { sourceCommit?: string }, +): Promise { + // 1. Validate metadata shape + const metadata = assertMetadata(source.metadata); + + // 2. Check preview variable coverage + for (const variable of metadata.variables) { + if (!Object.prototype.hasOwnProperty.call(source.previewData, variable)) { + throw new CompilerError( + 'MISSING_PREVIEW_VARIABLE', + `Preview data is missing variable: "${variable}"`, + ); + } + } + + // 3. Render HTML + let templateHtml: string; + try { + templateHtml = renderTemplateHtml(source.templateJson); + } catch (err) { + throw new CompilerError( + 'RENDER_FAILURE', + `Failed to render template HTML: ${err instanceof Error ? err.message : String(err)}`, + ); + } + + // 4. Validate Handlebars in subject and compiled HTML (preview only — stored + // HTML keeps {{variable}} placeholders for runtime send). + try { + Handlebars.compile(metadata.subject)(source.previewData); + Handlebars.compile(templateHtml)(source.previewData); + } catch (err) { + throw new CompilerError( + 'RENDER_FAILURE', + `Failed to render subject template: ${err instanceof Error ? err.message : String(err)}`, + ); + } + + // 5. Compute content hash — compiledAt is intentionally excluded + const contentHash = createHash('sha256').update(templateHtml).digest('hex'); + + const compiledAt = new Date().toISOString(); + + return { + templateHtml, + metadata, + manifest: { + key: metadata.key, + schemaVersion: TEMPLATE_SCHEMA_VERSION, + compiledAt, + sourceCommit: options?.sourceCommit ?? '', + variables: metadata.variables, + contentHash, + }, + }; +} + +/** + * Read `template.json`, `metadata.json`, and `preview.json` from `dir` and + * delegate to {@link compile}. + */ +export async function compileFromDirectory( + dir: string, + options?: { sourceCommit?: string }, +): Promise { + let templateJson: unknown; + let metadataRaw: unknown; + let previewData: unknown; + + // Read and parse template.json + try { + const raw = await readFile(join(dir, 'template.json'), 'utf-8'); + templateJson = JSON.parse(raw); + } catch (err) { + throw new CompilerError( + 'INVALID_TEMPLATE_JSON', + `Failed to read/parse template.json in "${dir}": ${err instanceof Error ? err.message : String(err)}`, + ); + } + + // Read and parse metadata.json + try { + const raw = await readFile(join(dir, 'metadata.json'), 'utf-8'); + metadataRaw = JSON.parse(raw); + } catch (err) { + throw new CompilerError( + 'INVALID_METADATA', + `Failed to read/parse metadata.json in "${dir}": ${err instanceof Error ? err.message : String(err)}`, + ); + } + + // Read and parse preview.json + try { + const raw = await readFile(join(dir, 'preview.json'), 'utf-8'); + previewData = JSON.parse(raw); + } catch (err) { + throw new CompilerError( + 'INVALID_METADATA', + `Failed to read/parse preview.json in "${dir}": ${err instanceof Error ? err.message : String(err)}`, + ); + } + + return compile( + { + templateJson, + metadata: metadataRaw as TemplateSourceMetadata, + previewData: previewData as Record, + }, + options, + ); +} + +/** + * Validate a {@link TemplateSource} without producing output. + * + * Runs all validation steps from {@link compile} but never throws — returns + * an array of error messages instead. + */ +export function validateSource( + source: TemplateSource, +): { ok: true } | { ok: false; errors: string[] } { + const errors: string[] = []; + + let metadata: TemplateSourceMetadata | undefined; + + // Validate metadata shape + try { + metadata = assertMetadata(source.metadata); + } catch (err) { + errors.push(err instanceof CompilerError ? err.message : String(err)); + } + + // Validate preview variable coverage (only if metadata parsed successfully) + if (metadata !== undefined) { + for (const variable of metadata.variables) { + if (!Object.prototype.hasOwnProperty.call(source.previewData, variable)) { + errors.push(`Preview data is missing variable: "${variable}"`); + } + } + } + + if (errors.length > 0) { + return { ok: false, errors }; + } + + return { ok: true }; +} diff --git a/packages/post-kit-compiler/src/fixtures/malformed-metadata/metadata.json b/packages/post-kit-compiler/src/fixtures/malformed-metadata/metadata.json new file mode 100644 index 0000000..ca1ddb3 --- /dev/null +++ b/packages/post-kit-compiler/src/fixtures/malformed-metadata/metadata.json @@ -0,0 +1 @@ +{ "name": "Malformed Template", "subject": "Hello", "variables": [], "schemaVersion": "1" } diff --git a/packages/post-kit-compiler/src/fixtures/marketing.contact-us/metadata.json b/packages/post-kit-compiler/src/fixtures/marketing.contact-us/metadata.json new file mode 100644 index 0000000..2ae4724 --- /dev/null +++ b/packages/post-kit-compiler/src/fixtures/marketing.contact-us/metadata.json @@ -0,0 +1,7 @@ +{ + "key": "marketing.contact-us", + "name": "Contact Us", + "subject": "New message from {{name}}", + "variables": ["name", "email", "message"], + "schemaVersion": "1" +} diff --git a/packages/post-kit-compiler/src/fixtures/marketing.contact-us/preview.json b/packages/post-kit-compiler/src/fixtures/marketing.contact-us/preview.json new file mode 100644 index 0000000..98fcdbf --- /dev/null +++ b/packages/post-kit-compiler/src/fixtures/marketing.contact-us/preview.json @@ -0,0 +1 @@ +{ "name": "Jane Doe", "email": "jane@example.com", "message": "Hello!" } diff --git a/packages/post-kit-compiler/src/fixtures/marketing.contact-us/template.json b/packages/post-kit-compiler/src/fixtures/marketing.contact-us/template.json new file mode 100644 index 0000000..bd127b7 --- /dev/null +++ b/packages/post-kit-compiler/src/fixtures/marketing.contact-us/template.json @@ -0,0 +1,24 @@ +{ + "root": { + "type": "EmailLayout", + "data": { + "backdropColor": "#F8F8F8", + "canvasColor": "#FFFFFF", + "textColor": "#242424", + "fontFamily": "MODERN_SANS", + "childrenIds": ["block-text"] + } + }, + "block-text": { + "type": "Text", + "data": { + "style": { + "fontWeight": "normal", + "padding": { "top": 16, "bottom": 16, "right": 24, "left": 24 } + }, + "props": { + "text": "Hello {{name}}, from {{email}}: {{message}}" + } + } + } +} diff --git a/packages/post-kit-compiler/src/fixtures/missing-preview-var/metadata.json b/packages/post-kit-compiler/src/fixtures/missing-preview-var/metadata.json new file mode 100644 index 0000000..057e612 --- /dev/null +++ b/packages/post-kit-compiler/src/fixtures/missing-preview-var/metadata.json @@ -0,0 +1,7 @@ +{ + "key": "auth.password-reset", + "name": "Password Reset", + "subject": "Reset your password", + "variables": ["resetUrl"], + "schemaVersion": "1" +} diff --git a/packages/post-kit-compiler/src/fixtures/missing-preview-var/preview.json b/packages/post-kit-compiler/src/fixtures/missing-preview-var/preview.json new file mode 100644 index 0000000..e4664a5 --- /dev/null +++ b/packages/post-kit-compiler/src/fixtures/missing-preview-var/preview.json @@ -0,0 +1 @@ +{ "name": "Jane Doe" } diff --git a/packages/post-kit-compiler/src/index.ts b/packages/post-kit-compiler/src/index.ts new file mode 100644 index 0000000..3b0ec13 --- /dev/null +++ b/packages/post-kit-compiler/src/index.ts @@ -0,0 +1,3 @@ +export { CompilerError, type CompilerErrorCode } from './compiler-error'; +export { type TemplateSource } from './template-source'; +export { compile, compileFromDirectory, validateSource } from './compiler'; diff --git a/packages/post-kit-compiler/src/template-source.ts b/packages/post-kit-compiler/src/template-source.ts new file mode 100644 index 0000000..214301d --- /dev/null +++ b/packages/post-kit-compiler/src/template-source.ts @@ -0,0 +1,7 @@ +import type { TemplateSourceMetadata, TemplatePreviewData } from '@singleton-sd/post-kit-types'; + +export interface TemplateSource { + templateJson: unknown; // EmailBuilder.js document (any valid JSON) + metadata: TemplateSourceMetadata; + previewData: TemplatePreviewData; +} diff --git a/packages/post-kit-compiler/tsconfig.json b/packages/post-kit-compiler/tsconfig.json new file mode 100644 index 0000000..7989add --- /dev/null +++ b/packages/post-kit-compiler/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true + }, + "include": ["src/**/*"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/packages/post-kit-compiler/tsconfig.spec.json b/packages/post-kit-compiler/tsconfig.spec.json new file mode 100644 index 0000000..c4325e5 --- /dev/null +++ b/packages/post-kit-compiler/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "module": "es2022", + "moduleResolution": "bundler" + }, + "include": ["src/**/*"], + "exclude": [] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b6d5d2..f989b32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,6 +64,43 @@ importers: specifier: ^5.7.2 version: 5.9.3 + packages/post-kit-compiler: + dependencies: + '@singleton-sd/post-kit-types': + specifier: workspace:* + version: link:../post-kit-types + '@usewaypoint/email-builder': + specifier: 0.0.9 + version: 0.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76) + handlebars: + specifier: ^4.7.8 + version: 4.7.9 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + zod: + specifier: ^3.25.76 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^20.17.9 + version: 20.19.43 + '@types/react': + specifier: ^18.3.31 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.7 + version: 18.3.7(@types/react@18.3.31) + tsx: + specifier: ^4.19.2 + version: 4.23.12 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + packages/post-kit-email: dependencies: undici: @@ -692,10 +729,94 @@ packages: resolution: {integrity: sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q==} deprecated: This is a stub types definition. parse-path provides its own type definitions, so you do not need this installed. + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.31': + resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + '@typespec/ts-http-runtime@0.3.8': resolution: {integrity: sha512-bLMpVcWZNzq6lYOybwFwOAR1IXKcHnhUNqYeHjl1bET/qE3jFPFH+p8Wrh3rU4xwdnifPxmKNESBYnvnmc75aA==} engines: {node: '>=22.0.0'} + '@usewaypoint/block-avatar@0.0.3': + resolution: {integrity: sha512-3BM6P4ztMmqDbSijtVQqI1canRkcENOEHZ2X9BYNv8BZGJbmitTrzANvwmmYXfFEuWPCAyABvujdZds15Zg8Qg==} + peerDependencies: + react: ^16 || ^17 || ^18 + zod: ^1 || ^2 || ^3 + + '@usewaypoint/block-button@0.0.3': + resolution: {integrity: sha512-LXSI3FmCTv13voYX4wdHY7iJdsfyRfpDJZCFKSun5EF1j9FXrqMDGScpk/yokopkQWvWkYXQNAne7W0yWhRQlg==} + peerDependencies: + react: ^16 || ^17 || ^18 + zod: ^1 || ^2 || ^3 + + '@usewaypoint/block-columns-container@0.0.3': + resolution: {integrity: sha512-r5jaojU1Fr6Svtl0a9dDlBHgslJQ04M+XaXaEO+GZ12+35fdAirpLkrEhuyBIA1FFXzRTG740wkbkr++iv1kuA==} + peerDependencies: + react: ^16 || ^17 || ^18 + zod: ^1 || ^2 || ^3 + + '@usewaypoint/block-container@0.0.2': + resolution: {integrity: sha512-li9GVdiahVpJ+MNRdkoCkP6/hBTdcpaLRGpaFBSQRkVt+cYAeB7qPNIo+242hUvVTm5Qky8ceGLDVblGYSZb7A==} + peerDependencies: + react: ^16 || ^17 || ^18 + zod: ^1 || ^2 || ^3 + + '@usewaypoint/block-divider@0.0.4': + resolution: {integrity: sha512-q54ydWvKdg7Zwc4hzIwE6i/mC8dFYxfPRACEEEyu2dvSNa9cbKFIsPD9ipVSntK+Ib3Ml84uT4aHQmOlzP6hZA==} + peerDependencies: + react: ^16 || ^17 || ^18 + zod: ^1 || ^2 || ^3 + + '@usewaypoint/block-heading@0.0.3': + resolution: {integrity: sha512-1dMrf1U34nq2FuwTUfsq+hBOdLQz1H+lVMEH9xvyCq5I7nSXCzpeo7QgumZ3zZEHtu3QgSEGafJaZyrj2paC0w==} + peerDependencies: + react: ^16 || ^17 || ^18 + zod: ^1 || ^2 || ^3 + + '@usewaypoint/block-html@0.0.3': + resolution: {integrity: sha512-ZI9oYDibMzs5y/YzfvUwuUBzHDKHOIjiStiVCvlmIA+VtJTycqT8X/ECjn+KmwesLTg5DhG07CC4WY2SL3AnJw==} + peerDependencies: + react: ^16 || ^17 || ^18 + zod: ^1 || ^2 || ^3 + + '@usewaypoint/block-image@0.0.5': + resolution: {integrity: sha512-b66jAXF79idsrIRc2QoBlZctIXdqg/qOAL7/QvKvENZH2KmuXoZhEUx+Z7sACvEQD/VI0u7TK5msDsA5S0/oVQ==} + peerDependencies: + react: ^16 || ^17 || ^18 + zod: ^1 || ^2 || ^3 + + '@usewaypoint/block-spacer@0.0.3': + resolution: {integrity: sha512-CCcMtwcpeC2rHvawQdh5f0Hez7o4xA/edWl/6I3RuA6Yb6STyyrGjmPFs2ZxHQsLOGUK+0OvBenuHlSTCZwuuA==} + peerDependencies: + react: ^16 || ^17 || ^18 + zod: ^1 || ^2 || ^3 + + '@usewaypoint/block-text@0.0.7': + resolution: {integrity: sha512-gbwzlUy0u47S/D+//9feYFlGvd8Py3Gx72Ldst/eyCGWFxBLRTwbbN1xpKTGQGR3VmDZd+asNYdX563PLkPX6w==} + peerDependencies: + react: ^16 || ^17 || ^18 + zod: ^1 || ^2 || ^3 + + '@usewaypoint/document-core@0.0.6': + resolution: {integrity: sha512-Hg10gszVCZRJhA4nIWwAi2rTXuoxPL+ATMe0hU243PFBIUZOwDIQus4XZSeoHsenMCq1uBFCRiFW4hl2+tVwgA==} + peerDependencies: + react: ^16 || ^17 || ^18 + zod: ^1 || ^2 || ^3 + + '@usewaypoint/email-builder@0.0.9': + resolution: {integrity: sha512-cw7C1u0YQBy2glhGyK39Vi8hVWlHZjQG5uYGsYc+aw1+tPdEv+xqle0zJffcmbASy97ybQwSmYjkdy7luagMsA==} + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + zod: ^1 || ^2 || ^3 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -742,6 +863,9 @@ packages: array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + assignment@2.0.0: + resolution: {integrity: sha512-naMULXjtgCs9SVUEtyvJNt68aF18em7/W+dhbR59kbz9cXWPEvUkCun2tqlgqRPSqZaKPpqLc5ZnwL8jVmJRvw==} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -899,6 +1023,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-uri-to-buffer@6.0.2: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} @@ -1162,6 +1289,10 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + he@0.5.0: + resolution: {integrity: sha512-DoufbNNOFzwRPy8uecq+j+VCPQ+JyDelHTmSgygrA5TsR8Cbw4Qcir5sGtWiusB4BdT89nmlaVDhSJOqC/33vw==} + hasBin: true + hosted-git-info@8.1.0: resolution: {integrity: sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==} engines: {node: ^18.17.0 || >=20.5.0} @@ -1223,6 +1354,9 @@ packages: peerDependencies: '@types/node': '>=18' + insane@2.6.2: + resolution: {integrity: sha512-BqEL1CJsjJi+/C/zKZxv31zs3r6zkLH5Nz1WMFb7UBX2KHY2yXDpbFTSEmNHzomBbGDysIfkTX55A0mQZ2CQiw==} + interpret@1.4.0: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} @@ -1419,6 +1553,10 @@ packages: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -1430,6 +1568,11 @@ packages: resolution: {integrity: sha512-Lci/1in+elqZ589PXnfP/iwZXpwQifTM94WJRQwG2tZSdfY7NfB/aUaTARHrohWCgHlXoabeaeXRDOnF5X9JQw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + marked@12.0.2: + resolution: {integrity: sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==} + engines: {node: '>= 18'} + hasBin: true + meow@13.2.0: resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} @@ -1641,6 +1784,15 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -1703,6 +1855,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -1972,6 +2127,9 @@ packages: resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} engines: {node: '>=18'} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + snapshots: '@azure-rest/core-client@2.8.0': @@ -2579,6 +2737,17 @@ snapshots: dependencies: parse-path: 7.1.0 + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.31)': + dependencies: + '@types/react': 18.3.31 + + '@types/react@18.3.31': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + '@typespec/ts-http-runtime@0.3.8': dependencies: http-proxy-agent: 7.0.2 @@ -2587,6 +2756,80 @@ snapshots: transitivePeerDependencies: - supports-color + '@usewaypoint/block-avatar@0.0.3(react@18.3.1)(zod@3.25.76)': + dependencies: + react: 18.3.1 + zod: 3.25.76 + + '@usewaypoint/block-button@0.0.3(react@18.3.1)(zod@3.25.76)': + dependencies: + react: 18.3.1 + zod: 3.25.76 + + '@usewaypoint/block-columns-container@0.0.3(react@18.3.1)(zod@3.25.76)': + dependencies: + react: 18.3.1 + zod: 3.25.76 + + '@usewaypoint/block-container@0.0.2(react@18.3.1)(zod@3.25.76)': + dependencies: + react: 18.3.1 + zod: 3.25.76 + + '@usewaypoint/block-divider@0.0.4(react@18.3.1)(zod@3.25.76)': + dependencies: + react: 18.3.1 + zod: 3.25.76 + + '@usewaypoint/block-heading@0.0.3(react@18.3.1)(zod@3.25.76)': + dependencies: + react: 18.3.1 + zod: 3.25.76 + + '@usewaypoint/block-html@0.0.3(react@18.3.1)(zod@3.25.76)': + dependencies: + react: 18.3.1 + zod: 3.25.76 + + '@usewaypoint/block-image@0.0.5(react@18.3.1)(zod@3.25.76)': + dependencies: + react: 18.3.1 + zod: 3.25.76 + + '@usewaypoint/block-spacer@0.0.3(react@18.3.1)(zod@3.25.76)': + dependencies: + react: 18.3.1 + zod: 3.25.76 + + '@usewaypoint/block-text@0.0.7(react@18.3.1)(zod@3.25.76)': + dependencies: + insane: 2.6.2 + marked: 12.0.2 + react: 18.3.1 + zod: 3.25.76 + + '@usewaypoint/document-core@0.0.6(react@18.3.1)(zod@3.25.76)': + dependencies: + react: 18.3.1 + zod: 3.25.76 + + '@usewaypoint/email-builder@0.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76)': + dependencies: + '@usewaypoint/block-avatar': 0.0.3(react@18.3.1)(zod@3.25.76) + '@usewaypoint/block-button': 0.0.3(react@18.3.1)(zod@3.25.76) + '@usewaypoint/block-columns-container': 0.0.3(react@18.3.1)(zod@3.25.76) + '@usewaypoint/block-container': 0.0.2(react@18.3.1)(zod@3.25.76) + '@usewaypoint/block-divider': 0.0.4(react@18.3.1)(zod@3.25.76) + '@usewaypoint/block-heading': 0.0.3(react@18.3.1)(zod@3.25.76) + '@usewaypoint/block-html': 0.0.3(react@18.3.1)(zod@3.25.76) + '@usewaypoint/block-image': 0.0.5(react@18.3.1)(zod@3.25.76) + '@usewaypoint/block-spacer': 0.0.3(react@18.3.1)(zod@3.25.76) + '@usewaypoint/block-text': 0.0.7(react@18.3.1)(zod@3.25.76) + '@usewaypoint/document-core': 0.0.6(react@18.3.1)(zod@3.25.76) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zod: 3.25.76 + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: acorn: 8.18.0 @@ -2624,6 +2867,8 @@ snapshots: array-ify@1.0.0: {} + assignment@2.0.0: {} + ast-types@0.13.4: dependencies: tslib: 2.8.1 @@ -2792,6 +3037,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.2.3: {} + data-uri-to-buffer@6.0.2: {} debug@4.4.3: @@ -3111,6 +3358,8 @@ snapshots: dependencies: function-bind: 1.1.2 + he@0.5.0: {} + hosted-git-info@8.1.0: dependencies: lru-cache: 10.4.3 @@ -3170,6 +3419,11 @@ snapshots: run-async: 3.0.0 rxjs: 7.8.2 + insane@2.6.2: + dependencies: + assignment: 2.0.0 + he: 0.5.0 + interpret@1.4.0: {} ip-address@10.5.0: {} @@ -3335,12 +3589,18 @@ snapshots: chalk: 5.4.1 is-unicode-supported: 1.3.0 + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@10.4.3: {} lru-cache@7.18.3: {} macos-release@3.5.1: {} + marked@12.0.2: {} + meow@13.2.0: {} merge-stream@2.0.0: {} @@ -3557,6 +3817,16 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -3641,6 +3911,10 @@ snapshots: safer-buffer@2.1.2: {} + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + semver@7.6.3: {} semver@7.8.5: {} @@ -3859,3 +4133,5 @@ snapshots: yoctocolors-cjs@2.1.3: {} yoctocolors@2.2.0: {} + + zod@3.25.76: {}