@maizzle/framework
Version:
Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.
768 lines (767 loc) • 23 kB
TypeScript
import { RemoveValue } from "../plugins/postcss/removeDeclarations.js";
import { TemplateInfo } from "../events/index.js";
import { Directive, Plugin } from "vue";
import { InlineConfig } from "vite";
import juice from "juice";
import { Options } from "unplugin-vue-markdown/types";
//#region src/types/config.d.ts
type JuiceOptions = NonNullable<Parameters<typeof juice>[1]>;
interface UrlQueryOptions {
/**
* CSS selectors for elements to process.
*
* @default ['a']
*/
tags?: string[];
/**
* HTML attributes containing URLs to append query params to.
*
* @default ['src', 'href', 'poster', 'srcset', 'background']
*/
attributes?: string[];
/**
* When `true`, only appends query params to absolute URLs.
*
* @default true
*/
strict?: boolean;
/**
* Options forwarded to the `query-string` library for controlling serialization.
*
* @default { encode: false }
*/
qs?: Record<string, unknown>;
}
type UrlQuery = Record<string, unknown> & {
_options?: UrlQueryOptions;
};
interface UrlConfig {
/**
* Append query parameters to URLs in your HTML.
*
* @example
* url: {
* query: {
* utm_source: 'maizzle',
* utm_medium: 'email',
* }
* }
*/
query?: UrlQuery;
/**
* Prepend a base URL to relative paths.
*
* Pass a string to prepend to all tags, or an object for fine-grained control.
*
* @example
* url: {
* base: 'https://cdn.example.com/emails/',
* }
*/
base?: string | {
/** The base URL to prepend. */url?: string; /** Tags or tag-attribute map to process. */
tags?: string[] | Record<string, Record<string, string | boolean>>; /** Attributes to process. */
attributes?: Record<string, string>; /** Also apply to URLs in `<style>` tags. */
styleTag?: boolean; /** Also apply to URLs in inline `style` attributes. */
inlineCss?: boolean;
};
}
interface CssConfig {
/**
* Base directory for Tailwind CSS `@source` resolution.
*
* Automatically set to `root` when `root` is configured.
*/
base?: string;
/**
* Remove unused CSS.
*
* Set to `true` to enable with defaults, or pass an options object.
*
* @default false
*/
purge?: boolean | Record<string, unknown>;
/**
* Inline CSS from `<style>` tags into matching HTML elements.
*
* Set to `true` to enable with defaults, or pass an options object for fine-grained control.
*
* @example
* css: {
* inline: {
* removeStyleTags: true,
* applyWidthAttributes: true,
* }
* }
*/
inline?: boolean | JuiceOptions & {
/**
* Convert HTML attributes like `width`, `height`, `bgcolor`, and `valign`
* to inline CSS styles. Set to `true` for all, or pass an array of attribute names.
*
* @default false
*/
attributeToStyle?: boolean | string[];
/**
* Convert `0px`, `0em` etc. to `0` in inline styles.
*
* @default true
*/
preferUnitlessValues?: boolean;
/**
* CSS selectors to preserve in `<style>` tags, even after inlining.
* Mapped to Juice's `preservedSelectors` option.
*
* @default []
*/
safelist?: string[];
/**
* Duplicate CSS properties to HTML attributes.
* Mapped to Juice's static `styleToAttribute` property.
*
* @default {}
*
* @example
* styleToAttribute: {
* 'background-color': 'bgcolor',
* }
*/
styleToAttribute?: Record<string, string>;
/**
* Elements that can receive `width` HTML attributes.
* Mapped to Juice's static `widthElements` property.
*
* @default ['img', 'video']
*/
widthElements?: string[];
/**
* Elements that can receive `height` HTML attributes.
* Mapped to Juice's static `heightElements` property.
*
* @default ['img', 'video']
*/
heightElements?: string[];
/**
* CSS properties to exclude from inlining.
* Mapped to Juice's static `excludedProperties` property.
*
* @default []
*/
excludedProperties?: string[];
/**
* Template language code blocks to preserve during inlining.
* Mapped to Juice's static `codeBlocks` property.
*
* @default { EJS: { start: '<%', end: '%>' }, HBS: { start: '\{\{', end: '}}' } }
*/
codeBlocks?: Record<string, {
start: string;
end: string;
}>;
/**
* Additional CSS string to inline alongside `<style>` tag contents.
* Mapped to Juice's `extraCss` option.
*/
customCSS?: string;
};
/**
* Merge duplicate `@media` queries and sort them.
*
* Enabled by default. Set to `false` to disable, or pass an object to control sort order.
*
* @default true
*
* @example
* css: {
* media: { sort: 'desktop-first' },
* }
*/
media?: boolean | {
/**
* Sort order for media queries.
*
* @default 'mobile-first'
*/
sort?: 'mobile-first' | 'desktop-first' | ((a: string, b: string) => number);
};
/**
* Strip units from zero values in inlined styles.
*
* For example, `padding: 0px 16px` becomes `padding: 0 16px`.
*
* @default true
*/
preferUnitless?: boolean;
/**
* Replace unsafe CSS class names with email-safe equivalents.
*
* @default true
*/
safe?: boolean | Record<string, string>;
/**
* Rewrite longhand CSS to shorthand where possible.
*
* For example, `padding: 10px 20px 10px 20px` becomes `padding: 10px 20px`.
*
* @default false
*/
shorthand?: boolean | {
tags?: string[];
};
/**
* Convert 3-digit HEX colors to 6-digit in `bgcolor` and `color` attributes.
*
* Some email clients don't support shorthand HEX like `#fff`.
*
* @default true
*/
sixHex?: boolean;
/**
* Remove specific CSS declarations by selector.
*
* @example
* css: {
* removeDeclarations: {
* ':root': '*',
* }
* }
*/
removeDeclarations?: Record<string, RemoveValue>;
/**
* Glob patterns or paths excluded from Tailwind's `@source` scanner.
*
* Tailwind won't generate utilities for classes used in these files.
* Useful for ignoring AMP variants or any templates whose classes
* shouldn't end up in the output CSS.
*
* @example
* css: {
* exclude: ['emails/amp/**'],
* }
*/
exclude?: string[];
}
interface AttributesConfig {
/**
* Add attributes to HTML elements.
*
* Set the whole map to `false` to disable auto-add. Set a selector
* entry to `false` to skip that selector. Set an individual attribute
* to `false` to skip just that attribute while keeping the rest.
*
* @example
* html: {
* attributes: {
* add: {
* table: { cellpadding: 0, cellspacing: 0, role: 'none' },
* img: { alt: '' },
* }
* }
* }
*/
add?: false | Record<string, false | Record<string, false | string | boolean | number>>;
/**
* Remove attributes from HTML elements.
*
* Empty `style` and `class` attributes are always stripped, regardless
* of config. Entries added here are appended to those defaults.
*
* - String — remove when the attribute's value is empty.
* - `{ name, value: 'literal' }` — remove when the value matches exactly.
* - `{ name, value: /regex/ }` — remove when the value matches the regex.
*
* @example
* html: {
* attributes: {
* remove: [
* 'data-foo',
* { name: 'role', value: 'none' },
* { name: 'class', value: /^js-/ },
* ],
* }
* }
*/
remove?: Array<string | {
name: string;
value?: string | RegExp;
}>;
}
type EntitiesConfig = boolean | Record<string, string>;
/**
* caniemail.com client family slugs. Maintained manually from
* https://www.caniemail.com/api/data.json — update this list when caniemail
* adds new clients.
*/
type CaniemailClient = 'gmail' | 'outlook' | 'yahoo' | 'apple-mail' | 'aol' | 'thunderbird' | 'microsoft' | 'samsung-email' | 'sfr' | 'orange' | 'protonmail' | 'hey' | 'mail-ru' | 'fastmail' | 'laposte' | 't-online-de' | 'free-fr' | 'gmx' | 'web-de' | 'ionos-1and1' | 'rainloop' | 'wp-pl';
interface ChecksConfig {
/**
* Client families to check against. Defaults to the four majors:
* Gmail, Apple Mail, Outlook, Yahoo. Pass `'all'` to check every client
* in the caniemail dataset.
*/
clients?: CaniemailClient[] | 'all';
/**
* Filter which severities are reported. Omit to show everything.
*
* - `'error'` — only errors (unsupported features, hard lint errors)
* - `'warning'` — only warnings (partial / unknown support, lint warnings)
* - `'lint'` — only lint items (both severities, no compat items)
*/
level?: 'error' | 'warning' | 'lint';
}
interface PostcssConfig {
/**
* Selector prefixes to strip from compiled CSS.
*
* @default [':host', ':lang']
*
* @example
* postcss: {
* removeSelectors: [':host', ':lang', ':root'],
* }
*/
removeSelectors?: string[];
/**
* At-rule names to strip from compiled CSS.
*
* @default ['layer', 'property']
*
* @example
* postcss: {
* removeAtRules: ['layer', 'property', 'charset'],
* }
*/
removeAtRules?: string[];
}
interface HtmlConfig {
/** Configure HTML attribute transformations. */
attributes?: AttributesConfig;
/**
* Decode HTML entities.
*
* Set to `true` to decode all, or pass a map of entities to decode.
*
* @default true
*/
decodeEntities?: EntitiesConfig;
/**
* Pretty-print the HTML output.
*
* Set to `true` to enable with defaults, or pass options.
*/
format?: boolean | import('oxfmt').FormatConfig;
/**
* Minify the HTML output.
*
* Set to `true` to enable with defaults, or pass `html-crush`
* options to customize.
*
* @see https://codsen.com/os/html-crush
*/
minify?: boolean | Partial<import('html-crush').Opts>;
}
type FilterFunction = (str: string, value: string) => string;
type FiltersConfig = false | Record<string, FilterFunction>;
interface MarkdownConfig extends Options {
/**
* The shiki theme to use for syntax highlighting in Markdown fenced code blocks.
*
* @default 'github-light'
*/
shikiTheme?: import('shiki').BundledTheme;
}
interface VueConfig {
/**
* Vue plugins to register on the app instance before rendering.
*
* Pass a factory (`() => Plugin[]`) for stateful plugins like vue-i18n
* or Pinia to get a fresh instance per render — otherwise state leaks
* across templates (e.g. one template setting `locale.value = 'fr'`
* affects the next render).
*/
plugins?: Plugin[] | (() => Plugin[]);
/** Custom Vue directives to register globally. */
directives?: Record<string, Directive>;
/** Properties added to `app.config.globalProperties`, available in all templates. */
globalProperties?: Record<string, unknown>;
}
/**
* Per-transformer toggle map for `useTransformers`.
*
* - `false` skips the listed transformer.
* - `true` force-enables it for this run (only meaningful for boolean-driven
* transformers: inlineCss, purgeCss, prettify, minify, shorthandCss,
* sixHex, safeSelectors, entities). Layers on the matching
* `css.*` / `html.*` config slice.
* - missing keys keep their default behavior.
*
* Data-driven transformers (filters, baseURL, urlQuery, addAttributes,
* removeAttributes, replaceStrings, attributeToStyle) require actual
* config values — a bare `true` is a no-op for them.
*
* Transformers without a toggle (Tailwind, MSO placeholder resolution,
* column width math, link inlining) always run; they're driven by markup
* or framework state, not user opt-in.
*/
interface TransformerToggles {
safeSelectors?: boolean;
attributeToStyle?: boolean;
inlineCss?: boolean;
removeAttributes?: boolean;
shorthandCss?: boolean;
sixHex?: boolean;
addAttributes?: boolean;
filters?: boolean;
baseURL?: boolean;
urlQuery?: boolean;
purgeCss?: boolean;
entities?: boolean;
replaceStrings?: boolean;
prettify?: boolean;
minify?: boolean;
}
/**
* Plaintext generation options.
*
* Control where plaintext files are written, what extension they use,
* and the options forwarded to `string-strip-html`.
*/
interface PlaintextConfig {
/**
* Output directory for plaintext files. When omitted, files are written
* next to their HTML counterpart in `build.output.path`.
*/
destination?: string;
/**
* File extension for plaintext files (without the leading dot).
*
* @default 'txt'
*/
extension?: string;
/**
* Options forwarded to `string-strip-html`.
*
* @see https://codsen.com/os/string-strip-html
*/
options?: Partial<import('string-strip-html').Opts>;
}
/**
* Source directory entry for component auto-import.
*
* String → folder name becomes a namespace prefix automatically
* (e.g. `widgets/Button.vue` → `<WidgetsButton />`).
*
* Object → custom prefix overrides the folder-derived name. With
* `pathPrefix: false`, intermediate subfolders are dropped from the
* resolved name (useful for icon sets organized by category).
*/
type ComponentSource = string | {
/** Directory to scan, resolved relative to `cwd`. */path: string;
/**
* Custom prefix prepended to every resolved component name.
* Empty string disables the auto folder-name prefix.
*/
prefix?: string;
/**
* Include intermediate subfolder names in the resolved component
* name. Defaults to `true`.
*
* @example
* // pathPrefix: true → icons/social/twitter.vue → <IconSocialTwitter />
* // pathPrefix: false → icons/social/twitter.vue → <IconTwitter />
*/
pathPrefix?: boolean;
};
interface MaizzleConfig {
/**
* Root directory for the Maizzle email project.
*
* When set, relative paths for `content`, `static.source`,
* and `css.base` are all resolved relative to this directory.
*
* Defaults to `process.cwd()`.
*
* @example
* maizzle({
* root: 'resources/js/emails',
* content: ['./**\/*.vue'],
* })
*/
root?: string;
/** Options for Markdown template support, extending `unplugin-vue-markdown`. */
markdown?: MarkdownConfig;
/**
* Glob patterns for email template files to process.
*
* Resolved relative to `root`.
*
* @default ['emails/**\/*.{vue,md}']
*/
content?: string[];
/** Output configuration for built email templates. */
output?: {
/**
* Directory to write compiled HTML files to.
*
* @default 'dist'
*/
path?: string;
/**
* File extension for compiled templates.
*
* @default 'html'
*
* @example
* output: {
* extension: 'blade.php',
* }
*/
extension?: string;
};
/**
* Build templates in parallel across worker threads.
*
* - omitted (default): parallel when there are more than 50 templates
* - `true`: always parallel, using min(CPU count − 1, 8) workers
* - `false`: always sequential
* - `{ workers, threshold }`: parallel when the template count exceeds
* `threshold` (default 50), using `workers` threads (default
* min(CPU count − 1, 8)). Set `threshold: 0` to always parallelize.
*
* Note: more workers is not faster — each runs a full renderer, so ~8 is the
* sweet spot and over-provisioning slows builds down.
*
* Only applies to file-based configs (the CLI / a config path); an inline
* config object always builds sequentially. SFC-registered `afterBuild`
* handlers can't run in a worker — use the config `afterBuild` hook instead.
*/
parallel?: boolean | {
workers?: number;
threshold?: number;
};
/** Static file copying configuration. */
static?: {
/**
* Glob patterns for static files to copy to the output directory.
*
* @default ['public/**\/*.*']
*/
source?: string[];
/**
* Subdirectory in the output folder where static files are placed.
*
* @default 'public'
*/
destination?: string;
};
/** Component auto-import configuration. */
components?: {
/**
* Additional directories to scan for auto-imported Vue components.
*
* Resolved relative to `cwd` (not `root`), so paths outside the
* email root directory work as expected.
*
* String entries use the folder name as a namespace prefix
* automatically (e.g. `widgets/Button.vue` → `<WidgetsButton />`).
* Object entries override that with a custom prefix.
*
* @example
* components: {
* source: [
* 'resources/js/components/email',
* { path: 'src/widgets', prefix: 'W' },
* { path: 'src/icons', prefix: 'Icon', pathPrefix: false },
* ],
* }
*/
source?: ComponentSource | ComponentSource[];
};
/** Dev server configuration. */
server?: {
/**
* Port for the dev server.
*
* @default 3000
*/
port?: number;
/**
* Additional file paths to watch for changes.
*
* @default []
*
* @example
* server: {
* watch: ['./tailwind.config.ts'],
* }
*/
watch?: string[];
/**
* Email sending configuration for the "Send test" feature in the dev UI.
*
* When not configured, falls back to Ethereal (free fake SMTP — emails
* are captured and viewable via a URL, never actually delivered).
*
* @example
* server: {
* email: {
* to: ['test@example.com'],
* from: 'dev@maizzle.test',
* transport: {
* host: 'smtp.mailtrap.io',
* port: 587,
* auth: { user: '...', pass: '...' },
* },
* },
* }
*/
email?: {
/** Default recipient(s). */to?: string | string[]; /** Sender address. @default 'Maizzle <maizzle@ethereal.email>' */
from?: string; /** Default subject line. */
subject?: string; /** Nodemailer transport options (SMTP, SES, etc.). Omit to use Ethereal. */
transport?: Record<string, unknown>;
};
/**
* Configure or disable the Checks tab in the dev UI.
*
* Set to `false` to disable checks entirely (the tab is hidden).
*
* @example
* server: {
* checks: {
* clients: ['gmail', 'outlook', 'apple-mail'],
* level: 'error',
* }
* }
*/
checks?: false | ChecksConfig;
};
/** Tailwind CSS and email CSS optimization settings. */
css?: CssConfig;
/**
* Generate a plaintext version of the email.
*
* Set to `true` to enable with defaults, or pass an object to configure
* destination directory, file extension, and `string-strip-html` options.
*
* @default false
* @example
* plaintext: {
* destination: 'build_production/plaintext',
* extension: 'txt',
* options: { ignoreTags: ['br'] },
* }
*/
plaintext?: boolean | PlaintextConfig;
/** PostCSS processing options. */
postcss?: PostcssConfig;
/**
* Enable the transformer pipeline (CSS inlining, purging, shorthand, etc).
*
* Pass `false` to skip the entire pipeline. Pass an object to opt out of
* specific transformers while keeping the rest active — only keys set
* to `false` are skipped.
*
* @default true
* @example
* useTransformers: { inlineCss: false, minify: false }
*/
useTransformers?: boolean | TransformerToggles;
/**
* Replace strings in the final HTML output.
*
* @example
* replaceStrings: {
* '{{ year }}': new Date().getFullYear().toString(),
* }
*/
replaceStrings?: Record<string, string>;
/**
* Content filters that transform text inside HTML elements using custom attributes.
*
* Set to `false` to disable all filters. Pass an object to add custom filters
* (merged with built-in defaults).
*
* @example
* filters: {
* uppercase: str => str.toUpperCase(),
* }
*/
filters?: FiltersConfig;
/** URL transformation settings (base URL, query string appending). */
url?: UrlConfig;
/** HTML post-processing settings (attributes, formatting, minification). */
html?: HtmlConfig;
/**
* Vite configuration options passed to the internal Vite SSR server.
*
* Use this to add custom Vite plugins or other Vite options. The internal
* SSR server never loads a project `vite.config.{ts,js}` (it runs with
* `configFile: false`), so pass anything it needs here. These options are
* merged underneath Maizzle's required settings, which take precedence.
*
* @example
* vite: {
* plugins: [myPlugin()],
* }
*/
vite?: InlineConfig;
/**
* Vue app customization options.
*
* Register plugins, directives, or global properties on the
* internal Vue app instance used for SSR rendering.
*
* @example
* vue: {
* // Use a factory for stateful plugins (vue-i18n, Pinia, vue-router)
* // so each render gets a fresh instance.
* plugins: () => [createI18n({ locale: 'en', messages })],
* directives: { focus: vFocus },
* globalProperties: { $format: dateFormat },
* }
*/
vue?: VueConfig;
/**
* Props passed to the template's root component when rendering
* programmatically. Map 1:1 to the template's `defineProps`. Not
* merged into `useConfig()`.
*
* Props not declared via `defineProps` fall through as HTML
* attributes on the root element, so declare every prop you pass.
*
* @example
* render('./welcome.vue', { props: { name: 'Ava', plan: 'Pro' } })
*/
props?: Record<string, any>;
/** Called before any templates are processed. */
beforeCreate?: (params: {
config: MaizzleConfig;
}) => void | Promise<void>;
/** Called before each template is rendered. Return a string to replace `template.source`. */
beforeRender?: (params: {
config: MaizzleConfig;
template: TemplateInfo;
}) => string | void | Promise<string | void>;
/** Called after each template is rendered but before transformers run. Return a string to replace the output HTML. */
afterRender?: (params: {
config: MaizzleConfig;
template: TemplateInfo;
html: string;
}) => string | void | Promise<string | void>;
/** Called after transformers have run on each template. Return a string to replace the output HTML. */
afterTransform?: (params: {
config: MaizzleConfig;
template: TemplateInfo;
html: string;
}) => string | void | Promise<string | void>;
/** Called after all templates have been built. */
afterBuild?: (params: {
files: string[];
config: MaizzleConfig;
}) => void | Promise<void>;
[key: string]: any;
}
//#endregion
export { AttributesConfig, CaniemailClient, ChecksConfig, ComponentSource, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, MarkdownConfig, PlaintextConfig, PostcssConfig, TransformerToggles, UrlConfig, UrlQuery, UrlQueryOptions, VueConfig };
//# sourceMappingURL=config.d.ts.map