@stdout-design/core 0.8.0 → 0.9.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.
- package/dist/define-template-BPboUr6A.mjs +1 -0
- package/dist/{define-template-DIghvJHh.d.mts → define-template-DA_FwSV9.d.mts} +21 -3
- package/dist/index.d.mts +45 -2
- package/dist/index.mjs +2 -2
- package/dist/template.d.mts +4 -2
- package/dist/template.mjs +1 -1
- package/package.json +1 -1
- package/dist/define-template-nsnBI5Iz.mjs +0 -1
|
@@ -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`],assertInspector=(inspector,schemaKeys)=>{let seen=/* @__PURE__ */ new Set;for(let[title,keys]of inspector)for(let key of keys){if(!schemaKeys.includes(key))throw new AppError(ErrorCode.TEMPLATE_INVALID_EXPORT,`Template inspector group "${title}" names "${key}", which is not in the schema.`);if(seen.has(key))throw new AppError(ErrorCode.TEMPLATE_INVALID_EXPORT,`Template inspector names "${key}" more than once.`);seen.add(key)}},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 spec.inspector&&assertInspector(spec.inspector,Object.keys(spec.schema)),{[TEMPLATE_BRAND]:!0,fonts:spec.fonts??{},inspector:spec.inspector,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};
|
|
@@ -149,9 +149,25 @@ declare const TEMPLATE_BRAND: unique symbol;
|
|
|
149
149
|
/** Prop names the pipeline owns. A schema that declares one fails to type-check. */
|
|
150
150
|
declare const RESERVED_KEYS: readonly ["locale", "ctx"];
|
|
151
151
|
type ReservedKey = (typeof RESERVED_KEYS)[number];
|
|
152
|
-
|
|
152
|
+
/** One named group of the studio's inspector and the schema keys in it, in order. */
|
|
153
|
+
type InspectorGroup<K extends string = string> = readonly [title: string, keys: readonly K[]];
|
|
154
|
+
/** How the studio lays out a template's fields: groups, then keys inside each. */
|
|
155
|
+
type InspectorLayout<K extends string = string> = readonly InspectorGroup<K>[];
|
|
156
|
+
type FlatKeys<G> = G extends readonly [readonly [string, infer Ks extends readonly string[]], ...infer Rest] ? [...Ks, ...FlatKeys<Rest>] : [];
|
|
157
|
+
type HasDuplicate<T extends readonly string[]> = T extends readonly [infer Head extends string, ...infer Tail extends readonly string[]] ? Head extends Tail[number] ? true : HasDuplicate<Tail> : false;
|
|
158
|
+
/** Resolves to `never` for the `inspector` property when a key appears twice, so the call fails to type-check. */
|
|
159
|
+
type UniqueKeys<G> = HasDuplicate<FlatKeys<G>> extends true ? {
|
|
160
|
+
inspector: never;
|
|
161
|
+
} : unknown;
|
|
162
|
+
interface TemplateSpec<S extends z.ZodRawShape, R extends string, G extends InspectorLayout<keyof S & string> = []> {
|
|
153
163
|
/** User content only, built from `field.*`. Render facts arrive as `ctx`. */
|
|
154
164
|
schema: S & Readonly<Partial<Record<ReservedKey, never>>>;
|
|
165
|
+
/**
|
|
166
|
+
* The studio's field layout: named groups of schema keys, in order. A key
|
|
167
|
+
* the list omits goes to a trailing "Other" group; naming an unknown key,
|
|
168
|
+
* or a key twice, is a type error. Without it the studio groups by kind.
|
|
169
|
+
*/
|
|
170
|
+
inspector?: G;
|
|
155
171
|
/** Font roles; `ctx.fonts[role]` is the CSS stack and the pipeline loads every face. */
|
|
156
172
|
fonts?: FontRoles<R>;
|
|
157
173
|
render: (props: z.output<z.ZodObject<S>>, ctx: RenderContext<R>) => ReactElement;
|
|
@@ -164,6 +180,8 @@ interface TemplateDefinition {
|
|
|
164
180
|
readonly [TEMPLATE_BRAND]: true;
|
|
165
181
|
readonly propsSchema: PropSchema;
|
|
166
182
|
readonly fonts: FontRoles;
|
|
183
|
+
/** Field layout for the studio's inspector; absent means group by kind. */
|
|
184
|
+
readonly inspector?: InspectorLayout;
|
|
167
185
|
/**
|
|
168
186
|
* Built from a component plus `propsSchema` export. Legacy templates
|
|
169
187
|
* mirror themselves from the `locale` prop, so the pipeline sets no `dir`
|
|
@@ -173,8 +191,8 @@ interface TemplateDefinition {
|
|
|
173
191
|
readonly legacy: boolean;
|
|
174
192
|
readonly render: (props: Record<string, unknown>, ctx: RenderContext) => ReactElement;
|
|
175
193
|
}
|
|
176
|
-
declare const defineTemplate: <S extends z.ZodRawShape, R extends string = never>(spec: TemplateSpec<S, R>) => TemplateDefinition;
|
|
194
|
+
declare const defineTemplate: <S extends z.ZodRawShape, R extends string = never, const G extends InspectorLayout<keyof S & string> = []>(spec: TemplateSpec<S, R, G> & UniqueKeys<G>) => TemplateDefinition;
|
|
177
195
|
declare const isTemplateDefinition: (value: unknown) => value is TemplateDefinition;
|
|
178
196
|
/** Reads a loaded template module, in either shape. Throws `TEMPLATE_INVALID_EXPORT` otherwise. */
|
|
179
197
|
declare const toTemplateDefinition: (mod: unknown, templateId: string) => TemplateDefinition;
|
|
180
|
-
export {
|
|
198
|
+
export { TemplateModule as S, FontRole as _, defineTemplate as a, ImagePolicy as b, Canvas as c, Direction as d, LocaleInfo as f, FontFace as g, classifyLocale as h, TemplateSpec as i, RenderContext as l, Script as m, InspectorLayout as n, isTemplateDefinition as o, SCRIPTS as p, TemplateDefinition as r, toTemplateDefinition as s, InspectorGroup as t, TypeSpec as u, FontRoles as v, StudioConfig as x, FontConfig as y };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as TemplateModule, a as defineTemplate, b as ImagePolicy, d as Direction, f as LocaleInfo, h as classifyLocale, m as Script, o as isTemplateDefinition, p as SCRIPTS, r as TemplateDefinition, s as toTemplateDefinition, x as StudioConfig, y as FontConfig } from "./define-template-DA_FwSV9.mjs";
|
|
2
2
|
import { a as validateProps, i as isZodObject, n as PropValidationError, o as zodToJsonSchemaShape, t as PropSchema } from "./validation-2z23pVmR.mjs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { fromJsx } from "takumi-js/helpers/jsx";
|
|
@@ -362,6 +362,43 @@ interface OpenCacheOptions {
|
|
|
362
362
|
maxSizeMB?: number;
|
|
363
363
|
}
|
|
364
364
|
export declare const openCache: (rootDir: string, options?: OpenCacheOptions) => Promise<RenderCache>;
|
|
365
|
+
type Values = Record<string, unknown>;
|
|
366
|
+
/**
|
|
367
|
+
* Translatable text keyed by dotted prop path: `title`, `cta.label`,
|
|
368
|
+
* `stats.1.label`, `tags.0`. The flat form of a locale file's entry for one
|
|
369
|
+
* template, and the form the studio stores per-locale edits in, so the two
|
|
370
|
+
* layers can be listed, compared, reset one field at a time, and written
|
|
371
|
+
* back to a locale file.
|
|
372
|
+
*/
|
|
373
|
+
type TranslationPaths = Record<string, string>;
|
|
374
|
+
/** The layers a locale's props resolve from. Later layers win. */
|
|
375
|
+
interface LocaleLayers {
|
|
376
|
+
/** The locale file's entry for the template (`locales/ar.json` → `{ "launch-hero": … }`). */
|
|
377
|
+
translations?: Values;
|
|
378
|
+
/** Edits made for this locale only, e.g. in the studio. */
|
|
379
|
+
overrides?: TranslationPaths;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Turns flat paths back into the nested shape a locale file has, so they
|
|
383
|
+
* go through the same merge. A list item a path doesn't name is `null`,
|
|
384
|
+
* which the merge treats as "keep the base value".
|
|
385
|
+
*/
|
|
386
|
+
export declare const translationsFromPaths: (paths: TranslationPaths) => Values;
|
|
387
|
+
/**
|
|
388
|
+
* Lists the translatable strings a locale file sets for one template, by
|
|
389
|
+
* path. Only text the schema marks translatable is listed, the same values
|
|
390
|
+
* `applyTranslations` would let the file replace. A schema without
|
|
391
|
+
* `field.*` meta keeps the legacy rule: top-level strings.
|
|
392
|
+
*/
|
|
393
|
+
export declare const flattenTranslations: (schema: z.ZodType | undefined, translations: Values | undefined) => TranslationPaths;
|
|
394
|
+
/**
|
|
395
|
+
* A template's props for one locale: the base values, then the locale
|
|
396
|
+
* file, then this locale's overrides. The file and the overrides only
|
|
397
|
+
* replace translatable text, through the same rule (`applyTranslations`),
|
|
398
|
+
* so colours, numbers, URLs and list lengths always come from the base
|
|
399
|
+
* values. See ADR-0021.
|
|
400
|
+
*/
|
|
401
|
+
export declare const resolveLocaleLayers: (schema: z.ZodType | undefined, base: Values, { translations, overrides }: LocaleLayers) => Values;
|
|
365
402
|
interface AssetsForLocale {
|
|
366
403
|
fonts: Font[];
|
|
367
404
|
fontFamilies?: string[];
|
|
@@ -382,6 +419,8 @@ interface AssetsResolverOptions {
|
|
|
382
419
|
declaredFonts?: readonly FontConfig[];
|
|
383
420
|
/** The template's prop schema; with `field.*` meta, it decides which props a locale file may replace. */
|
|
384
421
|
propsSchema?: z.ZodType;
|
|
422
|
+
/** Edits for this locale only, laid over the locale file (ADR-0021). */
|
|
423
|
+
localeOverrides?: TranslationPaths;
|
|
385
424
|
}
|
|
386
425
|
export declare const resolveAssetsForLocale: (localeId: string, props: Record<string, unknown>, options?: AssetsResolverOptions) => Promise<AssetsForLocale>;
|
|
387
426
|
export declare const clearFontCache: () => void;
|
|
@@ -448,6 +487,8 @@ interface OrchestrateRenderInput {
|
|
|
448
487
|
propsSchema?: PropSchema;
|
|
449
488
|
locale?: string;
|
|
450
489
|
loadLocaleData?: (locale: string) => Promise<Record<string, unknown>>;
|
|
490
|
+
/** Edits for `locale` only, laid over its locale file (ADR-0021). */
|
|
491
|
+
localeOverrides?: TranslationPaths;
|
|
451
492
|
fetchFonts?: (localeId: string) => Promise<FontResolutionResult | null>;
|
|
452
493
|
fonts?: Font[];
|
|
453
494
|
fontFamilies?: string[];
|
|
@@ -484,6 +525,8 @@ interface OrchestrateMeasureInput {
|
|
|
484
525
|
propsSchema?: PropSchema;
|
|
485
526
|
locale?: string;
|
|
486
527
|
loadLocaleData?: (locale: string) => Promise<Record<string, unknown>>;
|
|
528
|
+
/** See `OrchestrateRenderInput.localeOverrides`. */
|
|
529
|
+
localeOverrides?: TranslationPaths;
|
|
487
530
|
fetchFonts?: (localeId: string) => Promise<FontResolutionResult | null>;
|
|
488
531
|
/** See `OrchestrateRenderInput.images`. */
|
|
489
532
|
images?: ImagePolicy;
|
|
@@ -520,4 +563,4 @@ interface BatchOutput {
|
|
|
520
563
|
elapsedMs: number;
|
|
521
564
|
}
|
|
522
565
|
export declare const runBatch: (input: BatchInput) => Promise<BatchOutput>;
|
|
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 };
|
|
566
|
+
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 LocaleLayers, 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, type TranslationPaths, classifyLocale, defineTemplate, isTemplateDefinition, isZodObject, toTemplateDefinition, validateProps, zodToJsonSchemaShape };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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`
|
|
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-BPboUr6A.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 compileTemplate,i as renderToPixels,n as registerFont,r as renderAut
|
|
|
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},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};
|
|
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)},INDEX_SEGMENT=/^\d+$/u,UNSAFE_SEGMENTS=/* @__PURE__ */ new Set([`__proto__`,`constructor`,`prototype`]),childFor=nextSegment=>INDEX_SEGMENT.test(nextSegment)?[]:{},setPath=(root,segments,value)=>{let node=root;for(let[i,segment]of segments.entries()){let slots=node,next=segments[i+1];if(next===void 0){slots[segment]=value;return}let existing=slots[segment];if(existing!==void 0&&typeof existing!=`object`)return;let child=existing??childFor(next);slots[segment]=child,node=child}},fillHoles=value=>Array.isArray(value)?Array.from(value,item=>item===void 0?null:fillHoles(item)):isRecord(value)?Object.fromEntries(Object.entries(value).map(([key,item])=>[key,fillHoles(item)])):value,translationsFromPaths=paths=>{let root={};for(let[path,value]of Object.entries(paths)){let segments=path.split(`.`);segments.every(segment=>segment.length>0&&!UNSAFE_SEGMENTS.has(segment))&&typeof value==`string`&&setPath(root,segments,value)}return fillHoles(root)},join=(prefix,key)=>prefix?`${prefix}.${key}`:String(key),flattenValue=(schema,translation,path,out)=>{let meta=getFieldMeta(schema);if(meta?.kind===`text`){meta.translatable&&typeof translation==`string`&&(out[path]=translation);return}let shape=shapeOf(schema);if(shape){if(isRecord(translation))for(let[key,value]of Object.entries(translation)){let child=shape[key];child&&flattenValue(child,value,join(path,key),out)}return}let element=elementOf(schema);if(element&&Array.isArray(translation))for(let[index,item]of translation.entries())flattenValue(element,item,join(path,index),out)},flattenTranslations=(schema,translations)=>{let out={};if(!translations)return out;let shape=schema?shapeOf(schema):void 0;if(schema&&shape&&hasFieldMeta(schema))return flattenValue(schema,translations,``,out),out;for(let[key,value]of Object.entries(translations))typeof value==`string`&&(!shape||key in shape)&&(out[key]=value);return out},resolveLocaleLayers=(schema,base,{translations,overrides})=>{let withFile=applyTranslations(schema,base,translations);if(!overrides||Object.keys(overrides).length===0)return withFile;let usable=schema&&shapeOf(schema)&&hasFieldMeta(schema)?overrides:Object.fromEntries(Object.entries(overrides).filter(([path])=>!path.includes(`.`)));return applyTranslations(schema,withFile,translationsFromPaths(usable))},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=resolveLocaleLayers(options?.propsSchema,props,{overrides:options?.localeOverrides,translations: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,localeOverrides,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,localeOverrides,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,localeOverrides,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,localeOverrides,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,localeOverrides,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,localeOverrides,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,flattenTranslations,importTemplateForBatch,isTemplateDefinition,isZodObject,loadConfig,logDebug,logError,logInfo,logWarn,measureTemplate,openCache,orchestrateMeasure,orchestrateRender,parseStudioConfig,registerFont,renderAutoSized,renderComponent,renderToPixels,resolveAssetsForLocale,resolveLocaleLayers,resolveProjectPaths,runBatch,studioConfigSchema,toTemplateDefinition,translationsFromPaths,validateProps,zodToJsonSchemaShape};
|
package/dist/template.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as FontRole, a as defineTemplate, c as Canvas, d as Direction, f as LocaleInfo, g as FontFace, i as TemplateSpec, l as RenderContext, m as Script, n as InspectorLayout, r as TemplateDefinition, t as InspectorGroup, u as TypeSpec, v as FontRoles } from "./define-template-DA_FwSV9.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { CSSProperties } from "react";
|
|
4
4
|
/**
|
|
@@ -10,6 +10,8 @@ import { CSSProperties } from "react";
|
|
|
10
10
|
interface Described {
|
|
11
11
|
/** Help text shown next to the field in the studio. */
|
|
12
12
|
description?: string;
|
|
13
|
+
/** Name shown in the studio. Defaults to the key in sentence case ("accentColor" becomes "Accent color"). */
|
|
14
|
+
label?: string;
|
|
13
15
|
}
|
|
14
16
|
declare const text: (defaultValue: string, options?: Described & {
|
|
15
17
|
min?: number;
|
|
@@ -86,4 +88,4 @@ export declare const Sparkle: ({ color, size }: {
|
|
|
86
88
|
color: string;
|
|
87
89
|
size?: number;
|
|
88
90
|
}) => 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 };
|
|
91
|
+
export { type Canvas, type Direction, type FontFace, type FontRole, type FontRoles, type InspectorGroup, type InspectorLayout, type Keyed, type LocaleInfo, type RenderContext, type Script, type TemplateDefinition, type TemplateSpec, type TypeSpec, defineTemplate };
|
package/dist/template.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as defineTemplate}from"./define-template-
|
|
1
|
+
import{t as defineTemplate}from"./define-template-BPboUr6A.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`,label:options.label,translatable:!1}),color:(defaultValue,options={})=>setFieldMeta(describe(z.string().regex(HEX_COLOR,`expected a hex color like #ff6a3d`),options.description).default(defaultValue),{kind:`color`,label:options.label,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`,label:options.label,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`,label:options.label,translatable:!1})},object:(shape,options={})=>setFieldMeta(describe(z.object(shape),options.description).prefault({}),{kind:`object`,label:options.label,translatable:!1}),select:(values,defaultValue,options={})=>setFieldMeta(describe(z.enum(values),options.description).default(defaultValue),{kind:`select`,label:options.label,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`,label:options.label,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};
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
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};
|