Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ ESLint and Prettier both run in CI. Tests are Vitest, not Jest. `markup-generato

```
src/index.ts public exports only — consumers import the package root
src/runtime/types.ts template, context, trace, and job types
src/runtime/types.ts template, context, and trace types
src/runtime/render.ts public re-exports
src/runtime/renderTemplate.ts renderTemplate from @llazyemail/render-template
src/runtime/api.ts defineTemplate, renderMany, slot, hasSlot, RenderError
src/runtime/api.ts defineTemplate, slot, hasSlot
src/runtime/errors.ts DisplayError, codes, and message catalog
src/runtime/explain.ts explain()
src/templates/email/email.ts defineEmailTemplate, renderEmail
Expand Down Expand Up @@ -76,7 +76,7 @@ Each part receives one context: `templateId`, the same props object the caller p

`explain` prints a short status report. A thrown `RenderError` prints its message. A result prints `ok` or `failed`, the part count, and the HTML length. Each part is one line: id, status, chars, ms, and the error when there is one. Full HTML stays on `parts[].html`.

`renderMany` runs jobs that share one props type. Throw mode stops the batch at the first broken job, and later jobs do not run. Pass `onError: 'collect'` when one broken template must not hide the others. Jobs may omit `props` (`{}`) and `slots`.
`renderMany`, `RenderJob`, `RenderOptions`, and `RenderError` come from `@llazyemail/render-template`. This package re-exports them. `renderMany` runs jobs that share one props type. Throw mode stops the batch at the first broken job, and later jobs do not run. Pass `onError: 'collect'` when one broken template must not hide the others. Jobs may omit `props` (`{}`) and `slots`.

Custom code is a part, or a slot that a part reads with `slot(ctx, id)`. A missing slot is `''`. `hasSlot` is true for an empty string. A function slot is called with the same context. `null` or `undefined` from that function is `''`. A non-string slot, or a non-string return, throws `DisplayError` (`slot_type` or `slot_return`) and the part that called `slot` is the part that fails. Package-owned messages are built in `src/runtime/errors.ts`. Render and validation failures stay `RenderError`.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const result = renderEmail(template, props, { onError: 'collect' });
explain(result);
```

`collect` records the broken part, uses `''` for it, and continues. `result.ok` is false. Full HTML for every part that ran stays on `result.parts[].html`. `renderMany` uses the same switch. In throw mode the first broken job stops the batch. Pass `collect` when one failure must not hide the rest. Jobs in one batch share a props type.
`collect` records the broken part, uses `''` for it, and continues. `result.ok` is false. Full HTML for every part that ran stays on `result.parts[].html`. `renderMany` is `@llazyemail/render-template`'s batch helper, re-exported here, and uses the same switch. In throw mode the first broken job stops the batch. Pass `collect` when one failure must not hide the rest. Jobs in one batch share a props type.

## A document that is not the email shell

Expand Down
2 changes: 2 additions & 0 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ A function slot is called with the same context. `null` and `undefined` become `

## renderMany(jobs, options?): RenderResult[]

Re-exported from `@llazyemail/render-template`, along with `RenderJob`, `RenderOptions`, and `RenderError`. This package does not define them.

Every job shares `TProps`. A job is `{ template, props?, slots? }`. Omitted props become `{}`. `options` is only `{ onError? }`.

`throw` (default) lets the first `RenderError` escape. Later jobs do not run. `collect` returns one result per job, including failures.
Expand Down
2 changes: 1 addition & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ explain(result);
result.html;
```

`renderTemplate` and `defineTemplate` are the same runtime without the email shell. `renderMany` batches jobs that share one props type. Default `onError` is `throw` (`RenderError` with `partId`, `trace`, and `cause`). `collect` keeps going and sets `ok: false`.
`renderTemplate` and `defineTemplate` are the same runtime without the email shell. `renderMany`, `RenderJob`, `RenderOptions`, and `RenderError` are re-exported from `@llazyemail/render-template`. `renderMany` batches jobs that share one props type. Default `onError` is `throw` (`RenderError` with `partId`, `trace`, and `cause`). `collect` keeps going and sets `ok: false`.

Fragment helpers `head`, `main`, `footer`, `body`, `content`, and `document` return strings. Omitted strings are empty. The button needs both `ctaLabel` and `ctaUrl`. Unsubscribe needs `unsubscribeUrl`. Props and slots are raw HTML. `document` uses `<html lang="en">`.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 2 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,23 @@ export {
defineTemplate,
displayErrors,
hasSlot,
renderMany,
renderTemplate,
slot,
DisplayError,
DisplayErrorCode,
RenderError,
} from './runtime/render';
export { renderMany, RenderError } from '@llazyemail/render-template';
export type {
PartFailure,
PartStatus,
PartTrace,
RenderContext,
RenderJob,
RenderOptions,
RenderResult,
SlotValue,
Template,
TemplatePart,
} from './runtime/types';
export type { RenderJob, RenderOptions } from '@llazyemail/render-template';
export { explain } from './runtime/explain';

export { defineEmailTemplate, renderEmail } from './templates/email/email';
Expand Down
18 changes: 1 addition & 17 deletions src/runtime/api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { assertTemplate, type Template as PackageTemplate } from '@llazyemail/render-template';
import type { RenderContext, RenderJob, RenderOptions, RenderResult, Template } from './types';
import type { RenderContext, Template } from './types';
import { displayErrors } from './errors';
import { renderTemplate } from './renderTemplate';

export { RenderError } from '@llazyemail/render-template';
export { DisplayError, DisplayErrorCode, displayErrors } from './errors';

/**
Expand All @@ -21,20 +19,6 @@ export function defineTemplate<TProps>(template: Template<TProps>): Template<TPr
});
}

/** Render many templates. Use `onError: 'collect'` so one broken template does not stop the batch. */
export function renderMany<TProps>(
jobs: readonly RenderJob<TProps>[],
options?: Pick<RenderOptions<TProps>, 'onError'>,
): RenderResult[] {
return jobs.map((job) => {
const next: RenderOptions<TProps> = {};
if (options?.onError !== undefined) next.onError = options.onError;
if (job.slots !== undefined) next.slots = job.slots;
const props = job.props === undefined ? ({} as TProps) : job.props;
return renderTemplate(job.template, props, next);
});
}

/** Read a slot. A missing slot is `''`. A function slot is called with the same context. */
export function slot<TProps>(ctx: RenderContext<TProps>, id: string): string {
if (!hasSlot(ctx, id)) return '';
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/explain.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RenderError } from './render';
import { RenderError } from '@llazyemail/render-template';
import type { RenderResult } from './types';

/** One text block for logs. Full HTML stays on each part trace. */
Expand Down
2 changes: 0 additions & 2 deletions src/runtime/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ export { renderTemplate } from './renderTemplate';
export {
defineTemplate,
hasSlot,
renderMany,
slot,
RenderError,
DisplayError,
DisplayErrorCode,
displayErrors,
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/renderTemplate.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
renderTemplate as renderPackageTemplate,
type RenderOptions as PackageRenderOptions,
type RenderOptions,
type RenderResult as PackageRenderResult,
type Template as PackageTemplate,
} from '@llazyemail/render-template';
import type { RenderOptions, RenderResult, Template } from './types';
import type { RenderResult, Template } from './types';

/**
* Render one template. The same template and props produce the same HTML.
Expand All @@ -18,6 +18,6 @@ export function renderTemplate<TProps>(
return renderPackageTemplate(
template as PackageTemplate<TProps>,
props,
options as PackageRenderOptions<TProps>,
options,
) as PackageRenderResult as RenderResult;
}
20 changes: 2 additions & 18 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** HTML string, or a function that receives the same context as a part. */
export type SlotValue<TProps> = string | ((ctx: RenderContext<TProps>) => string);
export type SlotValue<TProps> =
string | ((ctx: RenderContext<TProps>) => string | null | undefined);

/** One named step. The id is the name that shows up when this step breaks. */
export interface TemplatePart<TProps> {
Expand Down Expand Up @@ -50,20 +51,3 @@ export interface RenderResult {
/** Set when `compose` itself throws and `onError` is `collect`. */
composeError?: PartFailure;
}

export interface RenderOptions<TProps> {
/**
* `throw` stops at the first broken part and throws `RenderError`.
* `collect` records the error, uses `''` for that part, and continues.
* Later templates in `renderMany` still run.
*/
onError?: 'throw' | 'collect';
slots?: Readonly<Record<string, SlotValue<TProps>>>;
}

/** One template in a batch. Props are whatever that template declared. */
export interface RenderJob<TProps> {
template: Template<TProps>;
props?: TProps;
slots?: Readonly<Record<string, SlotValue<TProps>>>;
}
3 changes: 2 additions & 1 deletion src/templates/email/email.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { RenderOptions } from '@llazyemail/render-template';
import { footer } from '../footer/footer';
import { head } from '../head/head';
import { main } from '../main/main';
import { defineTemplate, renderTemplate } from '../../runtime/render';
import type { RenderOptions, RenderResult, Template } from '../../runtime/types';
import type { RenderResult, Template } from '../../runtime/types';
import { pickDefined } from '../../utils/pickDefined';
import { composeEmail, shellPart } from './shell';
import type { EmailProps, EmailTemplate } from './types';
Expand Down
Loading