@stdout-design/core 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ import { t as PropSchema } from "./validation-2z23pVmR.mjs";
2
+ import { z } from "zod";
3
+ import { CSSProperties, ComponentType, ReactElement } from "react";
4
+ import { EmojiType } from "@takumi-rs/helpers/emoji";
5
+ /** Fetch policy for remote images and emoji -- see docs/adr/0018-image-emoji-resolution-module.md. */
6
+ interface ImagePolicy {
7
+ /** Return true to permit fetching a URL. Unset denies every URL -- see assets/image-resolver.ts. */
8
+ allowUrl?: (url: string) => boolean;
9
+ /** Reject a fetched body past this size, by content-length or streamed. @default 32 MiB (the library default) */
10
+ maxBytes?: number;
11
+ /** Abort a fetch after this many milliseconds. @default 5000 (the library default) */
12
+ timeout?: number;
13
+ /** Custom fetch implementation. @default globalThis.fetch */
14
+ fetch?: typeof globalThis.fetch;
15
+ /**
16
+ * Emoji provider. `"from-font"` skips network extraction entirely and
17
+ * relies on a registered COLR font to supply emoji glyphs directly.
18
+ * Unset also skips extraction (emoji render as whatever glyph the
19
+ * fallback font stack provides, typically monochrome or missing).
20
+ */
21
+ emoji?: EmojiType | "from-font";
22
+ }
23
+ interface FontConfig {
24
+ family: string;
25
+ weights?: number[];
26
+ source?: "google" | "local";
27
+ path?: string;
28
+ }
29
+ interface TemplateEntry {
30
+ componentPath: string;
31
+ description?: string;
32
+ }
33
+ interface Preset {
34
+ id: string;
35
+ width: number;
36
+ height: number;
37
+ platform: string;
38
+ deprecated?: boolean;
39
+ }
40
+ interface StudioConfig {
41
+ templates: Record<string, TemplateEntry>;
42
+ presets: Preset[];
43
+ defaultPreset?: string;
44
+ locales?: string[];
45
+ outDir?: string;
46
+ scaffoldVersion?: string;
47
+ fonts?: Record<string, FontConfig[]>;
48
+ /**
49
+ * Fetch policy for external images/emoji any template's node tree may
50
+ * reference. This is the ONLY place `allowUrl` should be configured --
51
+ * never accept it from request props or template code. Unset denies
52
+ * every URL. See ADR-0018 and `assets/image-resolver.ts`.
53
+ */
54
+ images?: ImagePolicy;
55
+ }
56
+ interface TemplateModule {
57
+ default: (props: Record<string, unknown>) => unknown;
58
+ propsSchema: z.ZodObject<Record<string, z.ZodTypeAny>>;
59
+ }
60
+ interface FontFace {
61
+ family: string;
62
+ /** Weights to load from Google Fonts. @default [400] */
63
+ weights?: readonly number[];
64
+ }
65
+ /**
66
+ * One typographic role in a template (display, body, mono): an ordered
67
+ * list of faces. Put the Latin face first and the Arabic or Hebrew face
68
+ * after it; a glyph the first face lacks falls through to the next.
69
+ */
70
+ interface FontRole {
71
+ faces: readonly (string | FontFace)[];
72
+ /** Generic family appended last. @default "sans-serif" */
73
+ generic?: "sans-serif" | "serif" | "monospace";
74
+ }
75
+ type FontRoles<R extends string = string> = Readonly<Record<R, FontRole>>;
76
+ /**
77
+ * The one place a locale id becomes a script and a text direction
78
+ * (ADR-0019). Font fallback, the renderer's `lang`, the render context, and
79
+ * the studio's locale list all read `classifyLocale` instead of testing
80
+ * for `"ar"` themselves.
81
+ */
82
+ declare const SCRIPTS: readonly ["latin", "arabic", "hebrew"];
83
+ type Script = (typeof SCRIPTS)[number];
84
+ type Direction = "ltr" | "rtl";
85
+ interface LocaleInfo {
86
+ /** The locale id as configured (`"ar"`, `"en-GB"`), or `"default"` for a render without one. */
87
+ id: string;
88
+ script: Script;
89
+ dir: Direction;
90
+ /**
91
+ * BCP 47 language the renderer shapes text with. Set only for non-Latin
92
+ * scripts, whose shaping depends on it; Latin text renders the same
93
+ * without one.
94
+ */
95
+ lang: string | undefined;
96
+ }
97
+ declare const classifyLocale: (id: string | undefined) => LocaleInfo;
98
+ interface Canvas {
99
+ width: number;
100
+ height: number;
101
+ aspect: "square" | "landscape" | "portrait";
102
+ }
103
+ /** A text style written for Latin; `ctx.type` adapts it to the render's script. */
104
+ interface TypeSpec<R extends string = string> {
105
+ /** Font size in px; scaled per script. Leave out for text sized elsewhere. */
106
+ size?: number;
107
+ /** Line height, unitless. */
108
+ leading?: number;
109
+ /** Letter spacing in em. */
110
+ tracking?: number;
111
+ italic?: boolean;
112
+ uppercase?: boolean;
113
+ weight?: number;
114
+ /** Font role declared in the template's `fonts`. */
115
+ font?: R;
116
+ /**
117
+ * Exact size or leading for one script, used as given instead of the
118
+ * table's adjustment: for a face whose metrics the table doesn't suit,
119
+ * e.g. `{ arabic: { leading: 1 } }` for a tall display face.
120
+ */
121
+ scripts?: Partial<Record<Script, {
122
+ size?: number;
123
+ leading?: number;
124
+ }>>;
125
+ }
126
+ /**
127
+ * What the pipeline tells a template about the render it is part of
128
+ * (ADR-0019). Built once per render by the orchestrator; never part of
129
+ * props. For layout, prefer logical CSS properties (`insetInlineStart`,
130
+ * `marginInlineEnd`, `borderInlineStart`): the root carries `dir`, so they
131
+ * mirror on their own. `flip` covers what CSS doesn't mirror.
132
+ */
133
+ interface RenderContext<R extends string = string> {
134
+ locale: LocaleInfo;
135
+ dir: Direction;
136
+ canvas: Canvas;
137
+ /** CSS `font-family` value for each declared font role. */
138
+ fonts: Readonly<Record<R, string>>;
139
+ /** Mirrors a horizontal value (rotation angle, SVG x offset) for right-to-left renders. */
140
+ flip: (value: number) => number;
141
+ /**
142
+ * A text style adapted to the script: see `SCRIPT_TYPOGRAPHY`. Declared
143
+ * as a method so a context with named font roles still passes where a
144
+ * plain `RenderContext` is expected.
145
+ */
146
+ type(spec: TypeSpec<R>): CSSProperties;
147
+ }
148
+ declare const TEMPLATE_BRAND: unique symbol;
149
+ /** Prop names the pipeline owns. A schema that declares one fails to type-check. */
150
+ declare const RESERVED_KEYS: readonly ["locale", "ctx"];
151
+ type ReservedKey = (typeof RESERVED_KEYS)[number];
152
+ interface TemplateSpec<S extends z.ZodRawShape, R extends string> {
153
+ /** User content only, built from `field.*`. Render facts arrive as `ctx`. */
154
+ schema: S & Readonly<Partial<Record<ReservedKey, never>>>;
155
+ /** Font roles; `ctx.fonts[role]` is the CSS stack and the pipeline loads every face. */
156
+ fonts?: FontRoles<R>;
157
+ render: (props: z.output<z.ZodObject<S>>, ctx: RenderContext<R>) => ReactElement;
158
+ }
159
+ /**
160
+ * The one shape the pipeline renders. `defineTemplate` builds it;
161
+ * `toTemplateDefinition` builds the same shape from a legacy module.
162
+ */
163
+ interface TemplateDefinition {
164
+ readonly [TEMPLATE_BRAND]: true;
165
+ readonly propsSchema: PropSchema;
166
+ readonly fonts: FontRoles;
167
+ /**
168
+ * Built from a component plus `propsSchema` export. Legacy templates
169
+ * mirror themselves from the `locale` prop, so the pipeline sets no `dir`
170
+ * on their root (it would mirror them twice) and does not re-check props
171
+ * after translation.
172
+ */
173
+ readonly legacy: boolean;
174
+ readonly render: (props: Record<string, unknown>, ctx: RenderContext) => ReactElement;
175
+ }
176
+ declare const defineTemplate: <S extends z.ZodRawShape, R extends string = never>(spec: TemplateSpec<S, R>) => TemplateDefinition;
177
+ declare const isTemplateDefinition: (value: unknown) => value is TemplateDefinition;
178
+ /** Reads a loaded template module, in either shape. Throws `TEMPLATE_INVALID_EXPORT` otherwise. */
179
+ declare const toTemplateDefinition: (mod: unknown, templateId: string) => TemplateDefinition;
180
+ export { FontConfig as _, toTemplateDefinition as a, TemplateModule as b, TypeSpec as c, SCRIPTS as d, Script as f, FontRoles as g, FontRole as h, isTemplateDefinition as i, Direction as l, FontFace as m, TemplateSpec as n, Canvas as o, classifyLocale as p, defineTemplate as r, RenderContext as s, TemplateDefinition as t, LocaleInfo as u, ImagePolicy as v, StudioConfig as y };
@@ -0,0 +1 @@
1
+ import{i as logWarn}from"./logger-DLJyIHAR.mjs";import{r as isZodObject}from"./validation-C7SDI7z-.mjs";import{z}from"zod";import{createElement}from"react";const ErrorCode={API_ERROR:`API_ERROR`,CACHE_CLEANUP_FAILED:`CACHE_CLEANUP_FAILED`,CACHE_CORRUPT_ENTRY:`CACHE_CORRUPT_ENTRY`,CACHE_INIT_FAILED:`CACHE_INIT_FAILED`,CACHE_WRITE_FAILED:`CACHE_WRITE_FAILED`,CONFIG_INVALID:`CONFIG_INVALID`,CONFIG_NOT_FOUND:`CONFIG_NOT_FOUND`,FONT_REGISTRATION_FAILED:`FONT_REGISTRATION_FAILED`,INTERNAL_ERROR:`INTERNAL_ERROR`,LOCALE_LOAD_FAILED:`LOCALE_LOAD_FAILED`,PROP_VALIDATION_FAILED:`PROP_VALIDATION_FAILED`,RENDERER_NOT_INITIALIZED:`RENDERER_NOT_INITIALIZED`,RENDER_FAILED:`RENDER_FAILED`,SSE_ERROR:`SSE_ERROR`,TEMPLATE_INVALID_EXPORT:`TEMPLATE_INVALID_EXPORT`,TEMPLATE_LOAD_FAILED:`TEMPLATE_LOAD_FAILED`,TEMPLATE_NOT_FOUND:`TEMPLATE_NOT_FOUND`,UI_RENDER_ERROR:`UI_RENDER_ERROR`};var AppError=class extends Error{code;context;constructor(code,message,context){super(message),this.name=`AppError`,this.code=code,this.context=context??{}}};const TEMPLATE_BRAND=Symbol.for(`stdout-design.template`),RESERVED_KEYS=[`locale`,`ctx`],defineTemplate=spec=>{let reserved=RESERVED_KEYS.filter(key=>key in spec.schema);if(reserved.length>0)throw new AppError(ErrorCode.TEMPLATE_INVALID_EXPORT,`Template schema declares ${reserved.join(`, `)}, which the pipeline provides as ctx. Remove ${reserved.length>1?`them`:`it`} and read ctx instead.`);return{[TEMPLATE_BRAND]:!0,fonts:spec.fonts??{},legacy:!1,propsSchema:z.object(spec.schema),render:spec.render}},isTemplateDefinition=value=>typeof value==`object`&&!!value&&value[TEMPLATE_BRAND]===!0,warnedLegacyLocale=/* @__PURE__ */ new Set,legacyTemplateDefinition=(component,propsSchema,templateId=`template`)=>(`locale`in propsSchema.shape&&!warnedLegacyLocale.has(templateId)&&(warnedLegacyLocale.add(templateId),logWarn(`Template "${templateId}" declares a "locale" prop. It still works until 1.0, but switch to defineTemplate and read ctx.dir / ctx.locale instead.`)),{[TEMPLATE_BRAND]:!0,fonts:{},legacy:!0,propsSchema,render:(props,ctx)=>createElement(component,ctx.locale.id==="default"?props:{...props,locale:ctx.locale.id})}),toTemplateDefinition=(mod,templateId)=>{let{default:defaultExport,propsSchema}=mod??{};if(isTemplateDefinition(defaultExport))return defaultExport;if(typeof defaultExport!=`function`)throw new AppError(ErrorCode.TEMPLATE_INVALID_EXPORT,`Template "${templateId}" must have a default export: defineTemplate({ schema, render }), or a component function alongside a "propsSchema" export.`,{templateId});if(!isZodObject(propsSchema))throw new AppError(ErrorCode.TEMPLATE_INVALID_EXPORT,`Template "${templateId}" must export a "propsSchema" that is a z.object({...}), or default-export defineTemplate({ schema, render }).`,{templateId});return legacyTemplateDefinition(defaultExport,propsSchema,templateId)};export{AppError as a,toTemplateDefinition as i,isTemplateDefinition as n,ErrorCode as o,legacyTemplateDefinition as r,defineTemplate as t};
package/dist/index.d.mts CHANGED
@@ -1,64 +1,9 @@
1
+ import { _ as FontConfig, a as toTemplateDefinition, b as TemplateModule, d as SCRIPTS, f as Script, i as isTemplateDefinition, l as Direction, p as classifyLocale, r as defineTemplate, t as TemplateDefinition, u as LocaleInfo, v as ImagePolicy, y as StudioConfig } from "./define-template-DIghvJHh.mjs";
1
2
  import { a as validateProps, i as isZodObject, n as PropValidationError, o as zodToJsonSchemaShape, t as PropSchema } from "./validation-2z23pVmR.mjs";
2
3
  import { z } from "zod";
3
4
  import { fromJsx } from "takumi-js/helpers/jsx";
4
- import { prepareImages } from "@takumi-rs/helpers";
5
5
  import { ComponentType } from "react";
6
- import { EmojiType, extractEmojis } from "@takumi-rs/helpers/emoji";
7
6
  import { Node } from "takumi-js";
8
- interface ImagePolicy {
9
- /** Return true to permit fetching a URL. Unset denies every URL -- see the module doc comment. */
10
- allowUrl?: (url: string) => boolean;
11
- /** Reject a fetched body past this size, by content-length or streamed. @default 32 MiB (the library default) */
12
- maxBytes?: number;
13
- /** Abort a fetch after this many milliseconds. @default 5000 (the library default) */
14
- timeout?: number;
15
- /** Custom fetch implementation. @default globalThis.fetch */
16
- fetch?: typeof globalThis.fetch;
17
- /**
18
- * Emoji provider. `"from-font"` skips network extraction entirely and
19
- * relies on a registered COLR font to supply emoji glyphs directly.
20
- * Unset also skips extraction (emoji render as whatever glyph the
21
- * fallback font stack provides, typically monochrome or missing).
22
- */
23
- emoji?: EmojiType | "from-font";
24
- }
25
- interface FontConfig {
26
- family: string;
27
- weights?: number[];
28
- source?: "google" | "local";
29
- path?: string;
30
- }
31
- interface TemplateEntry {
32
- componentPath: string;
33
- description?: string;
34
- }
35
- interface Preset {
36
- id: string;
37
- width: number;
38
- height: number;
39
- platform: string;
40
- deprecated?: boolean;
41
- }
42
- interface StudioConfig {
43
- templates: Record<string, TemplateEntry>;
44
- presets: Preset[];
45
- defaultPreset?: string;
46
- locales?: string[];
47
- outDir?: string;
48
- scaffoldVersion?: string;
49
- fonts?: Record<string, FontConfig[]>;
50
- /**
51
- * Fetch policy for external images/emoji any template's node tree may
52
- * reference. This is the ONLY place `allowUrl` should be configured --
53
- * never accept it from request props or template code. Unset denies
54
- * every URL. See ADR-0018 and `assets/image-resolver.ts`.
55
- */
56
- images?: ImagePolicy;
57
- }
58
- interface TemplateModule {
59
- default: (props: Record<string, unknown>) => unknown;
60
- propsSchema: z.ZodObject<Record<string, z.ZodTypeAny>>;
61
- }
62
7
  export declare const studioConfigSchema: z.ZodObject<{
63
8
  defaultPreset: z.ZodOptional<z.ZodString>;
64
9
  fonts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
@@ -433,19 +378,16 @@ interface AssetsResolverOptions {
433
378
  loadLocaleData?: (locale: string) => Promise<Record<string, unknown>>;
434
379
  preResolvedFonts?: Font[];
435
380
  preResolvedFontFamilies?: string[];
381
+ /** Faces the template declares (`defineTemplate({ fonts })`), loaded on every locale. */
382
+ declaredFonts?: readonly FontConfig[];
383
+ /** The template's prop schema; with `field.*` meta, it decides which props a locale file may replace. */
384
+ propsSchema?: z.ZodType;
436
385
  }
437
386
  export declare const resolveAssetsForLocale: (localeId: string, props: Record<string, unknown>, options?: AssetsResolverOptions) => Promise<AssetsForLocale>;
438
387
  export declare const clearFontCache: () => void;
439
388
  export declare const createFetchFontsFromConfig: (fontsConfig?: Record<string, FontConfig[]> | undefined, configDir?: string) => ((localeId: string) => Promise<FontResolutionResult | null>) | undefined;
440
- /**
441
- * The writing systems the renderer distinguishes. Adding one means adding
442
- * its built-in fonts below; the compiler enforces it.
443
- */
444
- type Script = "arabic" | "latin";
445
- /** The one place a locale id is mapped to its script. */
446
- export declare const scriptOf: (localeId: string) => Script;
447
389
  interface LoadedTemplate {
448
- module: TemplateModule;
390
+ template: TemplateDefinition;
449
391
  contentHash: string;
450
392
  }
451
393
  export declare const importTemplateForBatch: (rootDir: string, componentPath: string, templateId: string) => Promise<LoadedTemplate>;
@@ -495,6 +437,9 @@ interface SingleOutput {
495
437
  export declare const renderComponent: (input: SingleInput) => Promise<SingleOutput>;
496
438
  type OutputFormat = "webp" | "png" | "jpeg" | "ico" | "raw";
497
439
  interface OrchestrateRenderInput {
440
+ /** The template to render, from `toTemplateDefinition` or `defineTemplate`. */
441
+ template?: TemplateDefinition;
442
+ /** Legacy: a component plus `propsSchema`. Prefer `template`. */
498
443
  component?: ComponentType<Record<string, unknown>>;
499
444
  compiledTemplate?: CompiledTemplate;
500
445
  templateContentHash: string;
@@ -528,6 +473,9 @@ interface OrchestrateRenderResult {
528
473
  durationMs: number;
529
474
  }
530
475
  interface OrchestrateMeasureInput {
476
+ /** See `OrchestrateRenderInput.template`. */
477
+ template?: TemplateDefinition;
478
+ /** Legacy: a component plus `propsSchema`. Prefer `template`. */
531
479
  component?: ComponentType<Record<string, unknown>>;
532
480
  compiledTemplate?: CompiledTemplate;
533
481
  templateContentHash: string;
@@ -539,6 +487,11 @@ interface OrchestrateMeasureInput {
539
487
  fetchFonts?: (localeId: string) => Promise<FontResolutionResult | null>;
540
488
  /** See `OrchestrateRenderInput.images`. */
541
489
  images?: ImagePolicy;
490
+ /** Canvas the template's `ctx.canvas` reports while measuring. @default 1200×630 */
491
+ canvasHint?: {
492
+ width: number;
493
+ height: number;
494
+ };
542
495
  cache: RenderCache;
543
496
  signal?: AbortSignal;
544
497
  }
@@ -567,4 +520,4 @@ interface BatchOutput {
567
520
  elapsedMs: number;
568
521
  }
569
522
  export declare const runBatch: (input: BatchInput) => Promise<BatchOutput>;
570
- export { type AssetsForLocale, type AssetsResolverOptions, type BatchInput, type BatchOutput, type CacheStats, type CompiledTemplate, type ErrorCode as ErrorCodeType, type FontConfig, type FontResolutionResult, type LogLevel, type Manifest, type ManifestEntry, type OpenCacheOptions, type OrchestrateMeasureInput, type OrchestrateMeasureResult, type OrchestrateRenderInput, type OrchestrateRenderResult, PropValidationError, type RenderCacheOptions, type RenderOutput, type Script, type SingleInput, type SingleOutput, type StudioConfig, type TemplateModule, isZodObject, validateProps, zodToJsonSchemaShape };
523
+ export { type AssetsForLocale, type AssetsResolverOptions, type BatchInput, type BatchOutput, type CacheStats, type CompiledTemplate, type Direction, type ErrorCode as ErrorCodeType, type FontConfig, type FontResolutionResult, type LocaleInfo, type LogLevel, type Manifest, type ManifestEntry, type OpenCacheOptions, type OrchestrateMeasureInput, type OrchestrateMeasureResult, type OrchestrateRenderInput, type OrchestrateRenderResult, PropValidationError, type RenderCacheOptions, type RenderOutput, SCRIPTS, type Script, type SingleInput, type SingleOutput, type StudioConfig, type TemplateDefinition, type TemplateModule, classifyLocale, defineTemplate, isTemplateDefinition, isZodObject, toTemplateDefinition, validateProps, zodToJsonSchemaShape };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import{a as logDebug,c as logWarn,i as renderToPixels,l as compileTemplate,n as registerFont,o as logError,r as renderAutoSized,s as logInfo,t as measureTemplate}from"./renderer-BtADMpXx.mjs";import{a as zodToJsonSchemaShape,i as validateProps,r as isZodObject,t as PropValidationError}from"./validation-BzP0M2-C.mjs";import{z}from"zod";import{existsSync,unlinkSync}from"node:fs";import{mkdir,readFile,readdir,stat,unlink,writeFile}from"node:fs/promises";import path from"node:path";import{createHash}from"node:crypto";import pLimit from"p-limit";import lockfile from"proper-lockfile";import realWriteFileAtomic from"write-file-atomic";import{Kysely,SqliteDialect,sql}from"kysely";import{googleFonts,prepareImages}from"@takumi-rs/helpers";import{pathToFileURL}from"node:url";import{createElement}from"react";import{extractEmojis}from"@takumi-rs/helpers/emoji";const fontConfigSchema=z.object({family:z.string().min(1),path:z.string().optional(),source:z.enum([`google`,`local`]).optional(),weights:z.array(z.number().int().positive()).optional()}),templateEntrySchema=z.object({componentPath:z.string().min(1,`componentPath is required`),description:z.string().optional()}),presetSchema=z.object({height:z.number().int().positive(),id:z.string().min(1),platform:z.string().min(1),width:z.number().int().positive()}),imagePolicySchema=z.object({allowUrl:z.custom(val=>typeof val==`function`).optional(),emoji:z.custom(val=>typeof val==`string`).optional(),fetch:z.custom(val=>typeof val==`function`).optional(),maxBytes:z.number().int().positive().optional(),timeout:z.number().int().optional()}),studioConfigSchema=z.object({defaultPreset:z.string().optional(),fonts:z.record(z.string(),z.array(fontConfigSchema)).optional(),images:imagePolicySchema.optional(),locales:z.array(z.string()).optional(),outDir:z.string().optional(),presets:z.array(presetSchema),scaffoldVersion:z.string().optional(),templates:z.record(z.string(),templateEntrySchema)}),parseStudioConfig=raw=>{let parsed=studioConfigSchema.safeParse(raw);return parsed.success?{config:parsed.data}:{issues:parsed.error.issues.map(issue=>`${issue.path.join(`.`)||`(root)`}: ${issue.message}`).join(`; `)}},ErrorCode={API_ERROR:`API_ERROR`,CACHE_CLEANUP_FAILED:`CACHE_CLEANUP_FAILED`,CACHE_CORRUPT_ENTRY:`CACHE_CORRUPT_ENTRY`,CACHE_INIT_FAILED:`CACHE_INIT_FAILED`,CACHE_WRITE_FAILED:`CACHE_WRITE_FAILED`,CONFIG_INVALID:`CONFIG_INVALID`,CONFIG_NOT_FOUND:`CONFIG_NOT_FOUND`,FONT_REGISTRATION_FAILED:`FONT_REGISTRATION_FAILED`,INTERNAL_ERROR:`INTERNAL_ERROR`,LOCALE_LOAD_FAILED:`LOCALE_LOAD_FAILED`,PROP_VALIDATION_FAILED:`PROP_VALIDATION_FAILED`,RENDERER_NOT_INITIALIZED:`RENDERER_NOT_INITIALIZED`,RENDER_FAILED:`RENDER_FAILED`,SSE_ERROR:`SSE_ERROR`,TEMPLATE_INVALID_EXPORT:`TEMPLATE_INVALID_EXPORT`,TEMPLATE_LOAD_FAILED:`TEMPLATE_LOAD_FAILED`,TEMPLATE_NOT_FOUND:`TEMPLATE_NOT_FOUND`,UI_RENDER_ERROR:`UI_RENDER_ERROR`};var AppError=class extends Error{code;context;constructor(code,message,context){super(message),this.name=`AppError`,this.code=code,this.context=context??{}}};const createCompileCacheKey=input=>createHash(`sha256`).update(`${input.templateId}\u0000${input.templateContentHash}\u0000${input.propsJSON}`).digest(`hex`).slice(0,32),createPixelCacheKey=input=>createHash(`sha256`).update(`${input.templateContentHash}\u0000${input.propsJSON}\u0000${input.width}\u0000${input.height}\u0000${input.format??`png`}`).digest(`hex`).slice(0,32);var CompileCache=class CompileCache{maxEntries;nodeMap=/* @__PURE__ */ new Map;head;tail;contentHashIndex=/* @__PURE__ */ new Map;keyToContentHash=/* @__PURE__ */ new Map;constructor(maxEntries=100){this.maxEntries=maxEntries,this.head={key:``,next:null,prev:null,value:void 0},this.tail={key:``,next:null,prev:null,value:void 0},this.head.next=this.tail,this.tail.prev=this.head}get size(){return this.nodeMap.size}get(key){let node=this.nodeMap.get(key);return node?(this.moveToTail(node),node.value):null}set(key,value){let existing=this.nodeMap.get(key);if(existing){existing.value=value,this.moveToTail(existing);return}let node={key,next:null,prev:null,value};this.nodeMap.set(key,node),this.insertBeforeTail(node),this.nodeMap.size>this.maxEntries&&this.evictLRU()}setWithContentHash(key,contentHash,value){let previousHash=this.keyToContentHash.get(key);if(previousHash!==void 0&&previousHash!==contentHash){let oldSet=this.contentHashIndex.get(previousHash);oldSet&&(oldSet.delete(key),oldSet.size===0&&this.contentHashIndex.delete(previousHash))}this.set(key,value);let set=this.contentHashIndex.get(contentHash);set||(set=/* @__PURE__ */ new Set,this.contentHashIndex.set(contentHash,set)),set.add(key),this.keyToContentHash.set(key,contentHash)}invalidateByContentHash(contentHash){let keys=this.contentHashIndex.get(contentHash);if(keys){for(let key of keys)this.removeNode(key);this.contentHashIndex.delete(contentHash)}}clear(){this.nodeMap.clear(),this.contentHashIndex.clear(),this.keyToContentHash.clear(),this.head.next=this.tail,this.tail.prev=this.head}insertBeforeTail(node){let prev=this.tail.prev;node.prev=prev,node.next=this.tail,prev.next=node,this.tail.prev=node}static removeFromList(node){node.prev.next=node.next,node.next.prev=node.prev,node.prev=null,node.next=null}moveToTail(node){CompileCache.removeFromList(node),this.insertBeforeTail(node)}evictLRU(){let lru=this.head.next;lru&&lru!==this.tail&&this.removeNode(lru.key)}removeNode(key){let node=this.nodeMap.get(key);if(!node)return;CompileCache.removeFromList(node),this.nodeMap.delete(key);let contentHash=this.keyToContentHash.get(key);if(contentHash!==void 0){this.keyToContentHash.delete(key);let set=this.contentHashIndex.get(contentHash);set&&(set.delete(key),set.size===0&&this.contentHashIndex.delete(contentHash))}}},ConcurrencyLimiter=class{limit;constructor(maxConcurrent){this.limit=maxConcurrent&&maxConcurrent>0?pLimit(maxConcurrent):null}run(fn){return this.limit?this.limit(fn):fn()}get activeCount(){return this.limit?.activeCount??0}get pendingCount(){return this.limit?.pendingCount??0}get isBounded(){return this.limit!==null}},EvictionCoordinator=class{lockTargetPath;maxSizeBytes;debounceMs;getCurrentTotalBytes;sweepFn;debounceTimer=null;constructor(options){this.lockTargetPath=options.lockTargetPath,this.maxSizeBytes=options.maxSizeBytes,this.debounceMs=options.debounceMs??250,this.getCurrentTotalBytes=options.getCurrentTotalBytes,this.sweepFn=options.sweep}requestEviction(){this.debounceTimer||(this.debounceTimer=setTimeout(async()=>{this.debounceTimer=null;try{await this.runIfOverBudget()}catch(error){logWarn(`EvictionCoordinator: debounced sweep failed`,{error:String(error)})}},this.debounceMs),this.debounceTimer.unref?.())}async runIfOverBudget(){return await this.getCurrentTotalBytes()<=this.maxSizeBytes?{freedBytes:0}:this.runSweep({full:!1})}runFullSweep(){return this.runSweep({full:!0})}async runSweep(options){let release=null;try{release=await lockfile.lock(this.lockTargetPath,{retries:0,stale:1e4})}catch(error){return logDebug(`EvictionCoordinator: lock held elsewhere, skipping this pass`,{error:String(error)}),{freedBytes:0}}try{return await this.sweepFn(options)}finally{await release()}}cancelPending(){this.debounceTimer&&=(clearTimeout(this.debounceTimer),null)}};const isFsNotFoundError=error=>error?.code===`ENOENT`,defaultFsPrimitives={existsSync,readFile,stat:filePath=>stat(filePath),unlink,writeFileAtomic:(filePath,data)=>realWriteFileAtomic(filePath,data)};var FsStore=class{limiter;fs;constructor(limiter,fs=defaultFsPrimitives){this.limiter=limiter,this.fs=fs}computeHash(bytes){let bunGlobal=globalThis.Bun;return bunGlobal?`bun1:${bunGlobal.hash(bytes).toString(16)}`:`sha256:${createHash(`sha256`).update(bytes).digest(`hex`)}`}hashesMatch(stored,computed){let[storedPrefix]=stored.split(`:`),[computedPrefix]=computed.split(`:`);return storedPrefix===computedPrefix?stored===computed:(logDebug(`FsStore: cross-runtime hash algorithm mismatch, treating as cache miss`,{computed:computedPrefix,stored:storedPrefix}),!1)}async writeAtomic(filePath,bytes){let doWrite=()=>this.fs.writeFileAtomic(filePath,bytes);await(this.limiter?this.limiter.run(doWrite):doWrite())}readVerified(filePath,expectedSizeBytes,expectedContentHash){let doRead=()=>this.readVerifiedUnbounded(filePath,expectedSizeBytes,expectedContentHash);return this.limiter?this.limiter.run(doRead):doRead()}async readVerifiedUnbounded(filePath,expectedSizeBytes,expectedContentHash){if(!this.fs.existsSync(filePath))return logDebug(`FsStore: cached file missing on read`,{filePath}),null;let fileStat;try{fileStat=await this.fs.stat(filePath)}catch(error){if(isFsNotFoundError(error))return logDebug(`FsStore: file disappeared before stat (TOCTOU)`,{filePath}),null;throw error}if(fileStat.size!==expectedSizeBytes)return logWarn(`FsStore: size mismatch on read, treating as corrupt`,{expected:expectedSizeBytes,filePath,onDisk:fileStat.size}),null;let bytes;try{bytes=await this.fs.readFile(filePath)}catch(error){if(isFsNotFoundError(error))return logDebug(`FsStore: file disappeared before readFile (TOCTOU)`,{filePath}),null;throw error}let actualHash=this.computeHash(bytes);return this.hashesMatch(expectedContentHash,actualHash)?bytes:(expectedContentHash.split(`:`)[0]===actualHash.split(`:`)[0]&&logWarn(`FsStore: content-hash mismatch on read, treating as corrupt`,{expected:expectedContentHash,filePath}),null)}async delete(filePath,knownSizeBytes){if(!this.fs.existsSync(filePath))return 0;let freed=knownSizeBytes??0;try{knownSizeBytes===void 0&&(freed=(await this.fs.stat(filePath)).size),await this.fs.unlink(filePath)}catch(error){return logWarn(`FsStore: failed to delete cache entry file`,{error:String(error),filePath}),freed}return freed}};const BUSY_TIMEOUT_MS=5e3,toRow=r=>({contentHash:r.content_hash,createdAt:r.created_at,fileName:r.file_name,hash:r.hash,height:r.height,lastAccessedAt:r.last_accessed_at,sizeBytes:r.size_bytes,width:r.width});var MetadataStore=class{cacheDir;dbPath;closeDb;db;initialized=!1;initPromise;constructor(cacheDir){this.cacheDir=cacheDir,this.dbPath=cacheDir===`:memory:`?`:memory:`:path.join(cacheDir,`cache.sqlite`)}async init(){if(!this.initialized){if(this.initPromise)return this.initPromise;this.initPromise=this.#doInit();try{await this.initPromise}finally{this.initPromise=void 0,this.closeDb&&(this.initialized=!0)}}}async#doInit(){this.cacheDir!==`:memory:`&&!existsSync(this.cacheDir)&&await mkdir(this.cacheDir,{recursive:!0});let isBun=!1;try{await import(`bun:sqlite`),isBun=!0}catch{isBun=!1}await(isBun?this.initBun():this.initNode())}async initBun(){let Database,BunSqliteDialect;try{({Database}=await import(`bun:sqlite`)),{BunSqliteDialect}=await import(`kysely-bun-sqlite`)}catch(error){throw Error(`Failed to load Bun SQLite modules. Ensure 'bun:sqlite' and 'kysely-bun-sqlite' are available.`,{cause:error})}let rawDb=this.createAndVerifyDb(()=>new Database(this.dbPath,{create:!0,readwrite:!0}),(db,statement)=>db.run(statement),db=>db.query(`PRAGMA integrity_check`).get()?.integrity_check??null);this.closeDb=()=>{try{rawDb.run(`PRAGMA wal_checkpoint(TRUNCATE)`)}catch{}rawDb.close()},this.db=new Kysely({dialect:new BunSqliteDialect({database:rawDb})}),await this.createSchema()}async initNode(){let BetterSqlite3;try{BetterSqlite3=(await import(`better-sqlite3`)).default}catch(error){throw Error("Failed to load better-sqlite3. This is required when running under Node.js. Install it as a dependency: `npm install better-sqlite3`.",{cause:error})}let rawDb=this.createAndVerifyDb(()=>new BetterSqlite3(this.dbPath),(db,statement)=>db.exec(statement),db=>db.prepare(`PRAGMA integrity_check`).get()?.integrity_check??null);this.closeDb=()=>{try{rawDb.exec(`PRAGMA wal_checkpoint(TRUNCATE)`)}catch{}rawDb.close()},this.db=new Kysely({dialect:new SqliteDialect({database:rawDb})}),await this.createSchema()}createAndVerifyDb(createFn,runFn,checkFn){let rawDb=createFn(),dbHealthy=!0;try{runFn(rawDb,`PRAGMA journal_mode = WAL`),runFn(rawDb,`PRAGMA synchronous = NORMAL`),runFn(rawDb,`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`),runFn(rawDb,`PRAGMA wal_checkpoint(TRUNCATE)`);let result=checkFn(rawDb);result!==`ok`&&(dbHealthy=!1,logWarn(`SQLite integrity check failed, recreating cache database`,{result:result??`null`}))}catch{dbHealthy=!1,logWarn(`SQLite setup or integrity check threw, recreating cache database`)}if(dbHealthy)return rawDb;rawDb.close(),this.deleteDatabaseFiles();let freshDb=createFn();return runFn(freshDb,`PRAGMA journal_mode = WAL`),runFn(freshDb,`PRAGMA synchronous = NORMAL`),runFn(freshDb,`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`),freshDb}async createSchema(){let db=this.requireDb();await db.schema.createTable(`pixel_cache_entries`).ifNotExists().addColumn(`hash`,`text`,c=>c.primaryKey()).addColumn(`file_name`,`text`,c=>c.notNull()).addColumn(`created_at`,`integer`,c=>c.notNull()).addColumn(`last_accessed_at`,`integer`,c=>c.notNull()).addColumn(`size_bytes`,`integer`,c=>c.notNull()).addColumn(`width`,`integer`,c=>c.notNull()).addColumn(`height`,`integer`,c=>c.notNull()).addColumn(`content_hash`,`text`,c=>c.notNull()).execute(),await db.schema.createIndex(`idx_last_accessed`).ifNotExists().on(`pixel_cache_entries`).column(`last_accessed_at`).execute(),await db.schema.createTable(`cache_totals`).ifNotExists().addColumn(`id`,`integer`,c=>c.primaryKey()).addColumn(`total_size_bytes`,`integer`,c=>c.notNull().defaultTo(0)).addColumn(`entry_count`,`integer`,c=>c.notNull().defaultTo(0)).execute(),await sql`INSERT OR IGNORE INTO cache_totals (id, total_size_bytes, entry_count) VALUES (1, 0, 0)`.execute(db),await sql`
1
+ import{a as compileTemplate,i as renderToPixels,n as registerFont,r as renderAutoSized,t as measureTemplate}from"./renderer-qptnxMng.mjs";import{a as AppError,i as toTemplateDefinition,n as isTemplateDefinition,o as ErrorCode,r as legacyTemplateDefinition,t as defineTemplate}from"./define-template-nsnBI5Iz.mjs";import{i as logWarn,n as logError,r as logInfo,t as logDebug}from"./logger-DLJyIHAR.mjs";import{a as zodToJsonSchemaShape,i as validateProps,o as getFieldMeta,r as isZodObject,t as PropValidationError}from"./validation-C7SDI7z-.mjs";import{z}from"zod";import{existsSync,unlinkSync}from"node:fs";import{mkdir,readFile,readdir,stat,unlink,writeFile}from"node:fs/promises";import path from"node:path";import{createHash}from"node:crypto";import pLimit from"p-limit";import lockfile from"proper-lockfile";import realWriteFileAtomic from"write-file-atomic";import{Kysely,SqliteDialect,sql}from"kysely";import{googleFonts,prepareImages}from"@takumi-rs/helpers";import{pathToFileURL}from"node:url";import{createElement,isValidElement}from"react";import{extractEmojis}from"@takumi-rs/helpers/emoji";const fontConfigSchema=z.object({family:z.string().min(1),path:z.string().optional(),source:z.enum([`google`,`local`]).optional(),weights:z.array(z.number().int().positive()).optional()}),templateEntrySchema=z.object({componentPath:z.string().min(1,`componentPath is required`),description:z.string().optional()}),presetSchema=z.object({height:z.number().int().positive(),id:z.string().min(1),platform:z.string().min(1),width:z.number().int().positive()}),imagePolicySchema=z.object({allowUrl:z.custom(val=>typeof val==`function`).optional(),emoji:z.custom(val=>typeof val==`string`).optional(),fetch:z.custom(val=>typeof val==`function`).optional(),maxBytes:z.number().int().positive().optional(),timeout:z.number().int().optional()}),studioConfigSchema=z.object({defaultPreset:z.string().optional(),fonts:z.record(z.string(),z.array(fontConfigSchema)).optional(),images:imagePolicySchema.optional(),locales:z.array(z.string()).optional(),outDir:z.string().optional(),presets:z.array(presetSchema),scaffoldVersion:z.string().optional(),templates:z.record(z.string(),templateEntrySchema)}),parseStudioConfig=raw=>{let parsed=studioConfigSchema.safeParse(raw);return parsed.success?{config:parsed.data}:{issues:parsed.error.issues.map(issue=>`${issue.path.join(`.`)||`(root)`}: ${issue.message}`).join(`; `)}},createCompileCacheKey=input=>createHash(`sha256`).update(`${input.templateId}\u0000${input.templateContentHash}\u0000${input.propsJSON}`).digest(`hex`).slice(0,32),createPixelCacheKey=input=>createHash(`sha256`).update(`${input.templateContentHash}\u0000${input.propsJSON}\u0000${input.width}\u0000${input.height}\u0000${input.format??`png`}`).digest(`hex`).slice(0,32);var CompileCache=class CompileCache{maxEntries;nodeMap=/* @__PURE__ */ new Map;head;tail;contentHashIndex=/* @__PURE__ */ new Map;keyToContentHash=/* @__PURE__ */ new Map;constructor(maxEntries=100){this.maxEntries=maxEntries,this.head={key:``,next:null,prev:null,value:void 0},this.tail={key:``,next:null,prev:null,value:void 0},this.head.next=this.tail,this.tail.prev=this.head}get size(){return this.nodeMap.size}get(key){let node=this.nodeMap.get(key);return node?(this.moveToTail(node),node.value):null}set(key,value){let existing=this.nodeMap.get(key);if(existing){existing.value=value,this.moveToTail(existing);return}let node={key,next:null,prev:null,value};this.nodeMap.set(key,node),this.insertBeforeTail(node),this.nodeMap.size>this.maxEntries&&this.evictLRU()}setWithContentHash(key,contentHash,value){let previousHash=this.keyToContentHash.get(key);if(previousHash!==void 0&&previousHash!==contentHash){let oldSet=this.contentHashIndex.get(previousHash);oldSet&&(oldSet.delete(key),oldSet.size===0&&this.contentHashIndex.delete(previousHash))}this.set(key,value);let set=this.contentHashIndex.get(contentHash);set||(set=/* @__PURE__ */ new Set,this.contentHashIndex.set(contentHash,set)),set.add(key),this.keyToContentHash.set(key,contentHash)}invalidateByContentHash(contentHash){let keys=this.contentHashIndex.get(contentHash);if(keys){for(let key of keys)this.removeNode(key);this.contentHashIndex.delete(contentHash)}}clear(){this.nodeMap.clear(),this.contentHashIndex.clear(),this.keyToContentHash.clear(),this.head.next=this.tail,this.tail.prev=this.head}insertBeforeTail(node){let prev=this.tail.prev;node.prev=prev,node.next=this.tail,prev.next=node,this.tail.prev=node}static removeFromList(node){node.prev.next=node.next,node.next.prev=node.prev,node.prev=null,node.next=null}moveToTail(node){CompileCache.removeFromList(node),this.insertBeforeTail(node)}evictLRU(){let lru=this.head.next;lru&&lru!==this.tail&&this.removeNode(lru.key)}removeNode(key){let node=this.nodeMap.get(key);if(!node)return;CompileCache.removeFromList(node),this.nodeMap.delete(key);let contentHash=this.keyToContentHash.get(key);if(contentHash!==void 0){this.keyToContentHash.delete(key);let set=this.contentHashIndex.get(contentHash);set&&(set.delete(key),set.size===0&&this.contentHashIndex.delete(contentHash))}}},ConcurrencyLimiter=class{limit;constructor(maxConcurrent){this.limit=maxConcurrent&&maxConcurrent>0?pLimit(maxConcurrent):null}run(fn){return this.limit?this.limit(fn):fn()}get activeCount(){return this.limit?.activeCount??0}get pendingCount(){return this.limit?.pendingCount??0}get isBounded(){return this.limit!==null}},EvictionCoordinator=class{lockTargetPath;maxSizeBytes;debounceMs;getCurrentTotalBytes;sweepFn;debounceTimer=null;constructor(options){this.lockTargetPath=options.lockTargetPath,this.maxSizeBytes=options.maxSizeBytes,this.debounceMs=options.debounceMs??250,this.getCurrentTotalBytes=options.getCurrentTotalBytes,this.sweepFn=options.sweep}requestEviction(){this.debounceTimer||(this.debounceTimer=setTimeout(async()=>{this.debounceTimer=null;try{await this.runIfOverBudget()}catch(error){logWarn(`EvictionCoordinator: debounced sweep failed`,{error:String(error)})}},this.debounceMs),this.debounceTimer.unref?.())}async runIfOverBudget(){return await this.getCurrentTotalBytes()<=this.maxSizeBytes?{freedBytes:0}:this.runSweep({full:!1})}runFullSweep(){return this.runSweep({full:!0})}async runSweep(options){let release=null;try{release=await lockfile.lock(this.lockTargetPath,{retries:0,stale:1e4})}catch(error){return logDebug(`EvictionCoordinator: lock held elsewhere, skipping this pass`,{error:String(error)}),{freedBytes:0}}try{return await this.sweepFn(options)}finally{await release()}}cancelPending(){this.debounceTimer&&=(clearTimeout(this.debounceTimer),null)}};const isFsNotFoundError=error=>error?.code===`ENOENT`,defaultFsPrimitives={existsSync,readFile,stat:filePath=>stat(filePath),unlink,writeFileAtomic:(filePath,data)=>realWriteFileAtomic(filePath,data)};var FsStore=class{limiter;fs;constructor(limiter,fs=defaultFsPrimitives){this.limiter=limiter,this.fs=fs}computeHash(bytes){let bunGlobal=globalThis.Bun;return bunGlobal?`bun1:${bunGlobal.hash(bytes).toString(16)}`:`sha256:${createHash(`sha256`).update(bytes).digest(`hex`)}`}hashesMatch(stored,computed){let[storedPrefix]=stored.split(`:`),[computedPrefix]=computed.split(`:`);return storedPrefix===computedPrefix?stored===computed:(logDebug(`FsStore: cross-runtime hash algorithm mismatch, treating as cache miss`,{computed:computedPrefix,stored:storedPrefix}),!1)}async writeAtomic(filePath,bytes){let doWrite=()=>this.fs.writeFileAtomic(filePath,bytes);await(this.limiter?this.limiter.run(doWrite):doWrite())}readVerified(filePath,expectedSizeBytes,expectedContentHash){let doRead=()=>this.readVerifiedUnbounded(filePath,expectedSizeBytes,expectedContentHash);return this.limiter?this.limiter.run(doRead):doRead()}async readVerifiedUnbounded(filePath,expectedSizeBytes,expectedContentHash){if(!this.fs.existsSync(filePath))return logDebug(`FsStore: cached file missing on read`,{filePath}),null;let fileStat;try{fileStat=await this.fs.stat(filePath)}catch(error){if(isFsNotFoundError(error))return logDebug(`FsStore: file disappeared before stat (TOCTOU)`,{filePath}),null;throw error}if(fileStat.size!==expectedSizeBytes)return logWarn(`FsStore: size mismatch on read, treating as corrupt`,{expected:expectedSizeBytes,filePath,onDisk:fileStat.size}),null;let bytes;try{bytes=await this.fs.readFile(filePath)}catch(error){if(isFsNotFoundError(error))return logDebug(`FsStore: file disappeared before readFile (TOCTOU)`,{filePath}),null;throw error}let actualHash=this.computeHash(bytes);return this.hashesMatch(expectedContentHash,actualHash)?bytes:(expectedContentHash.split(`:`)[0]===actualHash.split(`:`)[0]&&logWarn(`FsStore: content-hash mismatch on read, treating as corrupt`,{expected:expectedContentHash,filePath}),null)}async delete(filePath,knownSizeBytes){if(!this.fs.existsSync(filePath))return 0;let freed=knownSizeBytes??0;try{knownSizeBytes===void 0&&(freed=(await this.fs.stat(filePath)).size),await this.fs.unlink(filePath)}catch(error){return logWarn(`FsStore: failed to delete cache entry file`,{error:String(error),filePath}),freed}return freed}};const BUSY_TIMEOUT_MS=5e3,toRow=r=>({contentHash:r.content_hash,createdAt:r.created_at,fileName:r.file_name,hash:r.hash,height:r.height,lastAccessedAt:r.last_accessed_at,sizeBytes:r.size_bytes,width:r.width});var MetadataStore=class{cacheDir;dbPath;closeDb;db;initialized=!1;initPromise;constructor(cacheDir){this.cacheDir=cacheDir,this.dbPath=cacheDir===`:memory:`?`:memory:`:path.join(cacheDir,`cache.sqlite`)}async init(){if(!this.initialized){if(this.initPromise)return this.initPromise;this.initPromise=this.#doInit();try{await this.initPromise}finally{this.initPromise=void 0,this.closeDb&&(this.initialized=!0)}}}async#doInit(){this.cacheDir!==`:memory:`&&!existsSync(this.cacheDir)&&await mkdir(this.cacheDir,{recursive:!0});let isBun=!1;try{await import(`bun:sqlite`),isBun=!0}catch{isBun=!1}await(isBun?this.initBun():this.initNode())}async initBun(){let Database,BunSqliteDialect;try{({Database}=await import(`bun:sqlite`)),{BunSqliteDialect}=await import(`kysely-bun-sqlite`)}catch(error){throw Error(`Failed to load Bun SQLite modules. Ensure 'bun:sqlite' and 'kysely-bun-sqlite' are available.`,{cause:error})}let rawDb=this.createAndVerifyDb(()=>new Database(this.dbPath,{create:!0,readwrite:!0}),(db,statement)=>db.run(statement),db=>db.query(`PRAGMA integrity_check`).get()?.integrity_check??null);this.closeDb=()=>{try{rawDb.run(`PRAGMA wal_checkpoint(TRUNCATE)`)}catch{}rawDb.close()},this.db=new Kysely({dialect:new BunSqliteDialect({database:rawDb})}),await this.createSchema()}async initNode(){let BetterSqlite3;try{BetterSqlite3=(await import(`better-sqlite3`)).default}catch(error){throw Error("Failed to load better-sqlite3. This is required when running under Node.js. Install it as a dependency: `npm install better-sqlite3`.",{cause:error})}let rawDb=this.createAndVerifyDb(()=>new BetterSqlite3(this.dbPath),(db,statement)=>db.exec(statement),db=>db.prepare(`PRAGMA integrity_check`).get()?.integrity_check??null);this.closeDb=()=>{try{rawDb.exec(`PRAGMA wal_checkpoint(TRUNCATE)`)}catch{}rawDb.close()},this.db=new Kysely({dialect:new SqliteDialect({database:rawDb})}),await this.createSchema()}createAndVerifyDb(createFn,runFn,checkFn){let rawDb=createFn(),dbHealthy=!0;try{runFn(rawDb,`PRAGMA journal_mode = WAL`),runFn(rawDb,`PRAGMA synchronous = NORMAL`),runFn(rawDb,`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`),runFn(rawDb,`PRAGMA wal_checkpoint(TRUNCATE)`);let result=checkFn(rawDb);result!==`ok`&&(dbHealthy=!1,logWarn(`SQLite integrity check failed, recreating cache database`,{result:result??`null`}))}catch{dbHealthy=!1,logWarn(`SQLite setup or integrity check threw, recreating cache database`)}if(dbHealthy)return rawDb;rawDb.close(),this.deleteDatabaseFiles();let freshDb=createFn();return runFn(freshDb,`PRAGMA journal_mode = WAL`),runFn(freshDb,`PRAGMA synchronous = NORMAL`),runFn(freshDb,`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`),freshDb}async createSchema(){let db=this.requireDb();await db.schema.createTable(`pixel_cache_entries`).ifNotExists().addColumn(`hash`,`text`,c=>c.primaryKey()).addColumn(`file_name`,`text`,c=>c.notNull()).addColumn(`created_at`,`integer`,c=>c.notNull()).addColumn(`last_accessed_at`,`integer`,c=>c.notNull()).addColumn(`size_bytes`,`integer`,c=>c.notNull()).addColumn(`width`,`integer`,c=>c.notNull()).addColumn(`height`,`integer`,c=>c.notNull()).addColumn(`content_hash`,`text`,c=>c.notNull()).execute(),await db.schema.createIndex(`idx_last_accessed`).ifNotExists().on(`pixel_cache_entries`).column(`last_accessed_at`).execute(),await db.schema.createTable(`cache_totals`).ifNotExists().addColumn(`id`,`integer`,c=>c.primaryKey()).addColumn(`total_size_bytes`,`integer`,c=>c.notNull().defaultTo(0)).addColumn(`entry_count`,`integer`,c=>c.notNull().defaultTo(0)).execute(),await sql`INSERT OR IGNORE INTO cache_totals (id, total_size_bytes, entry_count) VALUES (1, 0, 0)`.execute(db),await sql`
2
2
  CREATE TRIGGER IF NOT EXISTS trg_pixel_cache_insert
3
3
  AFTER INSERT ON pixel_cache_entries
4
4
  BEGIN
@@ -25,4 +25,4 @@ import{a as logDebug,c as logWarn,i as renderToPixels,l as compileTemplate,n as
25
25
  SET total_size_bytes = total_size_bytes - OLD.size_bytes + NEW.size_bytes
26
26
  WHERE id = 1;
27
27
  END;
28
- `.execute(db)}close(){this.closeDb&&=(this.closeDb(),void 0),this.db=void 0,this.initialized=!1,this.initPromise=void 0}deleteDatabaseFiles(){if(this.dbPath!==`:memory:`)for(let suffix of[``,`-wal`,`-shm`])try{unlinkSync(`${this.dbPath}${suffix}`)}catch{}}async upsertEntry(input){let db=this.requireDb(),now=Date.now();await db.insertInto(`pixel_cache_entries`).values({content_hash:input.contentHash,created_at:now,file_name:input.fileName,hash:input.hash,height:input.height,last_accessed_at:now,size_bytes:input.sizeBytes,width:input.width}).onConflict(oc=>oc.column(`hash`).doUpdateSet({content_hash:input.contentHash,file_name:input.fileName,height:input.height,last_accessed_at:now,size_bytes:input.sizeBytes,width:input.width})).execute()}async getEntry(hash){let row=await this.requireDb().selectFrom(`pixel_cache_entries`).selectAll().where(`hash`,`=`,hash).executeTakeFirst();return row?toRow(row):null}async touchAccess(hash,timestamp=Date.now()){await this.requireDb().updateTable(`pixel_cache_entries`).set({last_accessed_at:timestamp}).where(`hash`,`=`,hash).execute()}async deleteEntry(hash){await this.requireDb().deleteFrom(`pixel_cache_entries`).where(`hash`,`=`,hash).execute()}async deleteAll(){await this.requireDb().deleteFrom(`pixel_cache_entries`).execute()}async listAllOrderedByLastAccessed(){return(await this.requireDb().selectFrom(`pixel_cache_entries`).selectAll().orderBy(`last_accessed_at`,`asc`).execute()).map(toRow)}async getTotals(){let row=await this.requireDb().selectFrom(`cache_totals`).select([`total_size_bytes`,`entry_count`]).where(`id`,`=`,1).executeTakeFirstOrThrow();return{entryCount:row.entry_count,totalSizeBytes:row.total_size_bytes}}requireDb(){if(!this.db)throw Error(`MetadataStore.init() must be called before use.`);return this.db}};const RECOVERY_COOLDOWN_MS=2e3,isCacheEntryFileName=name=>/^[0-9a-f]{32,}\.[a-z]+$/u.test(name);var RenderCache=class{cacheDir;maxSizeBytes;compiled;maxConcurrentWrites;evictionDebounceMs;metadata;fsStore;eviction;initialized=!1;recoveryPromise=null;lastRecoveryAt=0;static createCompileCacheKey=createCompileCacheKey;static createPixelCacheKey=createPixelCacheKey;constructor(options){this.cacheDir=options?.cacheDir??`.studio-cache`,this.maxSizeBytes=(options?.maxSizeMB??500)*1024*1024,this.compiled=new CompileCache(options?.compiledMaxEntries),this.maxConcurrentWrites=options?.maxConcurrentWrites,this.evictionDebounceMs=options?.evictionDebounceMs}async init(){if(this.initialized)return;existsSync(this.cacheDir)||await mkdir(this.cacheDir,{recursive:!0}),this.metadata=new MetadataStore(this.cacheDir),await this.metadata.init();let limiter=new ConcurrencyLimiter(this.maxConcurrentWrites);this.fsStore=new FsStore(limiter),this.eviction=new EvictionCoordinator({debounceMs:this.evictionDebounceMs,getCurrentTotalBytes:async()=>(await this.requireMetadata().getTotals()).totalSizeBytes,lockTargetPath:this.cacheDir,maxSizeBytes:this.maxSizeBytes,sweep:options=>this.performSweep(options)}),this.initialized=!0}close(){this.eviction?.cancelPending(),this.metadata?.close(),this.initialized=!1}getCompiled(key){return this.compiled.get(key)}setCompiled(key,value){this.compiled.set(key,value)}setCompiledWithContentHash(key,contentHash,value){this.compiled.setWithContentHash(key,contentHash,value)}invalidateCompiledByContentHash(contentHash){this.compiled.invalidateByContentHash(contentHash)}clearCompiled(){this.compiled.clear()}async getPixels(key){let metadata=this.requireMetadata(),fsStore=this.requireFsStore(),row;try{row=await metadata.getEntry(key)}catch(error){return logWarn(`Pixel metadata read failed, attempting recovery`,{error:error instanceof Error?error.message:String(error),key}),await this.recoverMetadata(),null}if(!row)return null;let filePath=path.join(this.cacheDir,row.fileName),bytes=await fsStore.readVerified(filePath,row.sizeBytes,row.contentHash);if(bytes===null){try{await metadata.deleteEntry(key)}catch{}return null}try{await metadata.touchAccess(key)}catch(error){logWarn(`Pixel touchAccess failed, attempting recovery`,{error:error instanceof Error?error.message:String(error),key}),await this.recoverMetadata()}return bytes}async setPixels(key,bytes,width,height,format=`png`){let metadata=this.requireMetadata(),fsStore=this.requireFsStore(),fileName=`${key}.${format}`,filePath=path.join(this.cacheDir,fileName),contentHash=fsStore.computeHash(bytes);try{await fsStore.writeAtomic(filePath,bytes)}catch(error){if(error?.code===`ENOENT`)logDebug(`FsStore: ENOENT during writeAtomic, recreating cache directory and retrying`,{filePath}),await mkdir(this.cacheDir,{recursive:!0}),await fsStore.writeAtomic(filePath,bytes);else throw error}try{await metadata.upsertEntry({contentHash,fileName,hash:key,height,sizeBytes:bytes.length,width})}catch(error){let err=error;if(err?.code===`SQLITE_IOERR_VNODE`||(err?.message??``).includes(`disk I/O error`))logWarn(`Metadata write failed (I/O error), recovering and retrying`,{error:String(error),key}),await this.recoverMetadata(),metadata=this.requireMetadata(),await metadata.upsertEntry({contentHash,fileName,hash:key,height,sizeBytes:bytes.length,width});else throw error}return this.requireEviction().requestEviction(),filePath}async stats(){let totals=await this.requireMetadata().getTotals();return{cacheDir:this.cacheDir,compiled:{entries:this.compiled.size},pixels:{entries:totals.entryCount,maxSizeBytes:this.maxSizeBytes,sizeBytes:totals.totalSizeBytes}}}clean(){return this.compiled.clear(),this.requireEviction().runFullSweep()}async performSweep(options){let metadata=this.requireMetadata(),fsStore=this.requireFsStore();if(options.full){let rows=await metadata.listAllOrderedByLastAccessed(),freedAmounts=await Promise.all(rows.map(row=>fsStore.delete(path.join(this.cacheDir,row.fileName),row.sizeBytes)));await metadata.deleteAll();let orphanFreed=await this.deleteOrphanFiles(new Set(rows.map(r=>r.fileName)),fsStore);return{freedBytes:freedAmounts.reduce((sum,n)=>sum+n,0)+orphanFreed}}let totals=await metadata.getTotals();if(totals.totalSizeBytes<=this.maxSizeBytes)return{freedBytes:0};let rows=await metadata.listAllOrderedByLastAccessed(),remaining=totals.totalSizeBytes,rowsToEvict=[];for(let row of rows){if(remaining<=this.maxSizeBytes)break;rowsToEvict.push(row),remaining-=row.sizeBytes}return{freedBytes:(await Promise.all(rowsToEvict.map(async row=>{let freedForRow=await fsStore.delete(path.join(this.cacheDir,row.fileName),row.sizeBytes);return await metadata.deleteEntry(row.hash),freedForRow}))).reduce((sum,n)=>sum+n,0)}}async deleteOrphanFiles(knownFileNames,fsStore){let freed=0;try{let entries=await readdir(this.cacheDir);await Promise.all(entries.map(async entry=>{if(knownFileNames.has(entry)||!isCacheEntryFileName(entry))return;let filePath=path.join(this.cacheDir,entry);logDebug(`FsStore: removing orphaned cache file`,{filePath}),freed+=await fsStore.delete(filePath)}))}catch(error){logWarn(`RenderCache: failed to scan for orphaned files`,{error:String(error)})}return freed}async recoverMetadata(){if(Date.now()-this.lastRecoveryAt<RECOVERY_COOLDOWN_MS)logDebug(`RenderCache: skipping recovery — within cooldown window`,{cooldownMs:RECOVERY_COOLDOWN_MS});else{if(this.recoveryPromise)return this.recoveryPromise;this.recoveryPromise=this.doRecoverMetadata();try{await this.recoveryPromise}finally{this.recoveryPromise=null}}}async doRecoverMetadata(){this.lastRecoveryAt=Date.now(),this.metadata?.close(),this.metadata=new MetadataStore(this.cacheDir),await this.metadata.init()}requireMetadata(){if(!this.metadata)throw Error(`RenderCache.init() must be called before use.`);return this.metadata}requireFsStore(){if(!this.fsStore)throw Error(`RenderCache.init() must be called before use.`);return this.fsStore}requireEviction(){if(!this.eviction)throw Error(`RenderCache.init() must be called before use.`);return this.eviction}};const resolveProjectPaths=rootDir=>({configPath:path.resolve(rootDir,`studio.config.ts`),defaultCacheDir:path.resolve(rootDir,`.studio-cache`),localesDir:path.resolve(rootDir,`locales`),templatesDir:path.resolve(rootDir,`templates`)}),openCache=async(rootDir,options)=>{let cache=new RenderCache({cacheDir:options?.cacheDir??resolveProjectPaths(rootDir).defaultCacheDir,maxSizeMB:options?.maxSizeMB});return await cache.init(),cache},scriptOf=localeId=>localeId.startsWith(`ar`)?`arabic`:`latin`,BUILT_IN_FONTS={arabic:[{family:`Noto Sans Arabic`,source:`google`,weights:[400,700]}],latin:[]},fontEntriesFor=(fonts,localeId)=>{let script=scriptOf(localeId),configured=fonts?.[localeId]??(script===`latin`?fonts?.default:void 0);return Array.isArray(configured)&&configured.length>0?configured:BUILT_IN_FONTS[script]},fontSetKey=(entries,configDir=``)=>JSON.stringify([configDir,entries.map(e=>[e.source??``,e.family,e.weights??[],e.path??``])]),isFontObject=f=>typeof f==`object`&&!(f instanceof Uint8Array),loadLocalFontDescriptor=async(family,filePath,weight,configDir)=>{let resolved=configDir?path.resolve(configDir,filePath):path.resolve(filePath);return{data:await readFile(resolved),name:family,weight:weight??400}},isLocalConfig=c=>c.source===`local`&&typeof c.path==`string`&&c.path.length>0,loadFontSet=async(configs,configDir)=>{try{let localConfigs=configs.filter(isLocalConfig),googleConfigs=configs.filter(c=>!isLocalConfig(c)),localFonts=await Promise.all(localConfigs.map(c=>loadLocalFontDescriptor(c.family,c.path,c.weights?.[0],configDir))),googleFontDescriptors=[];if(googleConfigs.length>0){let families=googleConfigs.map(c=>({name:c.family,weight:c.weights??[400,700]}));googleFontDescriptors=await googleFonts(families)}let fonts=[...localFonts,...googleFontDescriptors],fontFamilies=fonts.filter(isFontObject).map(f=>String(f.name??``)).filter(Boolean);return logDebug(`fonts resolved`,{count:fonts.length,names:fontFamilies}),{fontFamilies,fonts}}catch(error){return logDebug(`fonts failed to load`,{error:String(error),families:configs.map(c=>c.family)}),null}},fontCache=/* @__PURE__ */ new Map,clearFontCache=()=>{fontCache.clear()},loadFontSetCached=async(configs,configDir)=>{let key=fontSetKey(configs,configDir),cached=fontCache.get(key);if(cached)return await cached;let promise=loadFontSet(configs,configDir);fontCache.set(key,promise);let resolution=await promise;return resolution||fontCache.delete(key),resolution},fetchFontsFor=async(fontsConfig,localeId,configDir)=>{if(typeof localeId!=`string`)return null;let configs=fontEntriesFor(fontsConfig,localeId);return configs.length>0?await loadFontSetCached(configs,configDir):null},fetchBuiltInFonts=localeId=>fetchFontsFor(void 0,localeId),createFetchFontsFromConfig=(fontsConfig,configDir)=>{if(fontsConfig)return localeId=>fetchFontsFor(fontsConfig,localeId,configDir)},mergeLocaleProps=(props,localeData)=>{if(!localeData)return{...props};let merged={...props};for(let[key,value]of Object.entries(localeData))(Array.isArray(value)&&Array.isArray(merged[key])||typeof value==`string`&&typeof merged[key]==`string`)&&(merged[key]=value);return merged},resolveAssetsForLocale=async(localeId,props,options)=>{let localeData=options?.localeData;if(options?.loadLocaleData)try{localeData=await options.loadLocaleData(localeId)}catch{localeData={}}let mergedProps=mergeLocaleProps(props,localeData),lang=scriptOf(localeId)===`arabic`?`ar`:void 0,fonts=[],fontFamilies;if(options?.preResolvedFonts)fonts=options.preResolvedFonts,fontFamilies=options.preResolvedFontFamilies;else{let resolution=(options?.fetchFonts?await options.fetchFonts(localeId):null)??await fetchBuiltInFonts(localeId);resolution&&({fontFamilies,fonts}=resolution)}return{fontFamilies,fonts,lang,props:mergedProps}},templateCache=/* @__PURE__ */ new Map,loadTemplateForBatch=async(fullPath,templateId)=>{let content;try{content=await readFile(fullPath,`utf-8`)}catch{throw new AppError(ErrorCode.TEMPLATE_LOAD_FAILED,`Template file not found: ${fullPath}`,{componentPath:fullPath,templateId})}let contentHash=createHash(`sha256`).update(content).digest(`hex`).slice(0,16);logDebug(`Loading template`,{path:fullPath,templateId});let mod=await import(pathToFileURL(fullPath).href);if(typeof mod.default!=`function`)throw new AppError(ErrorCode.TEMPLATE_INVALID_EXPORT,`Template "${templateId}" must have a default export that is a component function.`,{templateId});let{propsSchema}=mod;if(!isZodObject(propsSchema))throw new AppError(ErrorCode.TEMPLATE_INVALID_EXPORT,`Template "${templateId}" must export a "propsSchema" that is a z.object({...}).`,{templateId});return{contentHash,module:mod}},importTemplateForBatch=async(rootDir,componentPath,templateId)=>{let fullPath=path.resolve(rootDir,`${componentPath}.tsx`),cached=templateCache.get(fullPath);if(cached)return cached;let promise=loadTemplateForBatch(fullPath,templateId);templateCache.set(fullPath,promise);try{return await promise}catch{throw templateCache.delete(fullPath),await promise.catch(error=>error)}},loadConfig=async rootDir=>{let{configPath}=resolveProjectPaths(rootDir);try{await readFile(configPath,`utf-8`)}catch{throw new AppError(ErrorCode.CONFIG_NOT_FOUND,`No studio.config.ts found in ${rootDir}.`)}logDebug(`Loading studio.config.ts`,{path:configPath});let mod=await import(pathToFileURL(configPath).href),raw=mod.default??mod,result=parseStudioConfig(raw);if(`issues`in result)throw new AppError(ErrorCode.CONFIG_INVALID,`studio.config.ts is invalid: ${result.issues}`);return result.config},parseDataFile=async filePath=>{let content=await readFile(filePath,`utf-8`);if(filePath.endsWith(`.json`)){let data=JSON.parse(content);if(Array.isArray(data))return data;throw new AppError(ErrorCode.API_ERROR,`JSON data file must contain an array of objects.`)}if(filePath.endsWith(`.csv`)){let{parse}=await import(`csv-parse/sync`);return parse(content,{columns:!0,skip_empty_lines:!0,trim:!0})}throw new AppError(ErrorCode.API_ERROR,`Data file must be .csv or .json.`)},resolveLocales=async(rootDir,localeCodes,templateId)=>{if(localeCodes.length===0)return[{id:`default`}];let{localesDir}=resolveProjectPaths(rootDir);return await Promise.all(localeCodes.map(async code=>{let localePath=path.resolve(localesDir,`${code}.json`),content;try{content=await readFile(localePath,`utf-8`)}catch{return logWarn(`No locales/${code}.json found — rendering "${code}" with untranslated props.`),{id:code}}let parsed;try{parsed=JSON.parse(content)}catch(error){throw new AppError(ErrorCode.CONFIG_INVALID,`locales/${code}.json is not valid JSON: ${error instanceof Error?error.message:String(error)}`)}let data=parsed[templateId];return data||logWarn(`locales/${code}.json has no "${templateId}" entry — expected { "${templateId}": { ...props } }.`),{data,id:code}}))},generateOutputFilename=options=>{let{templateId,locale,presetId,rowKey,format=`png`}=options;return`${templateId}.${locale}.${presetId}.${rowKey}.${format}`};var BoundedImageFetchCache=class{maxEntries;store=/* @__PURE__ */ new Map;constructor(maxEntries=500){this.maxEntries=maxEntries}get size(){return this.store.size}get(url){let value=this.store.get(url);if(value!==void 0)return this.store.delete(url),this.store.set(url,value),value}set(url,data){if(this.store.delete(url),this.store.set(url,data),this.store.size>this.maxEntries){let oldest=this.store.keys().next().value;oldest!==void 0&&this.store.delete(oldest)}}delete(url){this.store.delete(url)}clear(){this.store.clear()}},ImagePolicyBlockedError=class extends Error{url;constructor(url){super(`Blocked by image fetch policy: ${url}`),this.name=`ImagePolicyBlockedError`,this.url=url}};const denyAll=()=>!1;let sharedFetchCache=null;const getSharedFetchCache=()=>(sharedFetchCache||=new BoundedImageFetchCache,sharedFetchCache),isAllowUrlBlockedError=error=>error instanceof Error&&error.message.startsWith(`URL blocked by allowUrl policy: `),resolveImages=async(node,policy,deps)=>{let prepareImagesFn=deps?.prepareImages??prepareImages,extractEmojisFn=deps?.extractEmojis??extractEmojis,fetchCache=deps?.fetchCache??getSharedFetchCache(),resolvedNode=policy?.emoji&&policy.emoji!==`from-font`?extractEmojisFn(node,policy.emoji):node;try{let images=await prepareImagesFn({allowUrl:policy?.allowUrl??denyAll,fetch:policy?.fetch,fetchCache,maxBytes:policy?.maxBytes,node:resolvedNode,timeout:policy?.timeout});return logDebug(`image-resolver: resolved images`,{count:images.length}),{images,node:resolvedNode}}catch(error){throw isAllowUrlBlockedError(error)?new ImagePolicyBlockedError(error.message.slice(32)):(logWarn(`image-resolver: image fetch failed`,{error:error instanceof Error?error.message:String(error)}),error)}},prepPipeline=async input=>{let{compiledTemplate:preCompiled,component,templateContentHash,templateId,props,propsSchema,locale,loadLocaleData,preResolvedFonts,preResolvedFontFamilies,fetchFonts,images,cache}=input,validated=propsSchema?validateProps(propsSchema,props):{...props},{fonts:resolvedFonts,fontFamilies:resolvedFontFamilies,lang:resolvedLang,props:merged}=await resolveAssetsForLocale(locale,validated,{fetchFonts,loadLocaleData,preResolvedFontFamilies,preResolvedFonts}),propsJSON=JSON.stringify(merged),compileKey=RenderCache.createCompileCacheKey({propsJSON,templateContentHash,templateId}),compiled=preCompiled;if(!compiled){if(!component)throw Error(`Must provide either compiledTemplate or component`);let cachedCompile=cache.getCompiled(compileKey);if(cachedCompile)compiled=cachedCompile;else{let element=createElement(component,merged);compiled=await compileTemplate(element),cache.setCompiledWithContentHash(compileKey,templateContentHash,compiled)}}let{node:resolvedNode,images:resolvedImages}=await resolveImages(compiled.node,images),renderReady=resolvedNode===compiled.node?compiled:{...compiled,node:resolvedNode};return{compileKey,compiled,mergedProps:merged,propsJSON,renderReady,resolvedFontFamilies,resolvedFonts,resolvedImages,resolvedLang}},orchestrateRender=async input=>{let start=performance.now(),{compiledTemplate,component,templateContentHash,templateId,props,propsSchema,locale:localeId,loadLocaleData,fetchFonts,fonts:preResolvedFonts,fontFamilies:preResolvedFontFamilies,images,width,height,cache,format=`png`,signal}=input,prep=await prepPipeline({cache,compiledTemplate,component,fetchFonts,images,loadLocaleData,locale:localeId??`default`,preResolvedFontFamilies,preResolvedFonts,props,propsSchema,templateContentHash,templateId}),pixelKey=RenderCache.createPixelCacheKey({format,height,propsJSON:prep.propsJSON,templateContentHash,width}),cached=await cache.getPixels(pixelKey);if(cached)return{bytes:cached,cacheHit:!0,durationMs:performance.now()-start,format,height,width};let output=await renderToPixels(prep.renderReady,{height,width},{fontFamilies:(prep.resolvedFontFamilies??[]).length>0?prep.resolvedFontFamilies:void 0,fonts:prep.resolvedFonts.length>0?prep.resolvedFonts:void 0,format,images:prep.resolvedImages.length>0?prep.resolvedImages:void 0,lang:prep.resolvedLang},signal);try{await cache.setPixels(pixelKey,output.bytes,width,height,format)}catch(error){logWarn(`Failed to write to pixel cache`,{error:String(error),pixelKey})}return{bytes:output.bytes,cacheHit:!1,durationMs:performance.now()-start,format,height:output.height,width:output.width}},orchestrateMeasure=async input=>{let start=performance.now(),{compiledTemplate,component,templateContentHash,templateId,props,propsSchema,locale:localeId,loadLocaleData,fetchFonts,images,cache,signal}=input,prep=await prepPipeline({cache,compiledTemplate,component,fetchFonts,images,loadLocaleData,locale:localeId??`default`,preResolvedFontFamilies:void 0,preResolvedFonts:void 0,props,propsSchema,templateContentHash,templateId}),measured=await measureTemplate(prep.renderReady,{fontFamilies:(prep.resolvedFontFamilies??[]).length>0?prep.resolvedFontFamilies:void 0,fonts:prep.resolvedFonts.length>0?prep.resolvedFonts:void 0,images:prep.resolvedImages.length>0?prep.resolvedImages:void 0,lang:prep.resolvedLang},signal);return{durationMs:performance.now()-start,height:measured.height,width:measured.width}},ensureDir=async dir=>{existsSync(dir)||await mkdir(dir,{recursive:!0})},tryWriteFile=async(filePath,data)=>{try{await writeFile(filePath,data)}catch(error){logWarn(`Failed to write output file`,{error:String(error),filePath})}},noop=()=>{},noopCache={getCompiled:()=>null,getPixels:()=>null,setCompiledWithContentHash:noop,setPixels:noop},renderComponent=async input=>{let{component,format=`png`,images,outDir=`./out`,preset,props,propsSchema}=input,result=await orchestrateRender({cache:noopCache,component,format,height:preset.height,images,props,propsSchema,templateContentHash:`single`,templateId:`single`,width:preset.width}),outputPath=`${outDir}/${generateOutputFilename({locale:`default`,presetId:preset.id,rowKey:`single`,templateId:`single`})}`;return await ensureDir(outDir),await writeFile(outputPath,result.bytes),{outputPath}},writeManifest=async(filePath,manifest)=>{await mkdir(path.dirname(filePath),{recursive:!0}),await writeFile(filePath,JSON.stringify(manifest,null,2))},expandMatrix=input=>{let{rows,locales,presets,baseProps={}}=input;if(rows.length===0)return[];let cells=[];for(let[rowIndex,row]of rows.entries()){let rowKey=String(row.key??row.id??row.slug??String(rowIndex)),rowProps={...baseProps,...row};delete rowProps.key,delete rowProps.id,delete rowProps.slug;for(let localeId of locales)for(let preset of presets)cells.push({locale:localeId,preset:{height:preset.height,id:preset.id,width:preset.width},props:{...rowProps,locale:localeId},rowIndex,rowKey})}return cells},runBatch=async input=>{let start=performance.now(),{baseProps={},dataFile,failFast=!1,cacheDir:cacheDirOverride,locales:filterLocales,outDir:outDirOverride,presets:filterPresets,rootDir,rows:inlineRows,templateId}=input,config=await loadConfig(rootDir),fetchFonts=createFetchFontsFromConfig(config.fonts,rootDir),entry=config.templates[templateId];if(!entry)throw Error(`Template not found: ${templateId}`);let{contentHash,module}=await importTemplateForBatch(rootDir,entry.componentPath,templateId),Component=module.default,presets=(filterPresets??config.presets.map(p=>p.id)).map(id=>{let p=config.presets.find(preset=>preset.id===id);if(!p){let available=config.presets.map(preset=>preset.id).join(`, `);throw Error(`Preset "${id}" not found in studio.config.ts. Available presets: ${available}`)}return{height:p.height,id:p.id,width:p.width}}),localeCodes=filterLocales??config.locales??[],resolvedLocales=await resolveLocales(rootDir,localeCodes,templateId),localeDataMap=new Map(resolvedLocales.map(l=>[l.id,l.data??{}])),loadLocaleData=code=>Promise.resolve(localeDataMap.get(code)??{}),rows=dataFile?await parseDataFile(path.resolve(rootDir,dataFile)):inlineRows??[],cells=expandMatrix({baseProps,locales:resolvedLocales.map(l=>l.id),presets,rows}),outDir=path.resolve(rootDir,outDirOverride??config.outDir??`./out`),cache=await openCache(rootDir,{cacheDir:cacheDirOverride}),succeeded=[],failed=[];try{for(let cell of cells)try{let filename=generateOutputFilename({locale:cell.locale,presetId:cell.preset.id,rowKey:cell.rowKey,templateId}),result=await orchestrateRender({cache,component:Component,fetchFonts,format:`png`,height:cell.preset.height,images:config.images,loadLocaleData,locale:cell.locale,props:cell.props,propsSchema:module.propsSchema,templateContentHash:contentHash,templateId,width:cell.preset.width}),outputPath=`${outDir}/${filename}`;await ensureDir(outDir),await tryWriteFile(outputPath,result.bytes),succeeded.push({cacheHit:result.cacheHit,locale:cell.locale,outputPath,preset:cell.preset.id,rowIndex:cell.rowIndex})}catch(error){if(failed.push({error:error instanceof Error?error.message:String(error),locale:cell.locale,preset:cell.preset.id,rowIndex:cell.rowIndex}),failFast)throw error}}finally{cache.close()}let manifest={completedCount:cells.length,failed,status:failFast&&failed.length>0?`aborted`:`completed`,succeeded,totalCount:cells.length};return await writeManifest(`${outDir}/manifest.json`,manifest),{elapsedMs:performance.now()-start,manifest}};export{AppError,ErrorCode,PropValidationError,RenderCache,clearFontCache,compileTemplate,createFetchFontsFromConfig,importTemplateForBatch,isZodObject,loadConfig,logDebug,logError,logInfo,logWarn,measureTemplate,openCache,orchestrateMeasure,orchestrateRender,parseStudioConfig,registerFont,renderAutoSized,renderComponent,renderToPixels,resolveAssetsForLocale,resolveProjectPaths,runBatch,scriptOf,studioConfigSchema,validateProps,zodToJsonSchemaShape};
28
+ `.execute(db)}close(){this.closeDb&&=(this.closeDb(),void 0),this.db=void 0,this.initialized=!1,this.initPromise=void 0}deleteDatabaseFiles(){if(this.dbPath!==`:memory:`)for(let suffix of[``,`-wal`,`-shm`])try{unlinkSync(`${this.dbPath}${suffix}`)}catch{}}async upsertEntry(input){let db=this.requireDb(),now=Date.now();await db.insertInto(`pixel_cache_entries`).values({content_hash:input.contentHash,created_at:now,file_name:input.fileName,hash:input.hash,height:input.height,last_accessed_at:now,size_bytes:input.sizeBytes,width:input.width}).onConflict(oc=>oc.column(`hash`).doUpdateSet({content_hash:input.contentHash,file_name:input.fileName,height:input.height,last_accessed_at:now,size_bytes:input.sizeBytes,width:input.width})).execute()}async getEntry(hash){let row=await this.requireDb().selectFrom(`pixel_cache_entries`).selectAll().where(`hash`,`=`,hash).executeTakeFirst();return row?toRow(row):null}async touchAccess(hash,timestamp=Date.now()){await this.requireDb().updateTable(`pixel_cache_entries`).set({last_accessed_at:timestamp}).where(`hash`,`=`,hash).execute()}async deleteEntry(hash){await this.requireDb().deleteFrom(`pixel_cache_entries`).where(`hash`,`=`,hash).execute()}async deleteAll(){await this.requireDb().deleteFrom(`pixel_cache_entries`).execute()}async listAllOrderedByLastAccessed(){return(await this.requireDb().selectFrom(`pixel_cache_entries`).selectAll().orderBy(`last_accessed_at`,`asc`).execute()).map(toRow)}async getTotals(){let row=await this.requireDb().selectFrom(`cache_totals`).select([`total_size_bytes`,`entry_count`]).where(`id`,`=`,1).executeTakeFirstOrThrow();return{entryCount:row.entry_count,totalSizeBytes:row.total_size_bytes}}requireDb(){if(!this.db)throw Error(`MetadataStore.init() must be called before use.`);return this.db}};const RECOVERY_COOLDOWN_MS=2e3,isCacheEntryFileName=name=>/^[0-9a-f]{32,}\.[a-z]+$/u.test(name);var RenderCache=class{cacheDir;maxSizeBytes;compiled;maxConcurrentWrites;evictionDebounceMs;metadata;fsStore;eviction;initialized=!1;recoveryPromise=null;lastRecoveryAt=0;static createCompileCacheKey=createCompileCacheKey;static createPixelCacheKey=createPixelCacheKey;constructor(options){this.cacheDir=options?.cacheDir??`.studio-cache`,this.maxSizeBytes=(options?.maxSizeMB??500)*1024*1024,this.compiled=new CompileCache(options?.compiledMaxEntries),this.maxConcurrentWrites=options?.maxConcurrentWrites,this.evictionDebounceMs=options?.evictionDebounceMs}async init(){if(this.initialized)return;existsSync(this.cacheDir)||await mkdir(this.cacheDir,{recursive:!0}),this.metadata=new MetadataStore(this.cacheDir),await this.metadata.init();let limiter=new ConcurrencyLimiter(this.maxConcurrentWrites);this.fsStore=new FsStore(limiter),this.eviction=new EvictionCoordinator({debounceMs:this.evictionDebounceMs,getCurrentTotalBytes:async()=>(await this.requireMetadata().getTotals()).totalSizeBytes,lockTargetPath:this.cacheDir,maxSizeBytes:this.maxSizeBytes,sweep:options=>this.performSweep(options)}),this.initialized=!0}close(){this.eviction?.cancelPending(),this.metadata?.close(),this.initialized=!1}getCompiled(key){return this.compiled.get(key)}setCompiled(key,value){this.compiled.set(key,value)}setCompiledWithContentHash(key,contentHash,value){this.compiled.setWithContentHash(key,contentHash,value)}invalidateCompiledByContentHash(contentHash){this.compiled.invalidateByContentHash(contentHash)}clearCompiled(){this.compiled.clear()}async getPixels(key){let metadata=this.requireMetadata(),fsStore=this.requireFsStore(),row;try{row=await metadata.getEntry(key)}catch(error){return logWarn(`Pixel metadata read failed, attempting recovery`,{error:error instanceof Error?error.message:String(error),key}),await this.recoverMetadata(),null}if(!row)return null;let filePath=path.join(this.cacheDir,row.fileName),bytes=await fsStore.readVerified(filePath,row.sizeBytes,row.contentHash);if(bytes===null){try{await metadata.deleteEntry(key)}catch{}return null}try{await metadata.touchAccess(key)}catch(error){logWarn(`Pixel touchAccess failed, attempting recovery`,{error:error instanceof Error?error.message:String(error),key}),await this.recoverMetadata()}return bytes}async setPixels(key,bytes,width,height,format=`png`){let metadata=this.requireMetadata(),fsStore=this.requireFsStore(),fileName=`${key}.${format}`,filePath=path.join(this.cacheDir,fileName),contentHash=fsStore.computeHash(bytes);try{await fsStore.writeAtomic(filePath,bytes)}catch(error){if(error?.code===`ENOENT`)logDebug(`FsStore: ENOENT during writeAtomic, recreating cache directory and retrying`,{filePath}),await mkdir(this.cacheDir,{recursive:!0}),await fsStore.writeAtomic(filePath,bytes);else throw error}try{await metadata.upsertEntry({contentHash,fileName,hash:key,height,sizeBytes:bytes.length,width})}catch(error){let err=error;if(err?.code===`SQLITE_IOERR_VNODE`||(err?.message??``).includes(`disk I/O error`))logWarn(`Metadata write failed (I/O error), recovering and retrying`,{error:String(error),key}),await this.recoverMetadata(),metadata=this.requireMetadata(),await metadata.upsertEntry({contentHash,fileName,hash:key,height,sizeBytes:bytes.length,width});else throw error}return this.requireEviction().requestEviction(),filePath}async stats(){let totals=await this.requireMetadata().getTotals();return{cacheDir:this.cacheDir,compiled:{entries:this.compiled.size},pixels:{entries:totals.entryCount,maxSizeBytes:this.maxSizeBytes,sizeBytes:totals.totalSizeBytes}}}clean(){return this.compiled.clear(),this.requireEviction().runFullSweep()}async performSweep(options){let metadata=this.requireMetadata(),fsStore=this.requireFsStore();if(options.full){let rows=await metadata.listAllOrderedByLastAccessed(),freedAmounts=await Promise.all(rows.map(row=>fsStore.delete(path.join(this.cacheDir,row.fileName),row.sizeBytes)));await metadata.deleteAll();let orphanFreed=await this.deleteOrphanFiles(new Set(rows.map(r=>r.fileName)),fsStore);return{freedBytes:freedAmounts.reduce((sum,n)=>sum+n,0)+orphanFreed}}let totals=await metadata.getTotals();if(totals.totalSizeBytes<=this.maxSizeBytes)return{freedBytes:0};let rows=await metadata.listAllOrderedByLastAccessed(),remaining=totals.totalSizeBytes,rowsToEvict=[];for(let row of rows){if(remaining<=this.maxSizeBytes)break;rowsToEvict.push(row),remaining-=row.sizeBytes}return{freedBytes:(await Promise.all(rowsToEvict.map(async row=>{let freedForRow=await fsStore.delete(path.join(this.cacheDir,row.fileName),row.sizeBytes);return await metadata.deleteEntry(row.hash),freedForRow}))).reduce((sum,n)=>sum+n,0)}}async deleteOrphanFiles(knownFileNames,fsStore){let freed=0;try{let entries=await readdir(this.cacheDir);await Promise.all(entries.map(async entry=>{if(knownFileNames.has(entry)||!isCacheEntryFileName(entry))return;let filePath=path.join(this.cacheDir,entry);logDebug(`FsStore: removing orphaned cache file`,{filePath}),freed+=await fsStore.delete(filePath)}))}catch(error){logWarn(`RenderCache: failed to scan for orphaned files`,{error:String(error)})}return freed}async recoverMetadata(){if(Date.now()-this.lastRecoveryAt<RECOVERY_COOLDOWN_MS)logDebug(`RenderCache: skipping recovery — within cooldown window`,{cooldownMs:RECOVERY_COOLDOWN_MS});else{if(this.recoveryPromise)return this.recoveryPromise;this.recoveryPromise=this.doRecoverMetadata();try{await this.recoveryPromise}finally{this.recoveryPromise=null}}}async doRecoverMetadata(){this.lastRecoveryAt=Date.now(),this.metadata?.close(),this.metadata=new MetadataStore(this.cacheDir),await this.metadata.init()}requireMetadata(){if(!this.metadata)throw Error(`RenderCache.init() must be called before use.`);return this.metadata}requireFsStore(){if(!this.fsStore)throw Error(`RenderCache.init() must be called before use.`);return this.fsStore}requireEviction(){if(!this.eviction)throw Error(`RenderCache.init() must be called before use.`);return this.eviction}};const resolveProjectPaths=rootDir=>({configPath:path.resolve(rootDir,`studio.config.ts`),defaultCacheDir:path.resolve(rootDir,`.studio-cache`),localesDir:path.resolve(rootDir,`locales`),templatesDir:path.resolve(rootDir,`templates`)}),openCache=async(rootDir,options)=>{let cache=new RenderCache({cacheDir:options?.cacheDir??resolveProjectPaths(rootDir).defaultCacheDir,maxSizeMB:options?.maxSizeMB});return await cache.init(),cache},SCRIPTS=[`latin`,`arabic`,`hebrew`],SCRIPT_DIRECTION={arabic:`rtl`,hebrew:`rtl`,latin:`ltr`},SCRIPT_SUBTAG={Arab:`arabic`,Hebr:`hebrew`,Latn:`latin`},LANGUAGE_SCRIPT={ar:`arabic`,ckb:`arabic`,fa:`arabic`,he:`hebrew`,ps:`arabic`,sd:`arabic`,ug:`arabic`,ur:`arabic`,yi:`hebrew`},DEFAULT_LOCALE=`default`,parseLocale=id=>{try{let locale=new Intl.Locale(id);return{language:locale.language,scriptSubtag:locale.script}}catch{return}},classifyLocale=id=>{let localeId=id??DEFAULT_LOCALE,parsed=localeId===DEFAULT_LOCALE?void 0:parseLocale(localeId);if(!parsed)return{dir:`ltr`,id:localeId,lang:void 0,script:`latin`};let script=(parsed.scriptSubtag?SCRIPT_SUBTAG[parsed.scriptSubtag]:void 0)??LANGUAGE_SCRIPT[parsed.language]??`latin`;return{dir:SCRIPT_DIRECTION[script],id:localeId,lang:script===`latin`?void 0:parsed.language,script}},BUILT_IN_FONTS={arabic:[{family:`Noto Sans Arabic`,source:`google`,weights:[400,700]}],hebrew:[{family:`Noto Sans Hebrew`,source:`google`,weights:[400,700]}],latin:[]},configuredFontEntries=(fonts,localeId)=>{let isLatin=classifyLocale(localeId).script===`latin`,configured=fonts?.[localeId]??(isLatin?fonts?.default:void 0);return Array.isArray(configured)?configured:[]},fontSetKey=(entries,configDir=``)=>JSON.stringify([configDir,entries.map(e=>[e.source??``,e.family,e.weights??[],e.path??``])]),isFontObject=f=>typeof f==`object`&&!(f instanceof Uint8Array),loadLocalFontDescriptor=async(family,filePath,weight,configDir)=>{let resolved=configDir?path.resolve(configDir,filePath):path.resolve(filePath);return{data:await readFile(resolved),name:family,weight:weight??400}},isLocalConfig=c=>c.source===`local`&&typeof c.path==`string`&&c.path.length>0,loadFontSet=async(configs,configDir)=>{try{let localConfigs=configs.filter(isLocalConfig),googleConfigs=configs.filter(c=>!isLocalConfig(c)),localFonts=await Promise.all(localConfigs.map(c=>loadLocalFontDescriptor(c.family,c.path,c.weights?.[0],configDir))),googleFontDescriptors=googleConfigs.length>0?await googleFonts(googleConfigs.map(c=>({name:c.family,weight:[...c.weights??[400,700]]}))):[],fonts=[...localFonts,...googleFontDescriptors],fontFamilies=fonts.filter(isFontObject).map(f=>String(f.name??``)).filter(Boolean);return logDebug(`fonts resolved`,{count:fonts.length,names:fontFamilies}),fonts.length>0?{fontFamilies,fonts}:null}catch(error){return logDebug(`fonts failed to load`,{error:String(error),families:configs.map(c=>c.family)}),null}},fontCache=/* @__PURE__ */ new Map,clearFontCache=()=>{fontCache.clear()},loadFontConfigs=async(configs,configDir)=>{if(configs.length===0)return null;let key=fontSetKey(configs,configDir),cached=fontCache.get(key);if(cached)return await cached;let promise=loadFontSet(configs,configDir);fontCache.set(key,promise);let resolution=await promise;return resolution||fontCache.delete(key),resolution},createFetchFontsFromConfig=(fontsConfig,configDir)=>{if(fontsConfig)return async function(localeId){return typeof localeId==`string`?await loadFontConfigs(configuredFontEntries(fontsConfig,localeId),configDir):null}},WRAPPER_TYPES=/* @__PURE__ */ new Set([`default`,`prefault`,`optional`,`nullable`,`readonly`,`catch`]),isRecord=value=>typeof value==`object`&&!!value&&!Array.isArray(value),unwrap=schema=>{let current=schema;for(;WRAPPER_TYPES.has(current.type);)current=current.def.innerType;return current},shapeOf=schema=>{let inner=unwrap(schema);return inner.type===`object`?inner.shape:void 0},elementOf=schema=>{let inner=unwrap(schema);return inner.type===`array`?inner.element:void 0},hasFieldMeta=schema=>{if(getFieldMeta(schema))return!0;let shape=shapeOf(schema);if(shape)return Object.values(shape).some(hasFieldMeta);let element=elementOf(schema);return element?hasFieldMeta(element):!1},translateValue=(schema,value,translation)=>{let meta=getFieldMeta(schema);if(meta?.kind===`text`)return meta.translatable&&typeof translation==`string`?translation:value;let shape=shapeOf(schema);if(shape)return isRecord(value)&&isRecord(translation)?translateObject(shape,value,translation):value;let element=elementOf(schema);return element&&Array.isArray(value)&&Array.isArray(translation)?value.map((item,index)=>index<translation.length?translateValue(element,item,translation[index]):item):value},translateObject=(shape,values,translations)=>{let result={...values};for(let[key,translation]of Object.entries(translations)){let schema=shape[key];schema&&key in values&&(result[key]=translateValue(schema,values[key],translation))}return result},translateByValue=(values,translations)=>{let result={...values};for(let[key,translation]of Object.entries(translations)){let current=result[key];(typeof translation==`string`&&typeof current==`string`||Array.isArray(translation)&&Array.isArray(current))&&(result[key]=translation)}return result},applyTranslations=(schema,values,translations)=>{if(!translations)return{...values};let shape=schema?shapeOf(schema):void 0;return schema&&shape&&hasFieldMeta(schema)?translateObject(shape,values,translations):translateByValue(values,translations)},combine=parts=>{let present=parts.filter(part=>part!==null);return present.length===0?null:{fontFamilies:[...new Set(present.flatMap(part=>part.fontFamilies))],fonts:present.flatMap(part=>part.fonts)}},resolveFonts=async(localeId,script,options)=>{let configured=options?.fetchFonts?await options.fetchFonts(localeId):null,configuredFamilies=new Set(configured?.fontFamilies),declared=(options?.declaredFonts??[]).filter(c=>!configuredFamilies.has(c.family)),declaredResolution=await loadFontConfigs(declared);declared.length>0&&!declaredResolution&&logWarn(`Template fonts failed to load (${declared.map(c=>c.family).join(`, `)}); rendering with fallbacks.`);let builtIn=configured?[]:BUILT_IN_FONTS[script].filter(c=>!declared.some(d=>d.family===c.family)),fallback=await loadFontConfigs(builtIn);return combine([configured,declaredResolution,fallback])},resolveAssetsForLocale=async(localeId,props,options)=>{let localeData=options?.localeData;if(options?.loadLocaleData)try{localeData=await options.loadLocaleData(localeId)}catch{localeData={}}let mergedProps=applyTranslations(options?.propsSchema,props,localeData),{lang,script}=classifyLocale(localeId);if(options?.preResolvedFonts)return{fontFamilies:options.preResolvedFontFamilies,fonts:options.preResolvedFonts,lang,props:mergedProps};let resolution=await resolveFonts(localeId,script,options);return{fontFamilies:resolution?.fontFamilies,fonts:resolution?.fonts??[],lang,props:mergedProps}},templateCache=/* @__PURE__ */ new Map,loadTemplateForBatch=async(fullPath,templateId)=>{let content;try{content=await readFile(fullPath,`utf-8`)}catch{throw new AppError(ErrorCode.TEMPLATE_LOAD_FAILED,`Template file not found: ${fullPath}`,{componentPath:fullPath,templateId})}let contentHash=createHash(`sha256`).update(content).digest(`hex`).slice(0,16);logDebug(`Loading template`,{path:fullPath,templateId});let mod=await import(pathToFileURL(fullPath).href);return{contentHash,template:toTemplateDefinition(mod,templateId)}},importTemplateForBatch=async(rootDir,componentPath,templateId)=>{let fullPath=path.resolve(rootDir,`${componentPath}.tsx`),cached=templateCache.get(fullPath);if(cached)return cached;let promise=loadTemplateForBatch(fullPath,templateId);templateCache.set(fullPath,promise);try{return await promise}catch{throw templateCache.delete(fullPath),await promise.catch(error=>error)}},loadConfig=async rootDir=>{let{configPath}=resolveProjectPaths(rootDir);try{await readFile(configPath,`utf-8`)}catch{throw new AppError(ErrorCode.CONFIG_NOT_FOUND,`No studio.config.ts found in ${rootDir}.`)}logDebug(`Loading studio.config.ts`,{path:configPath});let mod=await import(pathToFileURL(configPath).href),raw=mod.default??mod,result=parseStudioConfig(raw);if(`issues`in result)throw new AppError(ErrorCode.CONFIG_INVALID,`studio.config.ts is invalid: ${result.issues}`);return result.config},parseDataFile=async filePath=>{let content=await readFile(filePath,`utf-8`);if(filePath.endsWith(`.json`)){let data=JSON.parse(content);if(Array.isArray(data))return data;throw new AppError(ErrorCode.API_ERROR,`JSON data file must contain an array of objects.`)}if(filePath.endsWith(`.csv`)){let{parse}=await import(`csv-parse/sync`);return parse(content,{columns:!0,skip_empty_lines:!0,trim:!0})}throw new AppError(ErrorCode.API_ERROR,`Data file must be .csv or .json.`)},resolveLocales=async(rootDir,localeCodes,templateId)=>{if(localeCodes.length===0)return[{id:`default`}];let{localesDir}=resolveProjectPaths(rootDir);return await Promise.all(localeCodes.map(async code=>{let localePath=path.resolve(localesDir,`${code}.json`),content;try{content=await readFile(localePath,`utf-8`)}catch{return logWarn(`No locales/${code}.json found — rendering "${code}" with untranslated props.`),{id:code}}let parsed;try{parsed=JSON.parse(content)}catch(error){throw new AppError(ErrorCode.CONFIG_INVALID,`locales/${code}.json is not valid JSON: ${error instanceof Error?error.message:String(error)}`)}let data=parsed[templateId];return data||logWarn(`locales/${code}.json has no "${templateId}" entry — expected { "${templateId}": { ...props } }.`),{data,id:code}}))},generateOutputFilename=options=>{let{templateId,locale,presetId,rowKey,format=`png`}=options;return`${templateId}.${locale}.${presetId}.${rowKey}.${format}`};var BoundedImageFetchCache=class{maxEntries;store=/* @__PURE__ */ new Map;constructor(maxEntries=500){this.maxEntries=maxEntries}get size(){return this.store.size}get(url){let value=this.store.get(url);if(value!==void 0)return this.store.delete(url),this.store.set(url,value),value}set(url,data){if(this.store.delete(url),this.store.set(url,data),this.store.size>this.maxEntries){let oldest=this.store.keys().next().value;oldest!==void 0&&this.store.delete(oldest)}}delete(url){this.store.delete(url)}clear(){this.store.clear()}},ImagePolicyBlockedError=class extends Error{url;constructor(url){super(`Blocked by image fetch policy: ${url}`),this.name=`ImagePolicyBlockedError`,this.url=url}};const denyAll=()=>!1;let sharedFetchCache=null;const getSharedFetchCache=()=>(sharedFetchCache||=new BoundedImageFetchCache,sharedFetchCache),isAllowUrlBlockedError=error=>error instanceof Error&&error.message.startsWith(`URL blocked by allowUrl policy: `),resolveImages=async(node,policy,deps)=>{let prepareImagesFn=deps?.prepareImages??prepareImages,extractEmojisFn=deps?.extractEmojis??extractEmojis,fetchCache=deps?.fetchCache??getSharedFetchCache(),resolvedNode=policy?.emoji&&policy.emoji!==`from-font`?extractEmojisFn(node,policy.emoji):node;try{let images=await prepareImagesFn({allowUrl:policy?.allowUrl??denyAll,fetch:policy?.fetch,fetchCache,maxBytes:policy?.maxBytes,node:resolvedNode,timeout:policy?.timeout});return logDebug(`image-resolver: resolved images`,{count:images.length}),{images,node:resolvedNode}}catch(error){throw isAllowUrlBlockedError(error)?new ImagePolicyBlockedError(error.message.slice(32)):(logWarn(`image-resolver: image fetch failed`,{error:error instanceof Error?error.message:String(error)}),error)}},DEFAULT_WEIGHTS=[400],toFace=face=>typeof face==`string`?{family:face}:face,fontStack=role=>[...role.faces.map(face=>toFace(face).family),role.generic??`sans-serif`].join(`, `),declaredFontConfigs=roles=>{let weightsByFamily=/* @__PURE__ */ new Map;for(let role of Object.values(roles))for(let face of role.faces.map(toFace)){let weights=weightsByFamily.get(face.family)??/* @__PURE__ */ new Set;for(let weight of face.weights??DEFAULT_WEIGHTS)weights.add(weight);weightsByFamily.set(face.family,weights)}return[...weightsByFamily].map(([family,weights])=>({family,source:`google`,weights:[...weights].toSorted((a,b)=>a-b)}))},SCRIPT_TYPOGRAPHY={arabic:{latinStyling:!1,minLeading:1.15,sizeScale:.88},hebrew:{latinStyling:!1,minLeading:1.05,sizeScale:.94},latin:{latinStyling:!0,minLeading:0,sizeScale:1}},toCanvas=(width,height)=>{let ratio=width/height,aspect=`square`;return ratio>1.05?aspect=`landscape`:ratio<.95&&(aspect=`portrait`),{aspect,height,width}},createRenderContext=input=>{let{locale,canvas}=input,fonts=Object.fromEntries(Object.entries(input.fonts).map(([role,definition])=>[role,fontStack(definition)])),script=SCRIPT_TYPOGRAPHY[locale.script],rtl=locale.dir===`rtl`;return{canvas,dir:locale.dir,flip:value=>rtl&&value!==0?-value:value,fonts,locale,type:spec=>{let style={},exact=spec.scripts?.[locale.script];return exact?.size===void 0?spec.size!==void 0&&(style.fontSize=Math.round(spec.size*script.sizeScale)):style.fontSize=exact.size,exact?.leading===void 0?spec.leading!==void 0&&(style.lineHeight=Math.max(spec.leading,script.minLeading)):style.lineHeight=exact.leading,spec.weight!==void 0&&(style.fontWeight=spec.weight),spec.font!==void 0&&(style.fontFamily=fonts[spec.font]),script.latinStyling&&(spec.tracking!==void 0&&(style.letterSpacing=`${spec.tracking}em`),spec.italic&&(style.fontStyle=`italic`),spec.uppercase&&(style.textTransform=`uppercase`)),style}}},MEASURE_CANVAS=toCanvas(1200,630),withLocaleRoot=(element,locale)=>{if(isValidElement(element)&&typeof element.type==`string`){let props=element.props;return createElement(element.type,{...props,dir:props.dir??locale.dir,key:element.key,lang:props.lang??locale.lang})}return createElement(`div`,{dir:locale.dir,lang:locale.lang,style:{display:`flex`,height:`100%`,width:`100%`}},element)},resolveDefinition=input=>{if(input.template)return input.template;if(input.component)return legacyTemplateDefinition(input.component,input.propsSchema??z.looseObject({}),input.templateId)},prepPipeline=async input=>{let{compiledTemplate:preCompiled,templateContentHash,templateId,props,locale,canvas,loadLocaleData,preResolvedFonts,preResolvedFontFamilies,fetchFonts,images,cache}=input,definition=resolveDefinition(input),validated=definition?validateProps(definition.propsSchema,props):{...props},{fonts:resolvedFonts,fontFamilies:resolvedFontFamilies,lang:resolvedLang,props:translated}=await resolveAssetsForLocale(locale,validated,{declaredFonts:definition?declaredFontConfigs(definition.fonts):[],fetchFonts,loadLocaleData,preResolvedFontFamilies,preResolvedFonts,propsSchema:definition?.propsSchema}),merged=definition&&!definition.legacy?validateProps(definition.propsSchema,translated):translated,localeInfo=classifyLocale(locale),propsJSON=JSON.stringify({canvas:[canvas.width,canvas.height],fonts:resolvedFontFamilies??[],locale:localeInfo.id,props:merged}),compileKey=RenderCache.createCompileCacheKey({propsJSON,templateContentHash,templateId}),compiled=preCompiled;if(!compiled){if(!definition)throw Error(`Must provide either compiledTemplate or a template`);let cachedCompile=cache.getCompiled(compileKey);if(cachedCompile)compiled=cachedCompile;else{let ctx=createRenderContext({canvas,fonts:definition.fonts,locale:localeInfo}),rendered=definition.render(merged,ctx),element=definition.legacy?rendered:withLocaleRoot(rendered,localeInfo);compiled=await compileTemplate(element),cache.setCompiledWithContentHash(compileKey,templateContentHash,compiled)}}let{node:resolvedNode,images:resolvedImages}=await resolveImages(compiled.node,images),renderReady=resolvedNode===compiled.node?compiled:{...compiled,node:resolvedNode};return{compileKey,compiled,mergedProps:merged,propsJSON,renderReady,resolvedFontFamilies,resolvedFonts,resolvedImages,resolvedLang}},orchestrateRender=async input=>{let start=performance.now(),{compiledTemplate,component,template,templateContentHash,templateId,props,propsSchema,locale:localeId,loadLocaleData,fetchFonts,fonts:preResolvedFonts,fontFamilies:preResolvedFontFamilies,images,width,height,cache,format=`png`,signal}=input,resolvedLocale=localeId??`default`,prep=await prepPipeline({cache,canvas:toCanvas(width,height),compiledTemplate,component,fetchFonts,images,loadLocaleData,locale:resolvedLocale,preResolvedFontFamilies,preResolvedFonts,props,propsSchema,template,templateContentHash,templateId}),pixelKey=RenderCache.createPixelCacheKey({format,height,propsJSON:prep.propsJSON,templateContentHash,width}),cached=await cache.getPixels(pixelKey);if(cached)return{bytes:cached,cacheHit:!0,durationMs:performance.now()-start,format,height,width};let output=await renderToPixels(prep.renderReady,{height,width},{fontFamilies:(prep.resolvedFontFamilies??[]).length>0?prep.resolvedFontFamilies:void 0,fonts:prep.resolvedFonts.length>0?prep.resolvedFonts:void 0,format,images:prep.resolvedImages.length>0?prep.resolvedImages:void 0,lang:prep.resolvedLang},signal);try{await cache.setPixels(pixelKey,output.bytes,width,height,format)}catch(error){logWarn(`Failed to write to pixel cache`,{error:String(error),pixelKey})}return{bytes:output.bytes,cacheHit:!1,durationMs:performance.now()-start,format,height:output.height,width:output.width}},orchestrateMeasure=async input=>{let start=performance.now(),{compiledTemplate,component,template,templateContentHash,templateId,props,propsSchema,locale:localeId,loadLocaleData,fetchFonts,images,cache,signal,canvasHint}=input,resolvedLocale=localeId??`default`,prep=await prepPipeline({cache,canvas:canvasHint?toCanvas(canvasHint.width,canvasHint.height):MEASURE_CANVAS,compiledTemplate,component,fetchFonts,images,loadLocaleData,locale:resolvedLocale,preResolvedFontFamilies:void 0,preResolvedFonts:void 0,props,propsSchema,template,templateContentHash,templateId}),measured=await measureTemplate(prep.renderReady,{fontFamilies:(prep.resolvedFontFamilies??[]).length>0?prep.resolvedFontFamilies:void 0,fonts:prep.resolvedFonts.length>0?prep.resolvedFonts:void 0,images:prep.resolvedImages.length>0?prep.resolvedImages:void 0,lang:prep.resolvedLang},signal);return{durationMs:performance.now()-start,height:measured.height,width:measured.width}},ensureDir=async dir=>{existsSync(dir)||await mkdir(dir,{recursive:!0})},tryWriteFile=async(filePath,data)=>{try{await writeFile(filePath,data)}catch(error){logWarn(`Failed to write output file`,{error:String(error),filePath})}},noop=()=>{},noopCache={getCompiled:()=>null,getPixels:()=>null,setCompiledWithContentHash:noop,setPixels:noop},renderComponent=async input=>{let{component,format=`png`,images,outDir=`./out`,preset,props,propsSchema}=input,result=await orchestrateRender({cache:noopCache,component,format,height:preset.height,images,props,propsSchema,templateContentHash:`single`,templateId:`single`,width:preset.width}),outputPath=`${outDir}/${generateOutputFilename({locale:`default`,presetId:preset.id,rowKey:`single`,templateId:`single`})}`;return await ensureDir(outDir),await writeFile(outputPath,result.bytes),{outputPath}},writeManifest=async(filePath,manifest)=>{await mkdir(path.dirname(filePath),{recursive:!0}),await writeFile(filePath,JSON.stringify(manifest,null,2))},expandMatrix=input=>{let{rows,locales,presets,baseProps={}}=input;if(rows.length===0)return[];let cells=[];for(let[rowIndex,row]of rows.entries()){let rowKey=String(row.key??row.id??row.slug??String(rowIndex)),rowProps={...baseProps,...row};delete rowProps.key,delete rowProps.id,delete rowProps.slug;for(let localeId of locales)for(let preset of presets)cells.push({locale:localeId,preset:{height:preset.height,id:preset.id,width:preset.width},props:rowProps,rowIndex,rowKey})}return cells},runBatch=async input=>{let start=performance.now(),{baseProps={},dataFile,failFast=!1,cacheDir:cacheDirOverride,locales:filterLocales,outDir:outDirOverride,presets:filterPresets,rootDir,rows:inlineRows,templateId}=input,config=await loadConfig(rootDir),fetchFonts=createFetchFontsFromConfig(config.fonts,rootDir),entry=config.templates[templateId];if(!entry)throw Error(`Template not found: ${templateId}`);let{contentHash,template}=await importTemplateForBatch(rootDir,entry.componentPath,templateId),presets=(filterPresets??config.presets.map(p=>p.id)).map(id=>{let p=config.presets.find(preset=>preset.id===id);if(!p){let available=config.presets.map(preset=>preset.id).join(`, `);throw Error(`Preset "${id}" not found in studio.config.ts. Available presets: ${available}`)}return{height:p.height,id:p.id,width:p.width}}),localeCodes=filterLocales??config.locales??[],resolvedLocales=await resolveLocales(rootDir,localeCodes,templateId),localeDataMap=new Map(resolvedLocales.map(l=>[l.id,l.data??{}])),loadLocaleData=code=>Promise.resolve(localeDataMap.get(code)??{}),rows=dataFile?await parseDataFile(path.resolve(rootDir,dataFile)):inlineRows??[],cells=expandMatrix({baseProps,locales:resolvedLocales.map(l=>l.id),presets,rows}),outDir=path.resolve(rootDir,outDirOverride??config.outDir??`./out`),cache=await openCache(rootDir,{cacheDir:cacheDirOverride}),succeeded=[],failed=[];try{for(let cell of cells)try{let filename=generateOutputFilename({locale:cell.locale,presetId:cell.preset.id,rowKey:cell.rowKey,templateId}),result=await orchestrateRender({cache,fetchFonts,format:`png`,height:cell.preset.height,images:config.images,loadLocaleData,locale:cell.locale,props:cell.props,template,templateContentHash:contentHash,templateId,width:cell.preset.width}),outputPath=`${outDir}/${filename}`;await ensureDir(outDir),await tryWriteFile(outputPath,result.bytes),succeeded.push({cacheHit:result.cacheHit,locale:cell.locale,outputPath,preset:cell.preset.id,rowIndex:cell.rowIndex})}catch(error){if(failed.push({error:error instanceof Error?error.message:String(error),locale:cell.locale,preset:cell.preset.id,rowIndex:cell.rowIndex}),failFast)throw error}}finally{cache.close()}let manifest={completedCount:cells.length,failed,status:failFast&&failed.length>0?`aborted`:`completed`,succeeded,totalCount:cells.length};return await writeManifest(`${outDir}/manifest.json`,manifest),{elapsedMs:performance.now()-start,manifest}};export{AppError,ErrorCode,PropValidationError,RenderCache,SCRIPTS,classifyLocale,clearFontCache,compileTemplate,createFetchFontsFromConfig,defineTemplate,importTemplateForBatch,isTemplateDefinition,isZodObject,loadConfig,logDebug,logError,logInfo,logWarn,measureTemplate,openCache,orchestrateMeasure,orchestrateRender,parseStudioConfig,registerFont,renderAutoSized,renderComponent,renderToPixels,resolveAssetsForLocale,resolveProjectPaths,runBatch,studioConfigSchema,toTemplateDefinition,validateProps,zodToJsonSchemaShape};
@@ -0,0 +1 @@
1
+ const timestamp=()=>(/* @__PURE__ */ new Date()).toISOString(),isDebugEnabled=()=>{let flag=process.env.STUDIO_DEBUG??process.env.DEBUG??``;return flag===`1`||flag===`true`||flag.includes(`studio`)},stringifyContext=context=>!context||Object.keys(context).length===0?``:` ${JSON.stringify(context)}`,logDebug=(message,context)=>{isDebugEnabled()&&console.error(`[${timestamp()}] DEBUG ${message}${stringifyContext(context)}`)},logInfo=(message,context)=>{console.error(`[${timestamp()}] INFO ${message}${stringifyContext(context)}`)},logWarn=(message,context)=>{console.warn(`[${timestamp()}] WARN ${message}${stringifyContext(context)}`)},logError=(code,message,context)=>{console.error(`[${timestamp()}] ERROR [${code}] ${message}${stringifyContext(context)}`)};export{logWarn as i,logError as n,logInfo as r,logDebug as t};
package/dist/og/index.mjs CHANGED
@@ -1 +1 @@
1
- import{i as renderToPixels,l as compileTemplate}from"../renderer-BtADMpXx.mjs";import{jsx,jsxs}from"react/jsx-runtime";import{ImageResponse}from"takumi-js/response";const getTitleSize=length=>length>60?`48px`:length>35?`60px`:`76px`,truncateText=(text,maxLength=130)=>text.length<=maxLength?text:`${text.slice(0,maxLength).trimEnd()}…`,StandardOgTemplate=({title,description,siteName})=>{let titleSize=getTitleSize(title.length),truncatedDescription=description?truncateText(description,130):null;return/* @__PURE__ */ jsxs(`div`,{style:{backgroundColor:`#0a0a0b`,color:`#e4e4e7`,display:`flex`,flexDirection:`column`,fontFamily:`system-ui, -apple-system, BlinkMacSystemFont, sans-serif`,height:`100%`,justifyContent:`space-between`,padding:`64px 72px 56px`,position:`relative`,width:`100%`},children:[/* @__PURE__ */ jsx(`div`,{style:{background:`linear-gradient(90deg, #6366f1 0%, #818cf8 100%)`,height:`4px`,left:`0`,position:`absolute`,right:`0`,top:`0`}}),/* @__PURE__ */ jsxs(`div`,{style:{alignItems:`center`,display:`flex`,gap:`10px`},children:[/* @__PURE__ */ jsx(`div`,{style:{backgroundColor:`#6366f1`,borderRadius:`50%`,flexShrink:`0`,height:`10px`,width:`10px`}}),siteName?/* @__PURE__ */ jsx(`span`,{style:{color:`#71717a`,fontSize:`18px`,fontWeight:`500`,letterSpacing:`0.04em`,textTransform:`uppercase`},children:siteName}):null]}),/* @__PURE__ */ jsxs(`div`,{style:{display:`flex`,flex:`1`,flexDirection:`column`,justifyContent:`center`,maxWidth:`860px`,paddingTop:`24px`},children:[/* @__PURE__ */ jsx(`h1`,{style:{color:`#fafafa`,fontSize:titleSize,fontWeight:`700`,letterSpacing:`-0.02em`,lineHeight:`1.1`,margin:`0`},children:title}),truncatedDescription?/* @__PURE__ */ jsx(`p`,{style:{color:`#a1a1aa`,display:`flex`,fontSize:`26px`,fontWeight:`400`,lineHeight:`1.5`,margin:`0`,marginTop:`24px`},children:truncatedDescription}):null]}),/* @__PURE__ */ jsx(`div`,{style:{backgroundColor:`#27272a`,height:`1px`,marginBottom:`0`,width:`100%`}})]})},OG_CACHE_CONTROL=`public, s-maxage=31536000, max-age=3600, immutable`,createOgErrorResponse=error=>(error&&console.error(`[og] render failed`,error),new Response(`Failed to generate OG image`,{headers:{"Cache-Control":`no-store`,"Content-Type":`text/plain; charset=utf-8`},status:500})),createOgResponse=async(element,options)=>{let{locale,format:explicitFormat,height:explicitHeight,width:explicitWidth,devicePixelRatio:explicitDpr,stylesheets,...rest}=options??{},response=new ImageResponse(element,{devicePixelRatio:explicitDpr??2,format:explicitFormat??`webp`,headers:{"Cache-Control":OG_CACHE_CONTROL,Vary:`Accept`},height:explicitHeight??630,...rest,css:stylesheets,lang:locale??rest.lang,onError:error=>{console.error(`[og] render failed`,error)},width:explicitWidth??1200});try{return await response.ready,response}catch(error){return console.error(`[og] returning 500 after render failure`,error),createOgErrorResponse()}},renderOgResponse=async(element,options)=>{let width=options?.width??1200,height=options?.height??630,format=options?.format??`webp`;try{let compiled=await compileTemplate(element),{bytes}=await renderToPixels(compiled,{height,width},{format});return new Response(new Uint8Array(bytes),{headers:{"Cache-Control":OG_CACHE_CONTROL,"Content-Type":`image/${format}`,Vary:`Accept`}})}catch{return createOgErrorResponse()}};export{StandardOgTemplate,createOgResponse,renderOgResponse};
1
+ import{a as compileTemplate,i as renderToPixels}from"../renderer-qptnxMng.mjs";import{jsx,jsxs}from"react/jsx-runtime";import{ImageResponse}from"takumi-js/response";const getTitleSize=length=>length>60?`48px`:length>35?`60px`:`76px`,truncateText=(text,maxLength=130)=>text.length<=maxLength?text:`${text.slice(0,maxLength).trimEnd()}…`,StandardOgTemplate=({title,description,siteName})=>{let titleSize=getTitleSize(title.length),truncatedDescription=description?truncateText(description,130):null;return/* @__PURE__ */ jsxs(`div`,{style:{backgroundColor:`#0a0a0b`,color:`#e4e4e7`,display:`flex`,flexDirection:`column`,fontFamily:`system-ui, -apple-system, BlinkMacSystemFont, sans-serif`,height:`100%`,justifyContent:`space-between`,padding:`64px 72px 56px`,position:`relative`,width:`100%`},children:[/* @__PURE__ */ jsx(`div`,{style:{background:`linear-gradient(90deg, #6366f1 0%, #818cf8 100%)`,height:`4px`,left:`0`,position:`absolute`,right:`0`,top:`0`}}),/* @__PURE__ */ jsxs(`div`,{style:{alignItems:`center`,display:`flex`,gap:`10px`},children:[/* @__PURE__ */ jsx(`div`,{style:{backgroundColor:`#6366f1`,borderRadius:`50%`,flexShrink:`0`,height:`10px`,width:`10px`}}),siteName?/* @__PURE__ */ jsx(`span`,{style:{color:`#71717a`,fontSize:`18px`,fontWeight:`500`,letterSpacing:`0.04em`,textTransform:`uppercase`},children:siteName}):null]}),/* @__PURE__ */ jsxs(`div`,{style:{display:`flex`,flex:`1`,flexDirection:`column`,justifyContent:`center`,maxWidth:`860px`,paddingTop:`24px`},children:[/* @__PURE__ */ jsx(`h1`,{style:{color:`#fafafa`,fontSize:titleSize,fontWeight:`700`,letterSpacing:`-0.02em`,lineHeight:`1.1`,margin:`0`},children:title}),truncatedDescription?/* @__PURE__ */ jsx(`p`,{style:{color:`#a1a1aa`,display:`flex`,fontSize:`26px`,fontWeight:`400`,lineHeight:`1.5`,margin:`0`,marginTop:`24px`},children:truncatedDescription}):null]}),/* @__PURE__ */ jsx(`div`,{style:{backgroundColor:`#27272a`,height:`1px`,marginBottom:`0`,width:`100%`}})]})},OG_CACHE_CONTROL=`public, s-maxage=31536000, max-age=3600, immutable`,createOgErrorResponse=error=>(error&&console.error(`[og] render failed`,error),new Response(`Failed to generate OG image`,{headers:{"Cache-Control":`no-store`,"Content-Type":`text/plain; charset=utf-8`},status:500})),createOgResponse=async(element,options)=>{let{locale,format:explicitFormat,height:explicitHeight,width:explicitWidth,devicePixelRatio:explicitDpr,stylesheets,...rest}=options??{},response=new ImageResponse(element,{devicePixelRatio:explicitDpr??2,format:explicitFormat??`webp`,headers:{"Cache-Control":OG_CACHE_CONTROL,Vary:`Accept`},height:explicitHeight??630,...rest,css:stylesheets,lang:locale??rest.lang,onError:error=>{console.error(`[og] render failed`,error)},width:explicitWidth??1200});try{return await response.ready,response}catch(error){return console.error(`[og] returning 500 after render failure`,error),createOgErrorResponse()}},renderOgResponse=async(element,options)=>{let width=options?.width??1200,height=options?.height??630,format=options?.format??`webp`;try{let compiled=await compileTemplate(element),{bytes}=await renderToPixels(compiled,{height,width},{format});return new Response(new Uint8Array(bytes),{headers:{"Cache-Control":OG_CACHE_CONTROL,"Content-Type":`image/${format}`,Vary:`Accept`}})}catch{return createOgErrorResponse()}};export{StandardOgTemplate,createOgResponse,renderOgResponse};
@@ -0,0 +1 @@
1
+ import{t as logDebug}from"./logger-DLJyIHAR.mjs";import{fromJsx}from"takumi-js/helpers/jsx";import{Renderer}from"takumi-js/node";const compileTemplate=async(element,options)=>{let result=await fromJsx(element,options);return{node:result.node,stylesheets:result.css}},Renderer$1=Renderer;let activeRenderer=null;const __getRendererForTesting=()=>(activeRenderer||=new Renderer$1,activeRenderer),registerFont=async(font,signal)=>(await __getRendererForTesting().registerFont(font,null,signal)).map(family=>family.name),renderToPixels=async(template,dimensions,options,signal)=>{let renderer=__getRendererForTesting();logDebug(`renderer.render start`,{fontFamilies:options?.fontFamilies,fonts:options?.fonts?.filter(f=>typeof f==`object`&&`name`in f).map(f=>({dataType:typeof f.data,name:f.name})),fontsCount:options?.fonts?.length??0,lang:options?.lang});let{stylesheets,...nativeOptions}=options??{};return{bytes:await renderer.render(template.node,{...nativeOptions,css:[...stylesheets??[],...template.stylesheets],height:dimensions.height,width:dimensions.width},signal),format:options?.format??`png`,height:dimensions.height,width:dimensions.width}},measureTemplate=(template,options,signal)=>{let renderer=__getRendererForTesting(),{stylesheets,...nativeOptions}=options??{};return renderer.measure(template.node,{...nativeOptions,css:[...stylesheets??[],...template.stylesheets]},signal)},renderAutoSized=async(template,options,signal)=>{let measured=await measureTemplate(template,options,signal);return renderToPixels(template,{height:Math.ceil(measured.height),width:Math.ceil(measured.width)},options,signal)};export{compileTemplate as a,renderToPixels as i,registerFont as n,renderAutoSized as r,measureTemplate as t};
package/dist/schema.mjs CHANGED
@@ -1 +1 @@
1
- export{n as defineSchema}from"./validation-BzP0M2-C.mjs";
1
+ export{n as defineSchema}from"./validation-C7SDI7z-.mjs";
@@ -0,0 +1,89 @@
1
+ import { c as TypeSpec, f as Script, g as FontRoles, h as FontRole, l as Direction, m as FontFace, n as TemplateSpec, o as Canvas, r as defineTemplate, s as RenderContext, t as TemplateDefinition, u as LocaleInfo } from "./define-template-DIghvJHh.mjs";
2
+ import { z } from "zod";
3
+ import { CSSProperties } from "react";
4
+ /**
5
+ * Prop builders for `defineTemplate`'s schema. Each one records what the
6
+ * field is (see `shared/field-meta.ts`) and carries its whole definition
7
+ * (default, limits, description) as options: chaining `.describe()` or
8
+ * `.min()` onto the result would build a new schema without that record.
9
+ */
10
+ interface Described {
11
+ /** Help text shown next to the field in the studio. */
12
+ description?: string;
13
+ }
14
+ declare const text: (defaultValue: string, options?: Described & {
15
+ min?: number;
16
+ max?: number;
17
+ multiline?: boolean;
18
+ /** Locale files may replace it. @default true */
19
+ translatable?: boolean;
20
+ }) => z.ZodDefault<z.ZodString>;
21
+ declare const color$1: (defaultValue: string, options?: Described) => z.ZodDefault<z.ZodString>;
22
+ declare const number: (defaultValue: number, options?: Described & {
23
+ min?: number;
24
+ max?: number;
25
+ integer?: boolean;
26
+ }) => z.ZodDefault<z.ZodNumber>;
27
+ declare const boolean: (defaultValue: boolean, options?: Described) => z.ZodDefault<z.ZodBoolean>;
28
+ declare const select: <const T extends readonly [string, ...string[]]>(values: T, defaultValue: T[number], options?: Described) => z.ZodDefault<z.ZodEnum<{ [k in T[number]]: k; } extends (infer T_1) ? { [k in keyof T_1]: T_1[k]; } : never>>;
29
+ declare const list: <T extends z.ZodType>(item: T, options: Described & {
30
+ default: z.input<T>[];
31
+ /** Exact item count; use `min`/`max` for a range. */
32
+ length?: number;
33
+ min?: number;
34
+ max?: number;
35
+ }) => z.ZodDefault<z.ZodArray<T>>;
36
+ /** A group of fields; every field inside needs a default, so the group defaults to `{}`. */
37
+ declare const object: <T extends z.ZodRawShape>(shape: T, options?: Described) => z.ZodPrefault<z.ZodObject<{ -readonly [P in keyof T]: T[P]; }, z.core.$strip>>;
38
+ export declare const field: {
39
+ boolean: typeof boolean;
40
+ color: typeof color$1;
41
+ list: typeof list;
42
+ number: typeof number;
43
+ object: typeof object;
44
+ select: typeof select;
45
+ text: typeof text;
46
+ };
47
+ /**
48
+ * Small pieces every poster template ends up writing. Glyphs are drawn,
49
+ * not typed: display faces rarely ship arrows or sparkles, and a missing
50
+ * glyph renders as tofu.
51
+ */
52
+ interface Keyed<T> {
53
+ key: string;
54
+ value: T;
55
+ index: number;
56
+ }
57
+ /**
58
+ * Pairs each item with a React key that stays unique when items repeat
59
+ * (two "Stickers" cells), without falling back to the bare index.
60
+ */
61
+ export declare const keyed: <T>(items: readonly T[], keyOf?: (item: T) => string) => Keyed<T>[];
62
+ /** The words of a line, for headlines set one word per line. */
63
+ export declare const words: (text: string) => string[];
64
+ /**
65
+ * Star polygon points in a 100×100 viewBox: `spikes` points, alternating
66
+ * between the `outer` and `inner` radius. Use as `<polygon points={…} />`.
67
+ */
68
+ export declare const starburst: (spikes: number, outer: number, inner: number) => string;
69
+ /** A print-style dot screen as a background: dots of `color` on a `spacing` px grid. */
70
+ export declare const halftone: ({ color, spacing, dot }: {
71
+ color: string;
72
+ spacing: number;
73
+ /** Dot radius as a share of the cell. @default 0.3 */
74
+ dot?: number;
75
+ }) => CSSProperties;
76
+ /** A forward arrow: points right in left-to-right renders and left in right-to-left ones. */
77
+ export declare const Arrow: ({ ctx, color, size, strokeWidth }: {
78
+ /** The render context, or anything with its `flip`. */
79
+ ctx: Pick<RenderContext, "flip">;
80
+ color: string;
81
+ size?: number;
82
+ strokeWidth?: number;
83
+ }) => import("react").JSX.Element;
84
+ /** A four-point sparkle. */
85
+ export declare const Sparkle: ({ color, size }: {
86
+ color: string;
87
+ size?: number;
88
+ }) => import("react").JSX.Element;
89
+ export { type Canvas, type Direction, type FontFace, type FontRole, type FontRoles, type Keyed, type LocaleInfo, type RenderContext, type Script, type TemplateDefinition, type TemplateSpec, type TypeSpec, defineTemplate };
@@ -0,0 +1 @@
1
+ import{t as defineTemplate}from"./define-template-nsnBI5Iz.mjs";import{s as setFieldMeta}from"./validation-C7SDI7z-.mjs";import{z}from"zod";import{jsx}from"react/jsx-runtime";const HEX_COLOR=/^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/iu,describe=(schema,description)=>description?schema.describe(description):schema,field={boolean:(defaultValue,options={})=>setFieldMeta(describe(z.boolean(),options.description).default(defaultValue),{kind:`boolean`,translatable:!1}),color:(defaultValue,options={})=>setFieldMeta(describe(z.string().regex(HEX_COLOR,`expected a hex color like #ff6a3d`),options.description).default(defaultValue),{kind:`color`,translatable:!1}),list:(item,options)=>{let schema=z.array(item);return options.length!==void 0&&(schema=schema.length(options.length)),options.min!==void 0&&(schema=schema.min(options.min)),options.max!==void 0&&(schema=schema.max(options.max)),setFieldMeta(describe(schema,options.description).default(options.default),{kind:`list`,translatable:!1})},number:(defaultValue,options={})=>{let schema=options.integer?z.int():z.number();return options.min!==void 0&&(schema=schema.min(options.min)),options.max!==void 0&&(schema=schema.max(options.max)),setFieldMeta(describe(schema,options.description).default(defaultValue),{kind:`number`,translatable:!1})},object:(shape,options={})=>setFieldMeta(describe(z.object(shape),options.description).prefault({}),{kind:`object`,translatable:!1}),select:(values,defaultValue,options={})=>setFieldMeta(describe(z.enum(values),options.description).default(defaultValue),{kind:`select`,translatable:!1}),text:(defaultValue,options={})=>{let schema=z.string();return options.min!==void 0&&(schema=schema.min(options.min)),options.max!==void 0&&(schema=schema.max(options.max)),setFieldMeta(describe(schema,options.description).default(defaultValue),{kind:`text`,multiline:options.multiline,translatable:options.translatable??!0})}},keyed=(items,keyOf=String)=>{let seen=/* @__PURE__ */ new Map;return items.map((value,index)=>{let base=keyOf(value),count=(seen.get(base)??0)+1;return seen.set(base,count),{index,key:count===1?base:`${base}#${count}`,value}})},WHITESPACE=/\s+/u,words=text=>text.split(WHITESPACE).filter(Boolean),starburst=(spikes,outer,inner)=>Array.from({length:spikes*2},(_,index)=>{let radius=index%2==0?outer:inner,angle=Math.PI*index/spikes-Math.PI/2;return`${50+radius*Math.cos(angle)},${50+radius*Math.sin(angle)}`}).join(` `),halftone=({color,spacing,dot=.3})=>{let edge=Math.round(dot*100);return{backgroundImage:`radial-gradient(${color} ${edge}%, transparent ${edge+2}%)`,backgroundSize:`${spacing}px ${spacing}px`}},Arrow=({ctx,color,size=22,strokeWidth=3})=>/* @__PURE__ */ jsx(`svg`,{fill:`none`,height:size,style:{transform:`scaleX(${ctx.flip(1)})`},viewBox:`0 0 24 24`,width:size,children:/* @__PURE__ */ jsx(`path`,{d:`M4 12h15M13 5l7 7-7 7`,stroke:color,strokeLinecap:`round`,strokeLinejoin:`round`,strokeWidth})}),Sparkle=({color,size=16})=>/* @__PURE__ */ jsx(`svg`,{height:size,viewBox:`0 0 24 24`,width:size,children:/* @__PURE__ */ jsx(`path`,{d:`M12 0 15 9 24 12 15 15 12 24 9 15 0 12 9 9Z`,fill:color})});export{Arrow,Sparkle,defineTemplate,field,halftone,keyed,starburst,words};
@@ -0,0 +1 @@
1
+ import{z}from"zod";const REGISTRY_KEY=Symbol.for(`stdout-design.field-meta`),registry=globalThis[REGISTRY_KEY]??=/* @__PURE__ */ new WeakMap,setFieldMeta=(schema,meta)=>(registry.set(schema,meta),schema),getFieldMeta=schema=>registry.get(schema),defineSchema=schema=>schema;var PropValidationError=class extends Error{issues;constructor(issues){super(`Invalid props: ${issues.map(i=>`${i.field||`(root)`} - ${i.message}${i.suggestion?`. Suggestion: ${i.suggestion}`:``}`).join(`; `)}`),this.name=`PropValidationError`,this.issues=issues}};const isObject=schema=>schema.type===`object`,isArray=schema=>schema.type===`array`,isEnum=schema=>schema.type===`enum`,isUnion=schema=>schema.type===`union`,isZodString=schema=>schema.type===`string`,isWrappable=schema=>schema.type==="default"||schema.type===`prefault`||schema.type===`optional`||schema.type===`nullable`,resolveFieldSchema=(schema,path)=>{let current=schema;for(let i=0;i<path.length;i+=1){let segment=path[i];if(i!==path.length-1&&isWrappable(current)&&(current=current.unwrap()),typeof segment==`number`){if(isArray(current)){current=current.element;continue}return}if(isObject(current)){let fieldType=current.shape[segment];if(fieldType){current=fieldType;continue}}return}return current},primitiveLabels={any:`any value`,bigint:`bigint`,boolean:`boolean`,date:`date`,never:`never`,null:`null`,number:`number`,string:`string`,symbol:`symbol`,undefined:`undefined`,unknown:`unknown value`,void:`void`},describeType=schema=>{let{type}=schema;if(isWrappable(schema)){let desc=describeType(schema.unwrap());return type===`optional`?`${desc} (optional)`:type===`nullable`?`${desc} | null`:desc}if(type in primitiveLabels)return primitiveLabels[type];if(isObject(schema))return`object (${Object.entries(schema.shape).map(([key,val])=>{let inner=isWrappable(val)?val.unwrap():val;return`${key}: ${describeType(inner)}`}).join(`, `)})`;if(isArray(schema))return`array of ${describeType(schema.element)}`;if(isEnum(schema))return`one of: ${schema.options.join(`, `)}`;if(isUnion(schema))return`one of: ${schema.options.map(o=>{let inner=isWrappable(o)?o.unwrap():o;return describeType(inner)}).join(` | `)}`;if(schema.type===`literal`){let val=schema.value;return JSON.stringify(val??`?`)}return schema.type===`record`?`object (record)`:isZodString(schema)?schema.format===`url`?`URL string`:schema.format===`email`?`email string`:schema.format===`uuid`?`UUID string`:schema.minLength===null?`string`:`string (min ${schema.minLength} characters)`:type},getObjectShapeKeys=schema=>{let inner=isWrappable(schema)?schema.unwrap():schema;if(isObject(inner))return Object.keys(inner.shape)},handleInvalidType=(issue,fieldSchema)=>{let received=`received`in issue?String(issue.received):`unknown`;if(isWrappable(fieldSchema)){let inner=fieldSchema.unwrap();return`expected ${describeType(inner)} but received ${received} — this field has a default value configured, ensure props are merged correctly before validation`}if(isObject(fieldSchema)||isWrappable(fieldSchema)&&isObject(fieldSchema.unwrap())){let keys=getObjectShapeKeys(fieldSchema);return`expected object${keys?` with properties (${keys.join(`, `)})`:``} but received ${received} — add .default({}) to make this field optional`}return isArray(fieldSchema)?`expected ${describeType(fieldSchema)} but received ${received} — add .default([]) to make this field optional`:issue.message},handleInvalidFormat=(issue,field)=>{let{format:fmt}=issue;return fmt===`url`?`invalid URL format for field '${field}' — expected a valid URL (e.g., https://example.com/image.png)`:fmt===`email`?`invalid email format for field '${field}' — expected a valid email address`:issue.message},handleInvalidValue=issue=>{let{values}=issue;return`expected one of: ${values?.join(`, `)??`unknown`}`},handleTooSmall=issue=>{let{minimum:min,origin}=issue;if(min!==null){let suffix=``;return origin===`string`?suffix=`character(s)`:origin===`array`&&(suffix=`item(s)`),`must be at least ${min} ${suffix}`}return issue.message},enhanceMessage=(issue,fieldSchema)=>{let field=issue.path.join(`.`);if(!fieldSchema)return issue.message;switch(issue.code){case`invalid_type`:return handleInvalidType(issue,fieldSchema);case`invalid_format`:return handleInvalidFormat(issue,field);case`invalid_value`:return handleInvalidValue(issue);case`too_small`:return handleTooSmall(issue);case`too_big`:return issue.message;case`unrecognized_keys`:{let{keys}=issue;return`unexpected field '${keys?.join(`, `)??field}' — not recognized by the schema, check for typos or remove it`}case`invalid_union`:{let inner=isWrappable(fieldSchema)?fieldSchema.unwrap():fieldSchema;return isUnion(inner)?`did not match any valid type — expected ${describeType(inner)}`:issue.message}default:return issue.message}},getSuggestion=(issue,fieldSchema)=>{if(fieldSchema){if(issue.code===`invalid_type`){if(isObject(fieldSchema))return`add .default({}) to make this object optional`;if(isArray(fieldSchema))return`add .default([]) to make this array optional`;if(isWrappable(fieldSchema))return`ensure props are merged correctly before validation`}if(issue.code===`invalid_value`){let{values}=issue;if(values)return`use one of: ${values.join(`, `)}`}if(issue.code===`invalid_format`){let{format:fmt}=issue;if(fmt===`url`)return`provide a valid URL starting with http:// or https://`;if(fmt===`email`)return`provide a valid email address`}}},validateProps=(schema,props)=>{let result=schema.safeParse(props);if(result.success)return result.data;throw new PropValidationError(result.error.issues.map(issue=>{let path=issue.path,fieldSchema=resolveFieldSchema(schema,path);return{code:String(issue.code),expected:`expected`in issue?String(issue.expected):`unknown`,field:path.join(`.`),message:enhanceMessage(issue,fieldSchema),received:`received`in issue?String(issue.received):`unknown`,suggestion:getSuggestion(issue,fieldSchema)}}))},isZodObject=schema=>typeof schema!=`object`||!schema?!1:schema.type===`object`,zodToJsonSchemaShape=schema=>typeof schema!=`object`||!schema||!(`shape`in schema)?{}:z.toJSONSchema(schema,{override:context=>{let meta=getFieldMeta(context.zodSchema);meta&&(context.jsonSchema[`x-field`]=meta)}});export{zodToJsonSchemaShape as a,validateProps as i,defineSchema as n,getFieldMeta as o,isZodObject as r,setFieldMeta as s,PropValidationError as t};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stdout-design/core",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Core rendering engine for stdout-design — compile TSX templates to pixel-perfect images via Takumi",
5
5
  "keywords": [
6
6
  "design",
@@ -33,6 +33,10 @@
33
33
  "import": "./dist/schema.mjs",
34
34
  "types": "./dist/schema.d.mts"
35
35
  },
36
+ "./template": {
37
+ "import": "./dist/template.mjs",
38
+ "types": "./dist/template.d.mts"
39
+ },
36
40
  "./og": {
37
41
  "import": "./dist/og/index.mjs",
38
42
  "types": "./dist/og/index.d.mts"
@@ -1 +0,0 @@
1
- import{fromJsx}from"takumi-js/helpers/jsx";import{Renderer}from"takumi-js/node";const compileTemplate=async(element,options)=>{let result=await fromJsx(element,options);return{node:result.node,stylesheets:result.css}},timestamp=()=>(/* @__PURE__ */ new Date()).toISOString(),isDebugEnabled=()=>{let flag=process.env.STUDIO_DEBUG??process.env.DEBUG??``;return flag===`1`||flag===`true`||flag.includes(`studio`)},stringifyContext=context=>!context||Object.keys(context).length===0?``:` ${JSON.stringify(context)}`,logDebug=(message,context)=>{isDebugEnabled()&&console.error(`[${timestamp()}] DEBUG ${message}${stringifyContext(context)}`)},logInfo=(message,context)=>{console.error(`[${timestamp()}] INFO ${message}${stringifyContext(context)}`)},logWarn=(message,context)=>{console.warn(`[${timestamp()}] WARN ${message}${stringifyContext(context)}`)},logError=(code,message,context)=>{console.error(`[${timestamp()}] ERROR [${code}] ${message}${stringifyContext(context)}`)},Renderer$1=Renderer;let activeRenderer=null;const __getRendererForTesting=()=>(activeRenderer||=new Renderer$1,activeRenderer),registerFont=async(font,signal)=>(await __getRendererForTesting().registerFont(font,null,signal)).map(family=>family.name),renderToPixels=async(template,dimensions,options,signal)=>{let renderer=__getRendererForTesting();logDebug(`renderer.render start`,{fontFamilies:options?.fontFamilies,fonts:options?.fonts?.filter(f=>typeof f==`object`&&`name`in f).map(f=>({dataType:typeof f.data,name:f.name})),fontsCount:options?.fonts?.length??0,lang:options?.lang});let{stylesheets,...nativeOptions}=options??{};return{bytes:await renderer.render(template.node,{...nativeOptions,css:[...stylesheets??[],...template.stylesheets],height:dimensions.height,width:dimensions.width},signal),format:options?.format??`png`,height:dimensions.height,width:dimensions.width}},measureTemplate=(template,options,signal)=>{let renderer=__getRendererForTesting(),{stylesheets,...nativeOptions}=options??{};return renderer.measure(template.node,{...nativeOptions,css:[...stylesheets??[],...template.stylesheets]},signal)},renderAutoSized=async(template,options,signal)=>{let measured=await measureTemplate(template,options,signal);return renderToPixels(template,{height:Math.ceil(measured.height),width:Math.ceil(measured.width)},options,signal)};export{logDebug as a,logWarn as c,renderToPixels as i,compileTemplate as l,registerFont as n,logError as o,renderAutoSized as r,logInfo as s,measureTemplate as t};
@@ -1 +0,0 @@
1
- import{z}from"zod";const defineSchema=schema=>schema;var PropValidationError=class extends Error{issues;constructor(issues){super(`Invalid props: ${issues.map(i=>`${i.field||`(root)`} - ${i.message}${i.suggestion?`. Suggestion: ${i.suggestion}`:``}`).join(`; `)}`),this.name=`PropValidationError`,this.issues=issues}};const isObject=schema=>schema.type===`object`,isArray=schema=>schema.type===`array`,isEnum=schema=>schema.type===`enum`,isUnion=schema=>schema.type===`union`,isZodString=schema=>schema.type===`string`,isWrappable=schema=>schema.type==="default"||schema.type===`optional`||schema.type===`nullable`,resolveFieldSchema=(schema,path)=>{let current=schema;for(let i=0;i<path.length;i+=1){let segment=path[i];if(i!==path.length-1&&isWrappable(current)&&(current=current.unwrap()),typeof segment==`number`){if(isArray(current)){current=current.element;continue}return}if(isObject(current)){let fieldType=current.shape[segment];if(fieldType){current=fieldType;continue}}return}return current},primitiveLabels={any:`any value`,bigint:`bigint`,boolean:`boolean`,date:`date`,never:`never`,null:`null`,number:`number`,string:`string`,symbol:`symbol`,undefined:`undefined`,unknown:`unknown value`,void:`void`},describeType=schema=>{let{type}=schema;if(isWrappable(schema)){let desc=describeType(schema.unwrap());return type===`optional`?`${desc} (optional)`:type===`nullable`?`${desc} | null`:desc}if(type in primitiveLabels)return primitiveLabels[type];if(isObject(schema))return`object (${Object.entries(schema.shape).map(([key,val])=>{let inner=isWrappable(val)?val.unwrap():val;return`${key}: ${describeType(inner)}`}).join(`, `)})`;if(isArray(schema))return`array of ${describeType(schema.element)}`;if(isEnum(schema))return`one of: ${schema.options.join(`, `)}`;if(isUnion(schema))return`one of: ${schema.options.map(o=>{let inner=isWrappable(o)?o.unwrap():o;return describeType(inner)}).join(` | `)}`;if(schema.type===`literal`){let val=schema.value;return JSON.stringify(val??`?`)}return schema.type===`record`?`object (record)`:isZodString(schema)?schema.format===`url`?`URL string`:schema.format===`email`?`email string`:schema.format===`uuid`?`UUID string`:schema.minLength===null?`string`:`string (min ${schema.minLength} characters)`:type},getObjectShapeKeys=schema=>{let inner=isWrappable(schema)?schema.unwrap():schema;if(isObject(inner))return Object.keys(inner.shape)},handleInvalidType=(issue,fieldSchema)=>{let received=`received`in issue?String(issue.received):`unknown`;if(isWrappable(fieldSchema)){let inner=fieldSchema.unwrap();return`expected ${describeType(inner)} but received ${received} — this field has a default value configured, ensure props are merged correctly before validation`}if(isObject(fieldSchema)||isWrappable(fieldSchema)&&isObject(fieldSchema.unwrap())){let keys=getObjectShapeKeys(fieldSchema);return`expected object${keys?` with properties (${keys.join(`, `)})`:``} but received ${received} — add .default({}) to make this field optional`}return isArray(fieldSchema)?`expected ${describeType(fieldSchema)} but received ${received} — add .default([]) to make this field optional`:issue.message},handleInvalidFormat=(issue,field)=>{let{format:fmt}=issue;return fmt===`url`?`invalid URL format for field '${field}' — expected a valid URL (e.g., https://example.com/image.png)`:fmt===`email`?`invalid email format for field '${field}' — expected a valid email address`:issue.message},handleInvalidValue=issue=>{let{values}=issue;return`expected one of: ${values?.join(`, `)??`unknown`}`},handleTooSmall=issue=>{let{minimum:min,origin}=issue;if(min!==null){let suffix=``;return origin===`string`?suffix=`character(s)`:origin===`array`&&(suffix=`item(s)`),`must be at least ${min} ${suffix}`}return issue.message},enhanceMessage=(issue,fieldSchema)=>{let field=issue.path.join(`.`);if(!fieldSchema)return issue.message;switch(issue.code){case`invalid_type`:return handleInvalidType(issue,fieldSchema);case`invalid_format`:return handleInvalidFormat(issue,field);case`invalid_value`:return handleInvalidValue(issue);case`too_small`:return handleTooSmall(issue);case`too_big`:return issue.message;case`unrecognized_keys`:{let{keys}=issue;return`unexpected field '${keys?.join(`, `)??field}' — not recognized by the schema, check for typos or remove it`}case`invalid_union`:{let inner=isWrappable(fieldSchema)?fieldSchema.unwrap():fieldSchema;return isUnion(inner)?`did not match any valid type — expected ${describeType(inner)}`:issue.message}default:return issue.message}},getSuggestion=(issue,fieldSchema)=>{if(fieldSchema){if(issue.code===`invalid_type`){if(isObject(fieldSchema))return`add .default({}) to make this object optional`;if(isArray(fieldSchema))return`add .default([]) to make this array optional`;if(isWrappable(fieldSchema))return`ensure props are merged correctly before validation`}if(issue.code===`invalid_value`){let{values}=issue;if(values)return`use one of: ${values.join(`, `)}`}if(issue.code===`invalid_format`){let{format:fmt}=issue;if(fmt===`url`)return`provide a valid URL starting with http:// or https://`;if(fmt===`email`)return`provide a valid email address`}}},validateProps=(schema,props)=>{let result=schema.safeParse(props);if(result.success)return result.data;throw new PropValidationError(result.error.issues.map(issue=>{let path=issue.path,fieldSchema=resolveFieldSchema(schema,path);return{code:String(issue.code),expected:`expected`in issue?String(issue.expected):`unknown`,field:path.join(`.`),message:enhanceMessage(issue,fieldSchema),received:`received`in issue?String(issue.received):`unknown`,suggestion:getSuggestion(issue,fieldSchema)}}))},isZodObject=schema=>typeof schema!=`object`||!schema?!1:schema.type===`object`,zodToJsonSchemaShape=schema=>typeof schema!=`object`||!schema||!(`shape`in schema)?{}:z.toJSONSchema(schema);export{zodToJsonSchemaShape as a,validateProps as i,defineSchema as n,isZodObject as r,PropValidationError as t};