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
5 changes: 3 additions & 2 deletions src/render/file-adapter.ts → src/adapters/file-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { pathExists } from 'markup-generator';
import { GenerateTemplateError } from '../errors';
import { serializePayload } from '../payload';
import type { RenderContext } from '../types';

/** Legacy adapter. Not part of the core render path. Requires allowFileTemplates. */
/** Opt-in spawn path. Engine must not import this unless allowFileTemplates. */
export function renderFromFile(ctx: RenderContext): string {
if (!ctx.entry.file) {
throw new GenerateTemplateError(
Expand All @@ -17,7 +18,7 @@ export function renderFromFile(ctx: RenderContext): string {
}

const modulePath = path.join(ctx.templatesDir, ctx.entry.file);
if (!fs.existsSync(modulePath)) {
if (!pathExists(modulePath)) {
throw new GenerateTemplateError(
'RENDER_FAILED',
`Template file missing: ${path.relative(ctx.root, modulePath)}`,
Expand Down
27 changes: 27 additions & 0 deletions src/adapters/list-files.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { listTemplateFiles } from './list-files';

const tempDirs: string[] = [];

afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});

describe('listTemplateFiles adapter', () => {
it('lists ts/js files and skips registry names', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gt-list-'));
tempDirs.push(dir);
fs.writeFileSync(path.join(dir, 'hello.ts'), '');
fs.writeFileSync(path.join(dir, 'index.js'), '');
expect(listTemplateFiles(dir, { skipFiles: ['index.js'] })).toEqual(['hello.ts']);
});

it('returns [] when the directory is missing', () => {
expect(listTemplateFiles('/nonexistent/templates')).toEqual([]);
});
});
9 changes: 6 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { pathExists } from 'markup-generator';
import { listTemplateFiles } from './adapters/list-files';
import { createGenerator, TemplateGenerator } from './generator';
import type { CliArgs, GenerateRequest } from './types';

Expand Down Expand Up @@ -42,7 +43,7 @@ export function requestsFromArgs(args: CliArgs, generator: TemplateGenerator): G
export async function main(argv = process.argv.slice(2), generator?: TemplateGenerator): Promise<void> {
const args = parseArgs(argv);
const gen = generator ?? createGenerator();
const templateFiles = gen.listTemplateFiles();
const templateFiles = listTemplateFiles(gen.templatesDir, { skipFiles: gen.skipFiles });

if (args.list) {
console.log(`Files in ${path.relative(gen.root, gen.templatesDir) || gen.templatesDir}:`);
Expand All @@ -52,7 +53,9 @@ export async function main(argv = process.argv.slice(2), generator?: TemplateGen
console.log(' (No catalog configured — pass createGenerator({ catalog }) from your project)');
} else {
gen.catalog.forEach((entry) => {
const exists = entry.render ? true : Boolean(entry.file && fs.existsSync(path.join(gen.templatesDir, entry.file)));
const exists = entry.render
? true
: Boolean(entry.file && pathExists(path.join(gen.templatesDir, entry.file)));
const source = entry.render ? 'renderer' : entry.file ?? '(no source)';
console.log(` ${entry.ids.join(' | ')} <- ${source}${exists ? '' : ' (missing)'}`);
});
Expand Down
11 changes: 1 addition & 10 deletions src/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe('createGenerator', () => {
});

expect(gen.find('WELCOME')?.ids).toContain('welcome');
expect(gen.ids()).toEqual(['welcome', 'WelcomeEmail']);
expect(await gen.render('welcome')).toBe('<h1>Hello Alex</h1>');
expect(await gen.render('WelcomeEmail', { payload: { name: 'Sam' } })).toBe('<h1>Hello Sam</h1>');
});
Expand Down Expand Up @@ -61,7 +62,6 @@ describe('createGenerator', () => {
});

expect(gen.templatesDir).toBe(path.join(root, 'emails'));
expect(gen.listTemplateFiles()).toEqual(['hello.ts']);
});

it('writes rendered html through markup-generator', async () => {
Expand All @@ -85,13 +85,4 @@ describe('createGenerator', () => {

await expect(gen.render('welcome', { payload: {} })).rejects.toThrow(/Unknown template id/);
});

it('handles missing templates directory gracefully', () => {
const gen = createGenerator({
templatesDir: '/nonexistent/templates',
catalog: [{ ids: ['test'], render: () => 'test' }],
});

expect(gen.listTemplateFiles()).toEqual([]);
});
});
8 changes: 4 additions & 4 deletions src/generator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'node:path';
import { Catalog } from './catalog';
import { listTemplateFiles as listTemplateFilesOnDisk } from './adapters/list-files';
import { renderFromFile } from './adapters/file-renderer';
import { PayloadSource } from './payload';
import { renderEntry } from './render';
import { Writer } from './writer';
Expand Down Expand Up @@ -68,8 +68,8 @@ export class TemplateGenerator {
return this.catalogPort.slug(templateId);
}

listTemplateFiles(): string[] {
return listTemplateFilesOnDisk(this.templatesDir, { skipFiles: this.skipFiles });
ids(): string[] {
return this.catalog.flatMap((entry) => entry.ids);
}

loadPayload(templateId: string, dataPath?: string): Promise<unknown> {
Expand All @@ -84,8 +84,8 @@ export class TemplateGenerator {
catalog: this.catalog,
templatesDir: this.templatesDir,
root: this.root,
allowFileTemplates: this.allowFileTemplates,
reviveDates: options.reviveDates ?? this.reviveDates,
fileRenderer: this.allowFileTemplates ? renderFromFile : undefined,
});
}

Expand Down
16 changes: 8 additions & 8 deletions src/render.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import { GenerateTemplateError } from './errors';
import { renderFromFile } from './render/file-adapter';
import { renderInProcess } from './render/in-process';
import { availableIds, findEntry } from './resolve';
import { reviveDates } from './payload';
import type { TemplateCatalogEntry } from './types';
import type { RenderContext, TemplateCatalogEntry } from './types';

export { invokeRenderer } from './render/in-process';
export { renderFromFile } from './render/file-adapter';

export type FileRenderer = (ctx: RenderContext) => string | Promise<string>;

export async function renderEntry(options: {
templateId: string;
payload: unknown;
catalog: TemplateCatalogEntry[];
templatesDir: string;
root: string;
allowFileTemplates?: boolean;
reviveDates?: boolean;
fileRenderer?: FileRenderer;
}): Promise<string> {
const {
templateId,
payload,
catalog,
templatesDir,
root,
allowFileTemplates = false,
reviveDates: shouldRevive = false,
fileRenderer,
} = options;
const entry = findEntry(catalog, templateId);
if (!entry) {
Expand All @@ -35,7 +35,7 @@ export async function renderEntry(options: {
);
}

const ctx = {
const ctx: RenderContext = {
templateId,
payload: shouldRevive ? reviveDates(payload) : payload,
entry,
Expand All @@ -48,14 +48,14 @@ export async function renderEntry(options: {
}

if (entry.file) {
if (!allowFileTemplates) {
if (!fileRenderer) {
throw new GenerateTemplateError(
'RENDER_FAILED',
`Template "${templateId}" uses a file adapter. Pass allowFileTemplates: true or inject a render function.`,
templateId
);
}
return renderFromFile(ctx);
return fileRenderer(ctx);
}

throw new GenerateTemplateError(
Expand Down
Loading