Skip to content
Closed
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
8 changes: 4 additions & 4 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"unplugin-vue-markdown": "^32.0.0",
"uqr": "^0.1.3",
"vite": "^8.0.16",
"vue-component-type-helpers": "^3.3.6",
"vue-router": "^5.0.2"
},
"peerDependencies": {
Expand Down
16 changes: 15 additions & 1 deletion src/render/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createPlaintext } from '../plaintext.ts'
import { stripForHtml, stripForPlaintext } from '../utils/output-markers.ts'
import defu from 'defu'
import type { Component } from 'vue'
import type { ComponentProps } from 'vue-component-type-helpers'
import type { MaizzleConfig } from '../types/index.ts'
import { createRenderer } from './createRenderer.ts'
import { getActiveRenderer } from './active.ts'
Expand All @@ -19,13 +20,26 @@ export interface RenderResult {
plaintext?: string
}

/**
* Render a Vue component to a fully-transformed HTML string.
*/
export async function render<TComponent extends Component>(
component: TComponent,
config?: Partial<MaizzleConfig<ComponentProps<TComponent>>>,
): Promise<RenderResult>;

/**
* Render a Vue SFC email template to a fully-transformed HTML string.
* Accepts a file path or a raw SFC source string.
*/
export async function render(
template: string | Component,
template: string,
config?: Partial<MaizzleConfig>,
): Promise<RenderResult>;

export async function render<TComponent extends Component>(
template: string | TComponent,
config?: Partial<MaizzleConfig<ComponentProps<TComponent>>>,
Comment on lines +26 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for render() call sites that might pass a union-typed template variable
rg -nP -C4 '\brender\s*\(' src --type=ts -g '!**/tests/**'

Repository: maizzle/framework

Length of output: 9298


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the public render overloads, the Component type, and any call sites/usages
sed -n '1,140p' src/render/index.ts
printf '\n--- Component type ---\n'
rg -n "export (type|interface) Component|type Component|interface Component" src -g '*.ts'
printf '\n--- render() call sites/usages ---\n'
rg -n "\brender\s*\(" src test tests examples docs -g '*.ts' -g '*.tsx' -g '*.js' -g '*.mjs' -g '*.cjs' -g '*.md'
printf '\n--- union-typed candidates near render calls ---\n'
rg -n -C 2 ":\s*string\s*\|\s*Component|:\s*Component\s*\|\s*string|as\s*string\s*\|\s*Component|as\s*Component\s*\|\s*string" src test tests examples docs -g '*.ts' -g '*.tsx'

Repository: maizzle/framework

Length of output: 42280


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for explicit union-typed template variables that could hit the overload gap
rg -n -C 2 "string\s*\|\s*Component|Component\s*\|\s*string" src src/tests -g '*.ts' -g '*.tsx'

printf '\n--- render() call sites with local template variables ---\n'
rg -n -C 2 "const\s+\w+\s*=\s*.*\n.*\brender\s*\(\s*\w+\s*[,)]" src src/tests -g '*.ts' -g '*.tsx'

Repository: maizzle/framework

Length of output: 1230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find explicit union-typed variables/usages relevant to render()
rg -n -C 2 "string\s*\|\s*Component|Component\s*\|\s*string" src src/tests -g '*.ts' -g '*.tsx' || true

printf '\n--- render() calls with non-literal arguments ---\n'
rg -n -C 1 "\brender\s*\(\s*[A-Za-z_][A-Za-z0-9_]*\s*[,)]" src src/tests -g '*.ts' -g '*.tsx' || true

Repository: maizzle/framework

Length of output: 3524


Keep a union-compatible overload. The split overloads reject callers whose template variable is typed string | Component, so existing TypeScript consumers with a union-typed template will stop compiling. Add an overload that accepts the union directly, or keep the previous broad signature.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/render/index.ts` around lines 26 - 42, The overload split in render is
rejecting union-typed callers, so add a union-compatible overload for render
that accepts template as string | Component (or restore the broader signature)
while keeping the existing string and generic Component overloads. Make sure the
new signature is alongside the render function declarations so TypeScript can
resolve callers whose template variable is typed as a union.

): Promise<RenderResult> {
if (template == null) {
throw new Error(
Expand Down
25 changes: 25 additions & 0 deletions src/tests/render/props.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { defineComponent, h, type PropType } from 'vue'
import { rmSync } from 'node:fs'
import { render } from '../../render/index.ts'
import { createTempProject } from './_helpers.ts'
Expand Down Expand Up @@ -32,6 +33,30 @@ describe('render props', () => {
expect(result.html).toContain('Hello Ava')
})

it('passes props to strongly typed component', async () => {
const component = defineComponent({
props: {
title: {
type: String as PropType<'Mr' | 'Ms' | 'Mx'>,
required: true,
},
name: {
type: String as PropType<string>,
required: true,
},
},
setup(props) {
return () => h('div', `Hello ${props.title} ${props.name}`)
}
})

const result = await render(component, {
props: { title: 'Mx', name: 'Ava' },
})

expect(result.html).toContain('Hello Mx Ava')
})

it('does not leak props into useConfig() or the returned config', async () => {
const result = await render(`
<script setup>
Expand Down
4 changes: 2 additions & 2 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ export type ComponentSource =
pathPrefix?: boolean
}

export interface MaizzleConfig {
export interface MaizzleConfig<TProps = Record<string, any>> {
/**
* Root directory for the Maizzle email project.
*
Expand Down Expand Up @@ -755,7 +755,7 @@ export interface MaizzleConfig {
* @example
* render('./welcome.vue', { props: { name: 'Ava', plan: 'Pro' } })
*/
props?: Record<string, any>
props?: TProps

// Events

Expand Down
Loading