@stdout-design/core 0.4.0 → 0.5.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/index.d.mts +36 -76
- package/dist/index.mjs +2 -2
- package/dist/og/index.d.mts +4 -10
- package/dist/og/index.mjs +1 -1
- package/dist/renderer-DLY7ffyj.mjs +1 -0
- package/dist/schema.d.mts +1 -1
- package/dist/schema.mjs +1 -1
- package/dist/{validation-MigzRuji.d.mts → validation-2z23pVmR.d.mts} +0 -2
- package/package.json +17 -14
- package/dist/renderer-B2yMpMcX.mjs +0 -1
- package/dist/takumi-types-shim-DCQcozlV.d.mts +0 -101
package/dist/index.d.mts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { a as validateProps, i as isZodObject, n as PropValidationError, o as zodToJsonSchemaShape, t as PropSchema } from "./validation-
|
|
1
|
+
import { a as validateProps, i as isZodObject, n as PropValidationError, o as zodToJsonSchemaShape, t as PropSchema } from "./validation-2z23pVmR.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { fromJsx } from "takumi-js/helpers/jsx";
|
|
4
4
|
import { prepareImages } from "@takumi-rs/helpers";
|
|
5
5
|
import { ComponentType } from "react";
|
|
6
6
|
import { EmojiType, extractEmojis } from "@takumi-rs/helpers/emoji";
|
|
7
7
|
import { Node } from "takumi-js";
|
|
8
|
-
//#region src/assets/image-resolver.d.ts
|
|
9
8
|
interface ImagePolicy {
|
|
10
9
|
/** Return true to permit fetching a URL. Unset denies every URL -- see the module doc comment. */
|
|
11
10
|
allowUrl?: (url: string) => boolean;
|
|
@@ -23,8 +22,6 @@ interface ImagePolicy {
|
|
|
23
22
|
*/
|
|
24
23
|
emoji?: EmojiType | "from-font";
|
|
25
24
|
}
|
|
26
|
-
//#endregion
|
|
27
|
-
//#region src/shared/types.d.ts
|
|
28
25
|
interface FontConfig {
|
|
29
26
|
family: string;
|
|
30
27
|
weights?: number[];
|
|
@@ -62,9 +59,7 @@ interface TemplateModule {
|
|
|
62
59
|
default: (props: Record<string, unknown>) => unknown;
|
|
63
60
|
propsSchema: z.ZodObject<Record<string, z.ZodTypeAny>>;
|
|
64
61
|
}
|
|
65
|
-
|
|
66
|
-
//#region src/shared/config-schema.d.ts
|
|
67
|
-
declare const studioConfigSchema: z.ZodObject<{
|
|
62
|
+
export declare const studioConfigSchema: z.ZodObject<{
|
|
68
63
|
defaultPreset: z.ZodOptional<z.ZodString>;
|
|
69
64
|
fonts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
|
|
70
65
|
family: z.ZodString;
|
|
@@ -96,21 +91,17 @@ declare const studioConfigSchema: z.ZodObject<{
|
|
|
96
91
|
description: z.ZodOptional<z.ZodString>;
|
|
97
92
|
}, z.core.$strip>>;
|
|
98
93
|
}, z.core.$strip>;
|
|
99
|
-
declare const parseStudioConfig: (raw: unknown) => {
|
|
94
|
+
export declare const parseStudioConfig: (raw: unknown) => {
|
|
100
95
|
config: StudioConfig;
|
|
101
96
|
} | {
|
|
102
97
|
issues: string;
|
|
103
98
|
};
|
|
104
|
-
//#endregion
|
|
105
|
-
//#region src/engine/render.d.ts
|
|
106
99
|
interface CompiledTemplate {
|
|
107
100
|
node: Node;
|
|
108
101
|
stylesheets: string[];
|
|
109
102
|
}
|
|
110
|
-
declare const compileTemplate: (element: Parameters<typeof fromJsx>[0], options?: Parameters<typeof fromJsx>[1]) => Promise<CompiledTemplate>;
|
|
111
|
-
|
|
112
|
-
//#region src/shared/error-codes.d.ts
|
|
113
|
-
declare const ErrorCode: {
|
|
103
|
+
export declare const compileTemplate: (element: Parameters<typeof fromJsx>[0], options?: Parameters<typeof fromJsx>[1]) => Promise<CompiledTemplate>;
|
|
104
|
+
export declare const ErrorCode: {
|
|
114
105
|
readonly API_ERROR: "API_ERROR";
|
|
115
106
|
readonly CACHE_CLEANUP_FAILED: "CACHE_CLEANUP_FAILED";
|
|
116
107
|
readonly CACHE_CORRUPT_ENTRY: "CACHE_CORRUPT_ENTRY";
|
|
@@ -130,21 +121,17 @@ declare const ErrorCode: {
|
|
|
130
121
|
readonly TEMPLATE_NOT_FOUND: "TEMPLATE_NOT_FOUND";
|
|
131
122
|
readonly UI_RENDER_ERROR: "UI_RENDER_ERROR";
|
|
132
123
|
};
|
|
133
|
-
type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
134
|
-
declare class AppError extends Error {
|
|
124
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
125
|
+
export declare class AppError extends Error {
|
|
135
126
|
readonly code: ErrorCode;
|
|
136
127
|
readonly context: Record<string, unknown>;
|
|
137
128
|
constructor(code: ErrorCode, message: string, context?: Record<string, unknown>);
|
|
138
129
|
}
|
|
139
|
-
//#endregion
|
|
140
|
-
//#region src/shared/logger.d.ts
|
|
141
130
|
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
142
|
-
declare const logDebug: (message: string, context?: Record<string, unknown>) => void;
|
|
143
|
-
declare const logInfo: (message: string, context?: Record<string, unknown>) => void;
|
|
144
|
-
declare const logWarn: (message: string, context?: Record<string, unknown>) => void;
|
|
145
|
-
declare const logError: (code: ErrorCode, message: string, context?: Record<string, unknown>) => void;
|
|
146
|
-
//#endregion
|
|
147
|
-
//#region src/cache/cache-keys.d.ts
|
|
131
|
+
export declare const logDebug: (message: string, context?: Record<string, unknown>) => void;
|
|
132
|
+
export declare const logInfo: (message: string, context?: Record<string, unknown>) => void;
|
|
133
|
+
export declare const logWarn: (message: string, context?: Record<string, unknown>) => void;
|
|
134
|
+
export declare const logError: (code: ErrorCode, message: string, context?: Record<string, unknown>) => void;
|
|
148
135
|
/**
|
|
149
136
|
* Cache key derivation, shared by every component that needs to name a
|
|
150
137
|
* cache entry consistently. Pulled out of the old monolithic RenderCache
|
|
@@ -166,8 +153,6 @@ interface PixelCacheKeyInput {
|
|
|
166
153
|
}
|
|
167
154
|
declare const createCompileCacheKey: (input: CompileCacheKeyInput) => string;
|
|
168
155
|
declare const createPixelCacheKey: (input: PixelCacheKeyInput) => string;
|
|
169
|
-
//#endregion
|
|
170
|
-
//#region src/cache/render-cache.d.ts
|
|
171
156
|
interface CacheStats {
|
|
172
157
|
compiled: {
|
|
173
158
|
entries: number;
|
|
@@ -219,7 +204,7 @@ interface SweepOutcome {
|
|
|
219
204
|
* file. The full sweep now also scans the directory for files with no
|
|
220
205
|
* metadata row and removes them.
|
|
221
206
|
*/
|
|
222
|
-
declare class RenderCache {
|
|
207
|
+
export declare class RenderCache {
|
|
223
208
|
private readonly cacheDir;
|
|
224
209
|
private readonly maxSizeBytes;
|
|
225
210
|
private readonly compiled;
|
|
@@ -273,8 +258,6 @@ declare class RenderCache {
|
|
|
273
258
|
private requireFsStore;
|
|
274
259
|
private requireEviction;
|
|
275
260
|
}
|
|
276
|
-
//#endregion
|
|
277
|
-
//#region src/engine/takumi-types-shim.d.ts
|
|
278
261
|
/**
|
|
279
262
|
* Type-only shim for `@takumi-rs/core` (as re-exported by `takumi-js/node`).
|
|
280
263
|
*
|
|
@@ -284,14 +267,13 @@ declare class RenderCache {
|
|
|
284
267
|
* `moduleResolution: "nodenext"`. This file re-declares the v2 surface we
|
|
285
268
|
* actually use, backed by the real runtime export.
|
|
286
269
|
*
|
|
287
|
-
* TODO
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
* `registerFont`
|
|
293
|
-
*
|
|
294
|
-
* upstream repo: https://github.com/kane50613/takumi
|
|
270
|
+
* TODO: the v1 packaging bug this shim worked around is fixed as of
|
|
271
|
+
* takumi-js 2.17.1 -- a throwaway file importing `Renderer` from
|
|
272
|
+
* "takumi-js/node" resolves `render`/`measure`/`registerFont` cleanly.
|
|
273
|
+
* Because of the `as unknown` cast below, these hand-written types can
|
|
274
|
+
* silently drift from the real ones (2.17 inserted a `key` parameter into
|
|
275
|
+
* `registerFont`), so prefer deleting this file and importing from
|
|
276
|
+
* `takumi-js/node` directly. Upstream: https://github.com/kane50613/takumi
|
|
295
277
|
*
|
|
296
278
|
* v2 breaking changes reflected below (see /docs/upgrade/v2):
|
|
297
279
|
* - `new Renderer()` -> `new Renderer(options?)`. The constructor takes an
|
|
@@ -364,6 +346,7 @@ interface RenderOptions {
|
|
|
364
346
|
fontFamilies?: string[];
|
|
365
347
|
/** BCP-47 language tag for locale-aware shaping. Inherited by children. */
|
|
366
348
|
lang?: string;
|
|
349
|
+
/** CSS stylesheet text. Sent to Takumi as `css` (its `stylesheets` field is deprecated since 2.17). */
|
|
367
350
|
stylesheets?: string[];
|
|
368
351
|
devicePixelRatio?: number;
|
|
369
352
|
timeMs?: number;
|
|
@@ -383,14 +366,12 @@ interface MeasuredNode {
|
|
|
383
366
|
children: MeasuredNode[];
|
|
384
367
|
runs: MeasuredTextRun[];
|
|
385
368
|
}
|
|
386
|
-
//#endregion
|
|
387
|
-
//#region src/engine/renderer.d.ts
|
|
388
369
|
/**
|
|
389
370
|
* Preload a font once and reuse it across many renders without re-passing
|
|
390
371
|
* its bytes on every call. Optional -- per Takumi's v2 guidance, passing
|
|
391
372
|
* `fonts` directly in a render/measure call's options covers most cases.
|
|
392
373
|
*/
|
|
393
|
-
declare const registerFont: (font: Font, signal?: AbortSignal) => Promise<string[]>;
|
|
374
|
+
export declare const registerFont: (font: Font, signal?: AbortSignal) => Promise<string[]>;
|
|
394
375
|
interface RenderOutput {
|
|
395
376
|
bytes: Buffer;
|
|
396
377
|
width: number;
|
|
@@ -411,11 +392,11 @@ interface RenderOutput {
|
|
|
411
392
|
* via `options` -- there is no construction-time renderer config to fall
|
|
412
393
|
* back on in v2.
|
|
413
394
|
*/
|
|
414
|
-
declare const renderToPixels: (template: CompiledTemplate, dimensions: {
|
|
395
|
+
export declare const renderToPixels: (template: CompiledTemplate, dimensions: {
|
|
415
396
|
width: number;
|
|
416
397
|
height: number;
|
|
417
398
|
}, options?: Omit<RenderOptions, "width" | "height">, signal?: AbortSignal) => Promise<RenderOutput>;
|
|
418
|
-
declare const measureTemplate: (template: CompiledTemplate, options?: RenderOptions, signal?: AbortSignal) => Promise<MeasuredNode>;
|
|
399
|
+
export declare const measureTemplate: (template: CompiledTemplate, options?: RenderOptions, signal?: AbortSignal) => Promise<MeasuredNode>;
|
|
419
400
|
/**
|
|
420
401
|
* Convenience: measure to discover natural dimensions, then render at
|
|
421
402
|
* those dimensions in one call. This is the "auto-size" path -- it costs
|
|
@@ -423,25 +404,19 @@ declare const measureTemplate: (template: CompiledTemplate, options?: RenderOpti
|
|
|
423
404
|
* so prefer explicit width/height (e.g. from a chosen output preset) when
|
|
424
405
|
* you have them.
|
|
425
406
|
*/
|
|
426
|
-
declare const renderAutoSized: (template: CompiledTemplate, options?: RenderOptions, signal?: AbortSignal) => Promise<RenderOutput>;
|
|
427
|
-
//#endregion
|
|
428
|
-
//#region src/project/project-paths.d.ts
|
|
407
|
+
export declare const renderAutoSized: (template: CompiledTemplate, options?: RenderOptions, signal?: AbortSignal) => Promise<RenderOutput>;
|
|
429
408
|
interface ProjectPaths {
|
|
430
409
|
configPath: string;
|
|
431
410
|
defaultCacheDir: string;
|
|
432
411
|
localesDir: string;
|
|
433
412
|
templatesDir: string;
|
|
434
413
|
}
|
|
435
|
-
declare const resolveProjectPaths: (rootDir: string) => ProjectPaths;
|
|
436
|
-
//#endregion
|
|
437
|
-
//#region src/cache/open-cache.d.ts
|
|
414
|
+
export declare const resolveProjectPaths: (rootDir: string) => ProjectPaths;
|
|
438
415
|
interface OpenCacheOptions {
|
|
439
416
|
cacheDir?: string;
|
|
440
417
|
maxSizeMB?: number;
|
|
441
418
|
}
|
|
442
|
-
declare const openCache: (rootDir: string, options?: OpenCacheOptions) => Promise<RenderCache>;
|
|
443
|
-
//#endregion
|
|
444
|
-
//#region src/assets/assets-resolver.d.ts
|
|
419
|
+
export declare const openCache: (rootDir: string, options?: OpenCacheOptions) => Promise<RenderCache>;
|
|
445
420
|
interface AssetsForLocale {
|
|
446
421
|
fonts: Font[];
|
|
447
422
|
fontFamilies?: string[];
|
|
@@ -459,21 +434,15 @@ interface AssetsResolverOptions {
|
|
|
459
434
|
preResolvedFonts?: Font[];
|
|
460
435
|
preResolvedFontFamilies?: string[];
|
|
461
436
|
}
|
|
462
|
-
declare const clearFontCache: () => void;
|
|
463
|
-
declare const resolveAssetsForLocale: (localeId: string, props: Record<string, unknown>, options?: AssetsResolverOptions) => Promise<AssetsForLocale>;
|
|
464
|
-
|
|
465
|
-
//#region src/assets/font-config.d.ts
|
|
466
|
-
declare const createFetchFontsFromConfig: (fontsConfig?: Record<string, FontConfig[]> | undefined, configDir?: string) => ((localeId: string) => Promise<FontResolutionResult | null>) | undefined;
|
|
467
|
-
//#endregion
|
|
468
|
-
//#region src/batch/loaders.d.ts
|
|
437
|
+
export declare const clearFontCache: () => void;
|
|
438
|
+
export declare const resolveAssetsForLocale: (localeId: string, props: Record<string, unknown>, options?: AssetsResolverOptions) => Promise<AssetsForLocale>;
|
|
439
|
+
export declare const createFetchFontsFromConfig: (fontsConfig?: Record<string, FontConfig[]> | undefined, configDir?: string) => ((localeId: string) => Promise<FontResolutionResult | null>) | undefined;
|
|
469
440
|
interface LoadedTemplate {
|
|
470
441
|
module: TemplateModule;
|
|
471
442
|
contentHash: string;
|
|
472
443
|
}
|
|
473
|
-
declare const importTemplateForBatch: (rootDir: string, componentPath: string, templateId: string) => Promise<LoadedTemplate>;
|
|
474
|
-
declare const loadConfig: (rootDir: string) => Promise<StudioConfig>;
|
|
475
|
-
//#endregion
|
|
476
|
-
//#region src/batch/types.d.ts
|
|
444
|
+
export declare const importTemplateForBatch: (rootDir: string, componentPath: string, templateId: string) => Promise<LoadedTemplate>;
|
|
445
|
+
export declare const loadConfig: (rootDir: string) => Promise<StudioConfig>;
|
|
477
446
|
interface ManifestEntry {
|
|
478
447
|
rowIndex: number;
|
|
479
448
|
locale: string;
|
|
@@ -493,8 +462,6 @@ interface Manifest {
|
|
|
493
462
|
error: string;
|
|
494
463
|
}[];
|
|
495
464
|
}
|
|
496
|
-
//#endregion
|
|
497
|
-
//#region src/orchestrate/single.d.ts
|
|
498
465
|
interface SingleInput {
|
|
499
466
|
component: ComponentType<Record<string, unknown>>;
|
|
500
467
|
propsSchema: PropSchema;
|
|
@@ -518,9 +485,7 @@ interface SingleInput {
|
|
|
518
485
|
interface SingleOutput {
|
|
519
486
|
outputPath: string;
|
|
520
487
|
}
|
|
521
|
-
declare const renderComponent: (input: SingleInput) => Promise<SingleOutput>;
|
|
522
|
-
//#endregion
|
|
523
|
-
//#region src/orchestrate/types.d.ts
|
|
488
|
+
export declare const renderComponent: (input: SingleInput) => Promise<SingleOutput>;
|
|
524
489
|
type OutputFormat = "webp" | "png" | "jpeg" | "ico" | "raw";
|
|
525
490
|
interface OrchestrateRenderInput {
|
|
526
491
|
component?: ComponentType<Record<string, unknown>>;
|
|
@@ -575,12 +540,8 @@ interface OrchestrateMeasureResult {
|
|
|
575
540
|
height: number;
|
|
576
541
|
durationMs: number;
|
|
577
542
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
declare const orchestrateRender: (input: OrchestrateRenderInput) => Promise<OrchestrateRenderResult>;
|
|
581
|
-
declare const orchestrateMeasure: (input: OrchestrateMeasureInput) => Promise<OrchestrateMeasureResult>;
|
|
582
|
-
//#endregion
|
|
583
|
-
//#region src/batch/batch.d.ts
|
|
543
|
+
export declare const orchestrateRender: (input: OrchestrateRenderInput) => Promise<OrchestrateRenderResult>;
|
|
544
|
+
export declare const orchestrateMeasure: (input: OrchestrateMeasureInput) => Promise<OrchestrateMeasureResult>;
|
|
584
545
|
interface BatchInput {
|
|
585
546
|
rootDir: string;
|
|
586
547
|
templateId: string;
|
|
@@ -598,6 +559,5 @@ interface BatchOutput {
|
|
|
598
559
|
manifest: Manifest;
|
|
599
560
|
elapsedMs: number;
|
|
600
561
|
}
|
|
601
|
-
declare const runBatch: (input: BatchInput) => Promise<BatchOutput>;
|
|
602
|
-
|
|
603
|
-
export { AppError, type AssetsForLocale, type AssetsResolverOptions, type BatchInput, type BatchOutput, type CacheStats, type CompiledTemplate, ErrorCode, type ErrorCode as ErrorCodeType, type FontConfig, type FontResolutionResult, type LogLevel, type Manifest, type ManifestEntry, type OpenCacheOptions, type OrchestrateMeasureInput, type OrchestrateMeasureResult, type OrchestrateRenderInput, type OrchestrateRenderResult, PropValidationError, RenderCache, type RenderCacheOptions, type RenderOutput, type SingleInput, type SingleOutput, type StudioConfig, type TemplateModule, clearFontCache, compileTemplate, createFetchFontsFromConfig, importTemplateForBatch, isZodObject, loadConfig, logDebug, logError, logInfo, logWarn, measureTemplate, openCache, orchestrateMeasure, orchestrateRender, parseStudioConfig, registerFont, renderAutoSized, renderComponent, renderToPixels, resolveAssetsForLocale, resolveProjectPaths, runBatch, studioConfigSchema, validateProps, zodToJsonSchemaShape };
|
|
562
|
+
export declare const runBatch: (input: BatchInput) => Promise<BatchOutput>;
|
|
563
|
+
export { type AssetsForLocale, type AssetsResolverOptions, type BatchInput, type BatchOutput, type CacheStats, type CompiledTemplate, type ErrorCode as ErrorCodeType, type FontConfig, type FontResolutionResult, type LogLevel, type Manifest, type ManifestEntry, type OpenCacheOptions, type OrchestrateMeasureInput, type OrchestrateMeasureResult, type OrchestrateRenderInput, type OrchestrateRenderResult, PropValidationError, type RenderCacheOptions, type RenderOutput, type SingleInput, type SingleOutput, type StudioConfig, type TemplateModule, isZodObject, validateProps, zodToJsonSchemaShape };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{a as logDebug,c as logWarn,i as renderToPixels,l as compileTemplate,n as registerFont,o as logError,r as renderAutoSized,s as logInfo,t as measureTemplate}from"./renderer-B2yMpMcX.mjs";import{a as zodToJsonSchemaShape,i as validateProps,r as isZodObject,t as PropValidationError}from"./validation-BzP0M2-C.mjs";import{z}from"zod";import{existsSync,unlinkSync}from"node:fs";import{mkdir,readFile,readdir,stat,unlink,writeFile}from"node:fs/promises";import path from"node:path";import{createHash}from"node:crypto";import pLimit from"p-limit";import lockfile from"proper-lockfile";import realWriteFileAtomic from"write-file-atomic";import{Kysely,SqliteDialect,sql}from"kysely";import{googleFonts,prepareImages}from"@takumi-rs/helpers";import{pathToFileURL}from"node:url";import{createElement}from"react";import{extractEmojis}from"@takumi-rs/helpers/emoji";const fontConfigSchema=z.object({family:z.string().min(1),path:z.string().optional(),source:z.enum([`google`,`local`]).optional(),weights:z.array(z.number().int().positive()).optional()}),templateEntrySchema=z.object({componentPath:z.string().min(1,`componentPath is required`),description:z.string().optional()}),presetSchema=z.object({height:z.number().int().positive(),id:z.string().min(1),platform:z.string().min(1),width:z.number().int().positive()}),imagePolicySchema=z.object({allowUrl:z.custom(val=>typeof val==`function`).optional(),emoji:z.custom(val=>typeof val==`string`).optional(),fetch:z.custom(val=>typeof val==`function`).optional(),maxBytes:z.number().int().positive().optional(),timeout:z.number().int().optional()}),studioConfigSchema=z.object({defaultPreset:z.string().optional(),fonts:z.record(z.string(),z.array(fontConfigSchema)).optional(),images:imagePolicySchema.optional(),locales:z.array(z.string()).optional(),outDir:z.string().optional(),presets:z.array(presetSchema),scaffoldVersion:z.string().optional(),templates:z.record(z.string(),templateEntrySchema)}),parseStudioConfig=raw=>{let parsed=studioConfigSchema.safeParse(raw);return parsed.success?{config:parsed.data}:{issues:parsed.error.issues.map(issue=>`${issue.path.join(`.`)||`(root)`}: ${issue.message}`).join(`; `)}},ErrorCode={API_ERROR:`API_ERROR`,CACHE_CLEANUP_FAILED:`CACHE_CLEANUP_FAILED`,CACHE_CORRUPT_ENTRY:`CACHE_CORRUPT_ENTRY`,CACHE_INIT_FAILED:`CACHE_INIT_FAILED`,CACHE_WRITE_FAILED:`CACHE_WRITE_FAILED`,CONFIG_INVALID:`CONFIG_INVALID`,CONFIG_NOT_FOUND:`CONFIG_NOT_FOUND`,FONT_REGISTRATION_FAILED:`FONT_REGISTRATION_FAILED`,INTERNAL_ERROR:`INTERNAL_ERROR`,LOCALE_LOAD_FAILED:`LOCALE_LOAD_FAILED`,PROP_VALIDATION_FAILED:`PROP_VALIDATION_FAILED`,RENDERER_NOT_INITIALIZED:`RENDERER_NOT_INITIALIZED`,RENDER_FAILED:`RENDER_FAILED`,SSE_ERROR:`SSE_ERROR`,TEMPLATE_INVALID_EXPORT:`TEMPLATE_INVALID_EXPORT`,TEMPLATE_LOAD_FAILED:`TEMPLATE_LOAD_FAILED`,TEMPLATE_NOT_FOUND:`TEMPLATE_NOT_FOUND`,UI_RENDER_ERROR:`UI_RENDER_ERROR`};var AppError=class extends Error{code;context;constructor(code,message,context){super(message),this.name=`AppError`,this.code=code,this.context=context??{}}};const createCompileCacheKey=input=>createHash(`sha256`).update(`${input.templateId}\u0000${input.templateContentHash}\u0000${input.propsJSON}`).digest(`hex`).slice(0,32),createPixelCacheKey=input=>createHash(`sha256`).update(`${input.templateContentHash}\u0000${input.propsJSON}\u0000${input.width}\u0000${input.height}\u0000${input.format??`png`}`).digest(`hex`).slice(0,32);var CompileCache=class CompileCache{maxEntries;nodeMap=new Map;head;tail;contentHashIndex=new Map;keyToContentHash=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=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 logDebug,c as logWarn,i as renderToPixels,l as compileTemplate,n as registerFont,o as logError,r as renderAutoSized,s as logInfo,t as measureTemplate}from"./renderer-DLY7ffyj.mjs";import{a as zodToJsonSchemaShape,i as validateProps,r as isZodObject,t as PropValidationError}from"./validation-BzP0M2-C.mjs";import{z}from"zod";import{existsSync,unlinkSync}from"node:fs";import{mkdir,readFile,readdir,stat,unlink,writeFile}from"node:fs/promises";import path from"node:path";import{createHash}from"node:crypto";import pLimit from"p-limit";import lockfile from"proper-lockfile";import realWriteFileAtomic from"write-file-atomic";import{Kysely,SqliteDialect,sql}from"kysely";import{googleFonts,prepareImages}from"@takumi-rs/helpers";import{pathToFileURL}from"node:url";import{createElement}from"react";import{extractEmojis}from"@takumi-rs/helpers/emoji";const fontConfigSchema=z.object({family:z.string().min(1),path:z.string().optional(),source:z.enum([`google`,`local`]).optional(),weights:z.array(z.number().int().positive()).optional()}),templateEntrySchema=z.object({componentPath:z.string().min(1,`componentPath is required`),description:z.string().optional()}),presetSchema=z.object({height:z.number().int().positive(),id:z.string().min(1),platform:z.string().min(1),width:z.number().int().positive()}),imagePolicySchema=z.object({allowUrl:z.custom(val=>typeof val==`function`).optional(),emoji:z.custom(val=>typeof val==`string`).optional(),fetch:z.custom(val=>typeof val==`function`).optional(),maxBytes:z.number().int().positive().optional(),timeout:z.number().int().optional()}),studioConfigSchema=z.object({defaultPreset:z.string().optional(),fonts:z.record(z.string(),z.array(fontConfigSchema)).optional(),images:imagePolicySchema.optional(),locales:z.array(z.string()).optional(),outDir:z.string().optional(),presets:z.array(presetSchema),scaffoldVersion:z.string().optional(),templates:z.record(z.string(),templateEntrySchema)}),parseStudioConfig=raw=>{let parsed=studioConfigSchema.safeParse(raw);return parsed.success?{config:parsed.data}:{issues:parsed.error.issues.map(issue=>`${issue.path.join(`.`)||`(root)`}: ${issue.message}`).join(`; `)}},ErrorCode={API_ERROR:`API_ERROR`,CACHE_CLEANUP_FAILED:`CACHE_CLEANUP_FAILED`,CACHE_CORRUPT_ENTRY:`CACHE_CORRUPT_ENTRY`,CACHE_INIT_FAILED:`CACHE_INIT_FAILED`,CACHE_WRITE_FAILED:`CACHE_WRITE_FAILED`,CONFIG_INVALID:`CONFIG_INVALID`,CONFIG_NOT_FOUND:`CONFIG_NOT_FOUND`,FONT_REGISTRATION_FAILED:`FONT_REGISTRATION_FAILED`,INTERNAL_ERROR:`INTERNAL_ERROR`,LOCALE_LOAD_FAILED:`LOCALE_LOAD_FAILED`,PROP_VALIDATION_FAILED:`PROP_VALIDATION_FAILED`,RENDERER_NOT_INITIALIZED:`RENDERER_NOT_INITIALIZED`,RENDER_FAILED:`RENDER_FAILED`,SSE_ERROR:`SSE_ERROR`,TEMPLATE_INVALID_EXPORT:`TEMPLATE_INVALID_EXPORT`,TEMPLATE_LOAD_FAILED:`TEMPLATE_LOAD_FAILED`,TEMPLATE_NOT_FOUND:`TEMPLATE_NOT_FOUND`,UI_RENDER_ERROR:`UI_RENDER_ERROR`};var AppError=class extends Error{code;context;constructor(code,message,context){super(message),this.name=`AppError`,this.code=code,this.context=context??{}}};const createCompileCacheKey=input=>createHash(`sha256`).update(`${input.templateId}\u0000${input.templateContentHash}\u0000${input.propsJSON}`).digest(`hex`).slice(0,32),createPixelCacheKey=input=>createHash(`sha256`).update(`${input.templateContentHash}\u0000${input.propsJSON}\u0000${input.width}\u0000${input.height}\u0000${input.format??`png`}`).digest(`hex`).slice(0,32);var CompileCache=class CompileCache{maxEntries;nodeMap=/* @__PURE__ */ new Map;head;tail;contentHashIndex=/* @__PURE__ */ new Map;keyToContentHash=/* @__PURE__ */ new Map;constructor(maxEntries=100){this.maxEntries=maxEntries,this.head={key:``,next:null,prev:null,value:void 0},this.tail={key:``,next:null,prev:null,value:void 0},this.head.next=this.tail,this.tail.prev=this.head}get size(){return this.nodeMap.size}get(key){let node=this.nodeMap.get(key);return node?(this.moveToTail(node),node.value):null}set(key,value){let existing=this.nodeMap.get(key);if(existing){existing.value=value,this.moveToTail(existing);return}let node={key,next:null,prev:null,value};this.nodeMap.set(key,node),this.insertBeforeTail(node),this.nodeMap.size>this.maxEntries&&this.evictLRU()}setWithContentHash(key,contentHash,value){let previousHash=this.keyToContentHash.get(key);if(previousHash!==void 0&&previousHash!==contentHash){let oldSet=this.contentHashIndex.get(previousHash);oldSet&&(oldSet.delete(key),oldSet.size===0&&this.contentHashIndex.delete(previousHash))}this.set(key,value);let set=this.contentHashIndex.get(contentHash);set||(set=/* @__PURE__ */ new Set,this.contentHashIndex.set(contentHash,set)),set.add(key),this.keyToContentHash.set(key,contentHash)}invalidateByContentHash(contentHash){let keys=this.contentHashIndex.get(contentHash);if(keys){for(let key of keys)this.removeNode(key);this.contentHashIndex.delete(contentHash)}}clear(){this.nodeMap.clear(),this.contentHashIndex.clear(),this.keyToContentHash.clear(),this.head.next=this.tail,this.tail.prev=this.head}insertBeforeTail(node){let prev=this.tail.prev;node.prev=prev,node.next=this.tail,prev.next=node,this.tail.prev=node}static removeFromList(node){node.prev.next=node.next,node.next.prev=node.prev,node.prev=null,node.next=null}moveToTail(node){CompileCache.removeFromList(node),this.insertBeforeTail(node)}evictLRU(){let lru=this.head.next;lru&&lru!==this.tail&&this.removeNode(lru.key)}removeNode(key){let node=this.nodeMap.get(key);if(!node)return;CompileCache.removeFromList(node),this.nodeMap.delete(key);let contentHash=this.keyToContentHash.get(key);if(contentHash!==void 0){this.keyToContentHash.delete(key);let set=this.contentHashIndex.get(contentHash);set&&(set.delete(key),set.size===0&&this.contentHashIndex.delete(contentHash))}}},ConcurrencyLimiter=class{limit;constructor(maxConcurrent){this.limit=maxConcurrent&&maxConcurrent>0?pLimit(maxConcurrent):null}run(fn){return this.limit?this.limit(fn):fn()}get activeCount(){return this.limit?.activeCount??0}get pendingCount(){return this.limit?.pendingCount??0}get isBounded(){return this.limit!==null}},EvictionCoordinator=class{lockTargetPath;maxSizeBytes;debounceMs;getCurrentTotalBytes;sweepFn;debounceTimer=null;constructor(options){this.lockTargetPath=options.lockTargetPath,this.maxSizeBytes=options.maxSizeBytes,this.debounceMs=options.debounceMs??250,this.getCurrentTotalBytes=options.getCurrentTotalBytes,this.sweepFn=options.sweep}requestEviction(){this.debounceTimer||(this.debounceTimer=setTimeout(async()=>{this.debounceTimer=null;try{await this.runIfOverBudget()}catch(error){logWarn(`EvictionCoordinator: debounced sweep failed`,{error:String(error)})}},this.debounceMs),this.debounceTimer.unref?.())}async runIfOverBudget(){return await this.getCurrentTotalBytes()<=this.maxSizeBytes?{freedBytes:0}:this.runSweep({full:!1})}runFullSweep(){return this.runSweep({full:!0})}async runSweep(options){let release=null;try{release=await lockfile.lock(this.lockTargetPath,{retries:0,stale:1e4})}catch(error){return logDebug(`EvictionCoordinator: lock held elsewhere, skipping this pass`,{error:String(error)}),{freedBytes:0}}try{return await this.sweepFn(options)}finally{await release()}}cancelPending(){this.debounceTimer&&=(clearTimeout(this.debounceTimer),null)}};const isFsNotFoundError=error=>error?.code===`ENOENT`,defaultFsPrimitives={existsSync,readFile,stat:filePath=>stat(filePath),unlink,writeFileAtomic:(filePath,data)=>realWriteFileAtomic(filePath,data)};var FsStore=class{limiter;fs;constructor(limiter,fs=defaultFsPrimitives){this.limiter=limiter,this.fs=fs}computeHash(bytes){let bunGlobal=globalThis.Bun;return bunGlobal?`bun1:${bunGlobal.hash(bytes).toString(16)}`:`sha256:${createHash(`sha256`).update(bytes).digest(`hex`)}`}hashesMatch(stored,computed){let[storedPrefix]=stored.split(`:`),[computedPrefix]=computed.split(`:`);return storedPrefix===computedPrefix?stored===computed:(logDebug(`FsStore: cross-runtime hash algorithm mismatch, treating as cache miss`,{computed:computedPrefix,stored:storedPrefix}),!1)}async writeAtomic(filePath,bytes){let doWrite=()=>this.fs.writeFileAtomic(filePath,bytes);await(this.limiter?this.limiter.run(doWrite):doWrite())}readVerified(filePath,expectedSizeBytes,expectedContentHash){let doRead=()=>this.readVerifiedUnbounded(filePath,expectedSizeBytes,expectedContentHash);return this.limiter?this.limiter.run(doRead):doRead()}async readVerifiedUnbounded(filePath,expectedSizeBytes,expectedContentHash){if(!this.fs.existsSync(filePath))return logDebug(`FsStore: cached file missing on read`,{filePath}),null;let fileStat;try{fileStat=await this.fs.stat(filePath)}catch(error){if(isFsNotFoundError(error))return logDebug(`FsStore: file disappeared before stat (TOCTOU)`,{filePath}),null;throw error}if(fileStat.size!==expectedSizeBytes)return logWarn(`FsStore: size mismatch on read, treating as corrupt`,{expected:expectedSizeBytes,filePath,onDisk:fileStat.size}),null;let bytes;try{bytes=await this.fs.readFile(filePath)}catch(error){if(isFsNotFoundError(error))return logDebug(`FsStore: file disappeared before readFile (TOCTOU)`,{filePath}),null;throw error}let actualHash=this.computeHash(bytes);return this.hashesMatch(expectedContentHash,actualHash)?bytes:(expectedContentHash.split(`:`)[0]===actualHash.split(`:`)[0]&&logWarn(`FsStore: content-hash mismatch on read, treating as corrupt`,{expected:expectedContentHash,filePath}),null)}async delete(filePath,knownSizeBytes){if(!this.fs.existsSync(filePath))return 0;let freed=knownSizeBytes??0;try{knownSizeBytes===void 0&&(freed=(await this.fs.stat(filePath)).size),await this.fs.unlink(filePath)}catch(error){return logWarn(`FsStore: failed to delete cache entry file`,{error:String(error),filePath}),freed}return freed}};const BUSY_TIMEOUT_MS=5e3,toRow=r=>({contentHash:r.content_hash,createdAt:r.created_at,fileName:r.file_name,hash:r.hash,height:r.height,lastAccessedAt:r.last_accessed_at,sizeBytes:r.size_bytes,width:r.width});var MetadataStore=class{cacheDir;dbPath;closeDb;db;initialized=!1;initPromise;constructor(cacheDir){this.cacheDir=cacheDir,this.dbPath=cacheDir===`:memory:`?`:memory:`:path.join(cacheDir,`cache.sqlite`)}async init(){if(!this.initialized){if(this.initPromise)return this.initPromise;this.initPromise=this.#doInit();try{await this.initPromise}finally{this.initPromise=void 0,this.closeDb&&(this.initialized=!0)}}}async#doInit(){this.cacheDir!==`:memory:`&&!existsSync(this.cacheDir)&&await mkdir(this.cacheDir,{recursive:!0});let isBun=!1;try{await import(`bun:sqlite`),isBun=!0}catch{isBun=!1}await(isBun?this.initBun():this.initNode())}async initBun(){let Database,BunSqliteDialect;try{({Database}=await import(`bun:sqlite`)),{BunSqliteDialect}=await import(`kysely-bun-sqlite`)}catch(error){throw Error(`Failed to load Bun SQLite modules. Ensure 'bun:sqlite' and 'kysely-bun-sqlite' are available.`,{cause:error})}let rawDb=this.createAndVerifyDb(()=>new Database(this.dbPath,{create:!0,readwrite:!0}),(db,statement)=>db.run(statement),db=>db.query(`PRAGMA integrity_check`).get()?.integrity_check??null);this.closeDb=()=>{try{rawDb.run(`PRAGMA wal_checkpoint(TRUNCATE)`)}catch{}rawDb.close()},this.db=new Kysely({dialect:new BunSqliteDialect({database:rawDb})}),await this.createSchema()}async initNode(){let BetterSqlite3;try{BetterSqlite3=(await import(`better-sqlite3`)).default}catch(error){throw Error("Failed to load better-sqlite3. This is required when running under Node.js. Install it as a dependency: `npm install better-sqlite3`.",{cause:error})}let rawDb=this.createAndVerifyDb(()=>new BetterSqlite3(this.dbPath),(db,statement)=>db.exec(statement),db=>db.prepare(`PRAGMA integrity_check`).get()?.integrity_check??null);this.closeDb=()=>{try{rawDb.exec(`PRAGMA wal_checkpoint(TRUNCATE)`)}catch{}rawDb.close()},this.db=new Kysely({dialect:new SqliteDialect({database:rawDb})}),await this.createSchema()}createAndVerifyDb(createFn,runFn,checkFn){let rawDb=createFn(),dbHealthy=!0;try{runFn(rawDb,`PRAGMA journal_mode = WAL`),runFn(rawDb,`PRAGMA synchronous = NORMAL`),runFn(rawDb,`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`),runFn(rawDb,`PRAGMA wal_checkpoint(TRUNCATE)`);let result=checkFn(rawDb);result!==`ok`&&(dbHealthy=!1,logWarn(`SQLite integrity check failed, recreating cache database`,{result:result??`null`}))}catch{dbHealthy=!1,logWarn(`SQLite setup or integrity check threw, recreating cache database`)}if(dbHealthy)return rawDb;rawDb.close(),this.deleteDatabaseFiles();let freshDb=createFn();return runFn(freshDb,`PRAGMA journal_mode = WAL`),runFn(freshDb,`PRAGMA synchronous = NORMAL`),runFn(freshDb,`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`),freshDb}async createSchema(){let db=this.requireDb();await db.schema.createTable(`pixel_cache_entries`).ifNotExists().addColumn(`hash`,`text`,c=>c.primaryKey()).addColumn(`file_name`,`text`,c=>c.notNull()).addColumn(`created_at`,`integer`,c=>c.notNull()).addColumn(`last_accessed_at`,`integer`,c=>c.notNull()).addColumn(`size_bytes`,`integer`,c=>c.notNull()).addColumn(`width`,`integer`,c=>c.notNull()).addColumn(`height`,`integer`,c=>c.notNull()).addColumn(`content_hash`,`text`,c=>c.notNull()).execute(),await db.schema.createIndex(`idx_last_accessed`).ifNotExists().on(`pixel_cache_entries`).column(`last_accessed_at`).execute(),await db.schema.createTable(`cache_totals`).ifNotExists().addColumn(`id`,`integer`,c=>c.primaryKey()).addColumn(`total_size_bytes`,`integer`,c=>c.notNull().defaultTo(0)).addColumn(`entry_count`,`integer`,c=>c.notNull().defaultTo(0)).execute(),await sql`INSERT OR IGNORE INTO cache_totals (id, total_size_bytes, entry_count) VALUES (1, 0, 0)`.execute(db),await sql`
|
|
2
2
|
CREATE TRIGGER IF NOT EXISTS trg_pixel_cache_insert
|
|
3
3
|
AFTER INSERT ON pixel_cache_entries
|
|
4
4
|
BEGIN
|
|
@@ -25,4 +25,4 @@ import{a as logDebug,c as logWarn,i as renderToPixels,l as compileTemplate,n as
|
|
|
25
25
|
SET total_size_bytes = total_size_bytes - OLD.size_bytes + NEW.size_bytes
|
|
26
26
|
WHERE id = 1;
|
|
27
27
|
END;
|
|
28
|
-
`.execute(db)}close(){this.closeDb&&=(this.closeDb(),void 0),this.db=void 0,this.initialized=!1,this.initPromise=void 0}deleteDatabaseFiles(){if(this.dbPath!==`:memory:`)for(let suffix of[``,`-wal`,`-shm`])try{unlinkSync(`${this.dbPath}${suffix}`)}catch{}}async upsertEntry(input){let db=this.requireDb(),now=Date.now();await db.insertInto(`pixel_cache_entries`).values({content_hash:input.contentHash,created_at:now,file_name:input.fileName,hash:input.hash,height:input.height,last_accessed_at:now,size_bytes:input.sizeBytes,width:input.width}).onConflict(oc=>oc.column(`hash`).doUpdateSet({content_hash:input.contentHash,file_name:input.fileName,height:input.height,last_accessed_at:now,size_bytes:input.sizeBytes,width:input.width})).execute()}async getEntry(hash){let row=await this.requireDb().selectFrom(`pixel_cache_entries`).selectAll().where(`hash`,`=`,hash).executeTakeFirst();return row?toRow(row):null}async touchAccess(hash,timestamp=Date.now()){await this.requireDb().updateTable(`pixel_cache_entries`).set({last_accessed_at:timestamp}).where(`hash`,`=`,hash).execute()}async deleteEntry(hash){await this.requireDb().deleteFrom(`pixel_cache_entries`).where(`hash`,`=`,hash).execute()}async deleteAll(){await this.requireDb().deleteFrom(`pixel_cache_entries`).execute()}async listAllOrderedByLastAccessed(){return(await this.requireDb().selectFrom(`pixel_cache_entries`).selectAll().orderBy(`last_accessed_at`,`asc`).execute()).map(toRow)}async getTotals(){let row=await this.requireDb().selectFrom(`cache_totals`).select([`total_size_bytes`,`entry_count`]).where(`id`,`=`,1).executeTakeFirstOrThrow();return{entryCount:row.entry_count,totalSizeBytes:row.total_size_bytes}}requireDb(){if(!this.db)throw Error(`MetadataStore.init() must be called before use.`);return this.db}};const RECOVERY_COOLDOWN_MS=2e3,isCacheEntryFileName=name=>/^[0-9a-f]{32,}\.[a-z]+$/u.test(name);var RenderCache=class{cacheDir;maxSizeBytes;compiled;maxConcurrentWrites;evictionDebounceMs;metadata;fsStore;eviction;initialized=!1;recoveryPromise=null;lastRecoveryAt=0;static createCompileCacheKey=createCompileCacheKey;static createPixelCacheKey=createPixelCacheKey;constructor(options){this.cacheDir=options?.cacheDir??`.studio-cache`,this.maxSizeBytes=(options?.maxSizeMB??500)*1024*1024,this.compiled=new CompileCache(options?.compiledMaxEntries),this.maxConcurrentWrites=options?.maxConcurrentWrites,this.evictionDebounceMs=options?.evictionDebounceMs}async init(){if(this.initialized)return;existsSync(this.cacheDir)||await mkdir(this.cacheDir,{recursive:!0}),this.metadata=new MetadataStore(this.cacheDir),await this.metadata.init();let limiter=new ConcurrencyLimiter(this.maxConcurrentWrites);this.fsStore=new FsStore(limiter),this.eviction=new EvictionCoordinator({debounceMs:this.evictionDebounceMs,getCurrentTotalBytes:async()=>(await this.requireMetadata().getTotals()).totalSizeBytes,lockTargetPath:this.cacheDir,maxSizeBytes:this.maxSizeBytes,sweep:options=>this.performSweep(options)}),this.initialized=!0}close(){this.eviction?.cancelPending(),this.metadata?.close(),this.initialized=!1}getCompiled(key){return this.compiled.get(key)}setCompiled(key,value){this.compiled.set(key,value)}setCompiledWithContentHash(key,contentHash,value){this.compiled.setWithContentHash(key,contentHash,value)}invalidateCompiledByContentHash(contentHash){this.compiled.invalidateByContentHash(contentHash)}clearCompiled(){this.compiled.clear()}async getPixels(key){let metadata=this.requireMetadata(),fsStore=this.requireFsStore(),row;try{row=await metadata.getEntry(key)}catch(error){return logWarn(`Pixel metadata read failed, attempting recovery`,{error:error instanceof Error?error.message:String(error),key}),await this.recoverMetadata(),null}if(!row)return null;let filePath=path.join(this.cacheDir,row.fileName),bytes=await fsStore.readVerified(filePath,row.sizeBytes,row.contentHash);if(bytes===null){try{await metadata.deleteEntry(key)}catch{}return null}try{await metadata.touchAccess(key)}catch(error){logWarn(`Pixel touchAccess failed, attempting recovery`,{error:error instanceof Error?error.message:String(error),key}),await this.recoverMetadata()}return bytes}async setPixels(key,bytes,width,height,format=`png`){let metadata=this.requireMetadata(),fsStore=this.requireFsStore(),fileName=`${key}.${format}`,filePath=path.join(this.cacheDir,fileName),contentHash=fsStore.computeHash(bytes);try{await fsStore.writeAtomic(filePath,bytes)}catch(error){if(error?.code===`ENOENT`)logDebug(`FsStore: ENOENT during writeAtomic, recreating cache directory and retrying`,{filePath}),await mkdir(this.cacheDir,{recursive:!0}),await fsStore.writeAtomic(filePath,bytes);else throw error}try{await metadata.upsertEntry({contentHash,fileName,hash:key,height,sizeBytes:bytes.length,width})}catch(error){let err=error;if(err?.code===`SQLITE_IOERR_VNODE`||(err?.message??``).includes(`disk I/O error`))logWarn(`Metadata write failed (I/O error), recovering and retrying`,{error:String(error),key}),await this.recoverMetadata(),metadata=this.requireMetadata(),await metadata.upsertEntry({contentHash,fileName,hash:key,height,sizeBytes:bytes.length,width});else throw error}return this.requireEviction().requestEviction(),filePath}async stats(){let totals=await this.requireMetadata().getTotals();return{cacheDir:this.cacheDir,compiled:{entries:this.compiled.size},pixels:{entries:totals.entryCount,maxSizeBytes:this.maxSizeBytes,sizeBytes:totals.totalSizeBytes}}}clean(){return this.compiled.clear(),this.requireEviction().runFullSweep()}async performSweep(options){let metadata=this.requireMetadata(),fsStore=this.requireFsStore();if(options.full){let rows=await metadata.listAllOrderedByLastAccessed(),freedAmounts=await Promise.all(rows.map(row=>fsStore.delete(path.join(this.cacheDir,row.fileName),row.sizeBytes)));await metadata.deleteAll();let orphanFreed=await this.deleteOrphanFiles(new Set(rows.map(r=>r.fileName)),fsStore);return{freedBytes:freedAmounts.reduce((sum,n)=>sum+n,0)+orphanFreed}}let totals=await metadata.getTotals();if(totals.totalSizeBytes<=this.maxSizeBytes)return{freedBytes:0};let rows=await metadata.listAllOrderedByLastAccessed(),remaining=totals.totalSizeBytes,rowsToEvict=[];for(let row of rows){if(remaining<=this.maxSizeBytes)break;rowsToEvict.push(row),remaining-=row.sizeBytes}return{freedBytes:(await Promise.all(rowsToEvict.map(async row=>{let freedForRow=await fsStore.delete(path.join(this.cacheDir,row.fileName),row.sizeBytes);return await metadata.deleteEntry(row.hash),freedForRow}))).reduce((sum,n)=>sum+n,0)}}async deleteOrphanFiles(knownFileNames,fsStore){let freed=0;try{let entries=await readdir(this.cacheDir);await Promise.all(entries.map(async entry=>{if(knownFileNames.has(entry)||!isCacheEntryFileName(entry))return;let filePath=path.join(this.cacheDir,entry);logDebug(`FsStore: removing orphaned cache file`,{filePath}),freed+=await fsStore.delete(filePath)}))}catch(error){logWarn(`RenderCache: failed to scan for orphaned files`,{error:String(error)})}return freed}async recoverMetadata(){if(Date.now()-this.lastRecoveryAt<RECOVERY_COOLDOWN_MS){logDebug(`RenderCache: skipping recovery — within cooldown window`,{cooldownMs:RECOVERY_COOLDOWN_MS});return}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},isFontObject=f=>typeof f==`object`&&!(f instanceof Uint8Array),loadLocalFontDescriptor=async(family,filePath,weight,configDir)=>({data:await readFile(configDir?path.resolve(configDir,filePath):path.resolve(filePath)),name:family,weight:weight??400}),isLocalConfig=c=>c.source===`local`&&typeof c.path==`string`&&c.path.length>0,createFetchFontsFromConfig=(fontsConfig,configDir)=>{if(fontsConfig)return async localeId=>{let configs=fontsConfig[localeId];if(!configs||configs.length===0)return null;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&&(googleFontDescriptors=await googleFonts(googleConfigs.map(c=>({name:c.family,weight:c.weights??[400,700]}))));let fonts=[...localFonts,...googleFontDescriptors],fontFamilies=fonts.filter(isFontObject).map(f=>String(f.name??``)).filter(Boolean);return logDebug(`fonts resolved from config`,{count:fonts.length,localeId,names:fontFamilies}),{fontFamilies,fonts}}catch(error){return logDebug(`fonts from config failed`,{error:String(error),localeId}),null}}},isFontDescriptor=f=>typeof f==`object`&&!(f instanceof Uint8Array),defaultFetchFonts=async localeId=>{try{let fonts=await googleFonts([{name:`Noto Sans Arabic`,weight:[400,700]}]),fontFamilies=fonts.filter(isFontDescriptor).map(f=>String(f.name??``)).filter(Boolean);return logDebug(`googleFonts resolved per locale`,{count:fonts.length,localeId,names:fonts.filter(isFontDescriptor).map(f=>f.name)}),{fontFamilies,fonts}}catch(error){return logDebug(`googleFonts failed for locale`,{error:String(error),localeId}),null}},mergeLocaleProps=(props,localeData)=>{if(!localeData)return{...props};let merged={...props};for(let[key,value]of Object.entries(localeData))(Array.isArray(value)&&Array.isArray(merged[key])||typeof value==`string`&&typeof merged[key]==`string`)&&(merged[key]=value);return merged},classifyLocale=localeId=>{let isArabic=localeId.startsWith(`ar`);return{lang:isArabic?`ar`:void 0,needsArabic:isArabic}},fontCache=new Map,clearFontCache=()=>{fontCache.clear()},resolveAssetsForLocale=async(localeId,props,options)=>{let localeData=options?.localeData;if(options?.loadLocaleData)try{localeData=await options.loadLocaleData(localeId)}catch{localeData={}}let mergedProps=mergeLocaleProps(props,localeData),{lang,needsArabic}=classifyLocale(localeId),fonts=[],fontFamilies;if(options?.preResolvedFonts)fonts=options.preResolvedFonts,fontFamilies=options.preResolvedFontFamilies;else if(needsArabic){let fetch=options?.fetchFonts??defaultFetchFonts,cached=fontCache.get(localeId),resolution;if(cached)resolution=await cached;else{let promise=fetch(localeId);fontCache.set(localeId,promise),resolution=await promise,resolution||fontCache.delete(localeId)}resolution&&({fontFamilies,fonts}=resolution)}return{fontFamilies,fonts,lang,props:mergedProps}},templateCache=new Map,loadTemplateForBatch=async(fullPath,templateId)=>{let content;try{content=await readFile(fullPath,`utf-8`)}catch{throw new AppError(ErrorCode.TEMPLATE_LOAD_FAILED,`Template file not found: ${fullPath}`,{componentPath:fullPath,templateId})}let contentHash=createHash(`sha256`).update(content).digest(`hex`).slice(0,16);logDebug(`Loading template`,{path:fullPath,templateId});let mod=await import(pathToFileURL(fullPath).href);if(typeof mod.default!=`function`)throw new AppError(ErrorCode.TEMPLATE_INVALID_EXPORT,`Template "${templateId}" must have a default export that is a component function.`,{templateId});let{propsSchema}=mod;if(!isZodObject(propsSchema))throw new AppError(ErrorCode.TEMPLATE_INVALID_EXPORT,`Template "${templateId}" must export a "propsSchema" that is a z.object({...}).`,{templateId});return{contentHash,module:mod}},importTemplateForBatch=async(rootDir,componentPath,templateId)=>{let fullPath=path.resolve(rootDir,`${componentPath}.tsx`),cached=templateCache.get(fullPath);if(cached)return cached;let promise=loadTemplateForBatch(fullPath,templateId);templateCache.set(fullPath,promise);try{return await promise}catch{throw templateCache.delete(fullPath),await promise.catch(error=>error)}},loadConfig=async rootDir=>{let{configPath}=resolveProjectPaths(rootDir);try{await readFile(configPath,`utf-8`)}catch{throw new AppError(ErrorCode.CONFIG_NOT_FOUND,`No studio.config.ts found in ${rootDir}.`)}logDebug(`Loading studio.config.ts`,{path:configPath});let mod=await import(pathToFileURL(configPath).href),result=parseStudioConfig(mod.default??mod);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`);try{let content=await readFile(localePath,`utf-8`);return{data:JSON.parse(content)[templateId],id:code}}catch{return{id:code}}}))},generateOutputFilename=options=>{let{templateId,locale,presetId,rowKey,format=`png`}=options;return`${templateId}.${locale}.${presetId}.${rowKey}.${format}`};var BoundedImageFetchCache=class{maxEntries;store=new Map;constructor(maxEntries=500){this.maxEntries=maxEntries}get size(){return this.store.size}get(url){let value=this.store.get(url);if(value!==void 0)return this.store.delete(url),this.store.set(url,value),value}set(url,data){if(this.store.delete(url),this.store.set(url,data),this.store.size>this.maxEntries){let oldest=this.store.keys().next().value;oldest!==void 0&&this.store.delete(oldest)}}delete(url){this.store.delete(url)}clear(){this.store.clear()}},ImagePolicyBlockedError=class extends Error{url;constructor(url){super(`Blocked by image fetch policy: ${url}`),this.name=`ImagePolicyBlockedError`,this.url=url}};const denyAll=()=>!1;let sharedFetchCache=null;const getSharedFetchCache=()=>(sharedFetchCache||=new BoundedImageFetchCache,sharedFetchCache),isAllowUrlBlockedError=error=>error instanceof Error&&error.message.startsWith(`URL blocked by allowUrl policy: `),resolveImages=async(node,policy,deps)=>{let prepareImagesFn=deps?.prepareImages??prepareImages,extractEmojisFn=deps?.extractEmojis??extractEmojis,fetchCache=deps?.fetchCache??getSharedFetchCache(),resolvedNode=policy?.emoji&&policy.emoji!==`from-font`?extractEmojisFn(node,policy.emoji):node;try{let images=await prepareImagesFn({allowUrl:policy?.allowUrl??denyAll,fetch:policy?.fetch,fetchCache,maxBytes:policy?.maxBytes,node:resolvedNode,timeout:policy?.timeout});return logDebug(`image-resolver: resolved images`,{count:images.length}),{images,node:resolvedNode}}catch(error){throw isAllowUrlBlockedError(error)?new ImagePolicyBlockedError(error.message.slice(32)):(logWarn(`image-resolver: image fetch failed`,{error:error instanceof Error?error.message:String(error)}),error)}},prepPipeline=async input=>{let{compiledTemplate:preCompiled,component,templateContentHash,templateId,props,propsSchema,locale,loadLocaleData,preResolvedFonts,preResolvedFontFamilies,fetchFonts,images,cache}=input,{fonts:resolvedFonts,fontFamilies:resolvedFontFamilies,lang:resolvedLang,props:merged}=await resolveAssetsForLocale(locale,propsSchema?validateProps(propsSchema,props):{...props},{fetchFonts,loadLocaleData,preResolvedFontFamilies,preResolvedFonts}),propsJSON=JSON.stringify(merged),compileKey=RenderCache.createCompileCacheKey({propsJSON,templateContentHash,templateId}),compiled=preCompiled;if(!compiled){if(!component)throw Error(`Must provide either compiledTemplate or component`);let cachedCompile=cache.getCompiled(compileKey);cachedCompile?compiled=cachedCompile:(compiled=await compileTemplate(createElement(component,merged)),cache.setCompiledWithContentHash(compileKey,templateContentHash,compiled))}let{node:resolvedNode,images:resolvedImages}=await resolveImages(compiled.node,images),renderReady=resolvedNode===compiled.node?compiled:{...compiled,node:resolvedNode};return{compileKey,compiled,mergedProps:merged,propsJSON,renderReady,resolvedFontFamilies,resolvedFonts,resolvedImages,resolvedLang}},orchestrateRender=async input=>{let start=performance.now(),{compiledTemplate,component,templateContentHash,templateId,props,propsSchema,locale:localeId,loadLocaleData,fetchFonts,fonts:preResolvedFonts,fontFamilies:preResolvedFontFamilies,images,width,height,cache,format=`png`,signal}=input,prep=await prepPipeline({cache,compiledTemplate,component,fetchFonts,images,loadLocaleData,locale:localeId??`default`,preResolvedFontFamilies,preResolvedFonts,props,propsSchema,templateContentHash,templateId}),pixelKey=RenderCache.createPixelCacheKey({format,height,propsJSON:prep.propsJSON,templateContentHash,width}),cached=await cache.getPixels(pixelKey);if(cached)return{bytes:cached,cacheHit:!0,durationMs:performance.now()-start,format,height,width};let output=await renderToPixels(prep.renderReady,{height,width},{fontFamilies:(prep.resolvedFontFamilies??[]).length>0?prep.resolvedFontFamilies:void 0,fonts:prep.resolvedFonts.length>0?prep.resolvedFonts:void 0,format,images:prep.resolvedImages.length>0?prep.resolvedImages:void 0,lang:prep.resolvedLang},signal);try{await cache.setPixels(pixelKey,output.bytes,width,height,format)}catch(error){logWarn(`Failed to write to pixel cache`,{error:String(error),pixelKey})}return{bytes:output.bytes,cacheHit:!1,durationMs:performance.now()-start,format,height:output.height,width:output.width}},orchestrateMeasure=async input=>{let start=performance.now(),{compiledTemplate,component,templateContentHash,templateId,props,propsSchema,locale:localeId,loadLocaleData,fetchFonts,images,cache,signal}=input,prep=await prepPipeline({cache,compiledTemplate,component,fetchFonts,images,loadLocaleData,locale:localeId??`default`,preResolvedFontFamilies:void 0,preResolvedFonts:void 0,props,propsSchema,templateContentHash,templateId}),measured=await measureTemplate(prep.renderReady,{fontFamilies:(prep.resolvedFontFamilies??[]).length>0?prep.resolvedFontFamilies:void 0,fonts:prep.resolvedFonts.length>0?prep.resolvedFonts:void 0,images:prep.resolvedImages.length>0?prep.resolvedImages:void 0,lang:prep.resolvedLang},signal);return{durationMs:performance.now()-start,height:measured.height,width:measured.width}},ensureDir=async dir=>{existsSync(dir)||await mkdir(dir,{recursive:!0})},tryWriteFile=async(filePath,data)=>{try{await writeFile(filePath,data)}catch(error){logWarn(`Failed to write output file`,{error:String(error),filePath})}},noop=()=>{},noopCache={getCompiled:()=>null,getPixels:()=>null,setCompiledWithContentHash:noop,setPixels:noop},renderComponent=async input=>{let{component,format=`png`,images,outDir=`./out`,preset,props,propsSchema}=input,result=await orchestrateRender({cache:noopCache,component,format,height:preset.height,images,props,propsSchema,templateContentHash:`single`,templateId:`single`,width:preset.width}),outputPath=`${outDir}/${generateOutputFilename({locale:`default`,presetId:preset.id,rowKey:`single`,templateId:`single`})}`;return await ensureDir(outDir),await writeFile(outputPath,result.bytes),{outputPath}},writeManifest=async(filePath,manifest)=>{await mkdir(path.dirname(filePath),{recursive:!0}),await writeFile(filePath,JSON.stringify(manifest,null,2))},expandMatrix=input=>{let{rows,locales,presets,baseProps={}}=input;if(rows.length===0)return[];let cells=[];for(let[rowIndex,row]of rows.entries()){let rowKey=String(row.key??row.id??row.slug??String(rowIndex)),rowProps={...baseProps,...row};delete rowProps.key,delete rowProps.id,delete rowProps.slug;for(let localeId of locales)for(let preset of presets)cells.push({locale:localeId,preset:{height:preset.height,id:preset.id,width:preset.width},props:{...rowProps,locale:localeId},rowIndex,rowKey})}return cells},runBatch=async input=>{let start=performance.now(),{baseProps={},dataFile,failFast=!1,cacheDir:cacheDirOverride,locales:filterLocales,outDir:outDirOverride,presets:filterPresets,rootDir,rows:inlineRows,templateId}=input,config=await loadConfig(rootDir),fetchFonts=createFetchFontsFromConfig(config.fonts,rootDir),entry=config.templates[templateId];if(!entry)throw Error(`Template not found: ${templateId}`);let{contentHash,module}=await importTemplateForBatch(rootDir,entry.componentPath,templateId),Component=module.default,presets=(filterPresets??config.presets.map(p=>p.id)).map(id=>{let p=config.presets.find(preset=>preset.id===id);if(!p)throw Error(`Preset not found: ${id}`);return{height:p.height,id:p.id,width:p.width}}),resolvedLocales=await resolveLocales(rootDir,filterLocales??config.locales??[],templateId),localeDataMap=new Map(resolvedLocales.map(l=>[l.id,l.data??{}])),loadLocaleData=code=>Promise.resolve(localeDataMap.get(code)??{}),rows=dataFile?await parseDataFile(path.resolve(rootDir,dataFile)):inlineRows??[],cells=expandMatrix({baseProps,locales:resolvedLocales.map(l=>l.id),presets,rows}),outDir=path.resolve(rootDir,outDirOverride??config.outDir??`./out`),cache=await openCache(rootDir,{cacheDir:cacheDirOverride}),succeeded=[],failed=[];try{for(let cell of cells)try{let filename=generateOutputFilename({locale:cell.locale,presetId:cell.preset.id,rowKey:cell.rowKey,templateId}),result=await orchestrateRender({cache,component:Component,fetchFonts,format:`png`,height:cell.preset.height,images:config.images,loadLocaleData,locale:cell.locale,props:cell.props,propsSchema:module.propsSchema,templateContentHash:contentHash,templateId,width:cell.preset.width}),outputPath=`${outDir}/${filename}`;await ensureDir(outDir),await tryWriteFile(outputPath,result.bytes),succeeded.push({cacheHit:result.cacheHit,locale:cell.locale,outputPath,preset:cell.preset.id,rowIndex:cell.rowIndex})}catch(error){if(failed.push({error:error instanceof Error?error.message:String(error),locale:cell.locale,preset:cell.preset.id,rowIndex:cell.rowIndex}),failFast)throw error}}finally{cache.close()}let manifest={completedCount:cells.length,failed,status:failFast&&failed.length>0?`aborted`:`completed`,succeeded,totalCount:cells.length};return await writeManifest(`${outDir}/manifest.json`,manifest),{elapsedMs:performance.now()-start,manifest}};export{AppError,ErrorCode,PropValidationError,RenderCache,clearFontCache,compileTemplate,createFetchFontsFromConfig,importTemplateForBatch,isZodObject,loadConfig,logDebug,logError,logInfo,logWarn,measureTemplate,openCache,orchestrateMeasure,orchestrateRender,parseStudioConfig,registerFont,renderAutoSized,renderComponent,renderToPixels,resolveAssetsForLocale,resolveProjectPaths,runBatch,studioConfigSchema,validateProps,zodToJsonSchemaShape};
|
|
28
|
+
`.execute(db)}close(){this.closeDb&&=(this.closeDb(),void 0),this.db=void 0,this.initialized=!1,this.initPromise=void 0}deleteDatabaseFiles(){if(this.dbPath!==`:memory:`)for(let suffix of[``,`-wal`,`-shm`])try{unlinkSync(`${this.dbPath}${suffix}`)}catch{}}async upsertEntry(input){let db=this.requireDb(),now=Date.now();await db.insertInto(`pixel_cache_entries`).values({content_hash:input.contentHash,created_at:now,file_name:input.fileName,hash:input.hash,height:input.height,last_accessed_at:now,size_bytes:input.sizeBytes,width:input.width}).onConflict(oc=>oc.column(`hash`).doUpdateSet({content_hash:input.contentHash,file_name:input.fileName,height:input.height,last_accessed_at:now,size_bytes:input.sizeBytes,width:input.width})).execute()}async getEntry(hash){let row=await this.requireDb().selectFrom(`pixel_cache_entries`).selectAll().where(`hash`,`=`,hash).executeTakeFirst();return row?toRow(row):null}async touchAccess(hash,timestamp=Date.now()){await this.requireDb().updateTable(`pixel_cache_entries`).set({last_accessed_at:timestamp}).where(`hash`,`=`,hash).execute()}async deleteEntry(hash){await this.requireDb().deleteFrom(`pixel_cache_entries`).where(`hash`,`=`,hash).execute()}async deleteAll(){await this.requireDb().deleteFrom(`pixel_cache_entries`).execute()}async listAllOrderedByLastAccessed(){return(await this.requireDb().selectFrom(`pixel_cache_entries`).selectAll().orderBy(`last_accessed_at`,`asc`).execute()).map(toRow)}async getTotals(){let row=await this.requireDb().selectFrom(`cache_totals`).select([`total_size_bytes`,`entry_count`]).where(`id`,`=`,1).executeTakeFirstOrThrow();return{entryCount:row.entry_count,totalSizeBytes:row.total_size_bytes}}requireDb(){if(!this.db)throw Error(`MetadataStore.init() must be called before use.`);return this.db}};const RECOVERY_COOLDOWN_MS=2e3,isCacheEntryFileName=name=>/^[0-9a-f]{32,}\.[a-z]+$/u.test(name);var RenderCache=class{cacheDir;maxSizeBytes;compiled;maxConcurrentWrites;evictionDebounceMs;metadata;fsStore;eviction;initialized=!1;recoveryPromise=null;lastRecoveryAt=0;static createCompileCacheKey=createCompileCacheKey;static createPixelCacheKey=createPixelCacheKey;constructor(options){this.cacheDir=options?.cacheDir??`.studio-cache`,this.maxSizeBytes=(options?.maxSizeMB??500)*1024*1024,this.compiled=new CompileCache(options?.compiledMaxEntries),this.maxConcurrentWrites=options?.maxConcurrentWrites,this.evictionDebounceMs=options?.evictionDebounceMs}async init(){if(this.initialized)return;existsSync(this.cacheDir)||await mkdir(this.cacheDir,{recursive:!0}),this.metadata=new MetadataStore(this.cacheDir),await this.metadata.init();let limiter=new ConcurrencyLimiter(this.maxConcurrentWrites);this.fsStore=new FsStore(limiter),this.eviction=new EvictionCoordinator({debounceMs:this.evictionDebounceMs,getCurrentTotalBytes:async()=>(await this.requireMetadata().getTotals()).totalSizeBytes,lockTargetPath:this.cacheDir,maxSizeBytes:this.maxSizeBytes,sweep:options=>this.performSweep(options)}),this.initialized=!0}close(){this.eviction?.cancelPending(),this.metadata?.close(),this.initialized=!1}getCompiled(key){return this.compiled.get(key)}setCompiled(key,value){this.compiled.set(key,value)}setCompiledWithContentHash(key,contentHash,value){this.compiled.setWithContentHash(key,contentHash,value)}invalidateCompiledByContentHash(contentHash){this.compiled.invalidateByContentHash(contentHash)}clearCompiled(){this.compiled.clear()}async getPixels(key){let metadata=this.requireMetadata(),fsStore=this.requireFsStore(),row;try{row=await metadata.getEntry(key)}catch(error){return logWarn(`Pixel metadata read failed, attempting recovery`,{error:error instanceof Error?error.message:String(error),key}),await this.recoverMetadata(),null}if(!row)return null;let filePath=path.join(this.cacheDir,row.fileName),bytes=await fsStore.readVerified(filePath,row.sizeBytes,row.contentHash);if(bytes===null){try{await metadata.deleteEntry(key)}catch{}return null}try{await metadata.touchAccess(key)}catch(error){logWarn(`Pixel touchAccess failed, attempting recovery`,{error:error instanceof Error?error.message:String(error),key}),await this.recoverMetadata()}return bytes}async setPixels(key,bytes,width,height,format=`png`){let metadata=this.requireMetadata(),fsStore=this.requireFsStore(),fileName=`${key}.${format}`,filePath=path.join(this.cacheDir,fileName),contentHash=fsStore.computeHash(bytes);try{await fsStore.writeAtomic(filePath,bytes)}catch(error){if(error?.code===`ENOENT`)logDebug(`FsStore: ENOENT during writeAtomic, recreating cache directory and retrying`,{filePath}),await mkdir(this.cacheDir,{recursive:!0}),await fsStore.writeAtomic(filePath,bytes);else throw error}try{await metadata.upsertEntry({contentHash,fileName,hash:key,height,sizeBytes:bytes.length,width})}catch(error){let err=error;if(err?.code===`SQLITE_IOERR_VNODE`||(err?.message??``).includes(`disk I/O error`))logWarn(`Metadata write failed (I/O error), recovering and retrying`,{error:String(error),key}),await this.recoverMetadata(),metadata=this.requireMetadata(),await metadata.upsertEntry({contentHash,fileName,hash:key,height,sizeBytes:bytes.length,width});else throw error}return this.requireEviction().requestEviction(),filePath}async stats(){let totals=await this.requireMetadata().getTotals();return{cacheDir:this.cacheDir,compiled:{entries:this.compiled.size},pixels:{entries:totals.entryCount,maxSizeBytes:this.maxSizeBytes,sizeBytes:totals.totalSizeBytes}}}clean(){return this.compiled.clear(),this.requireEviction().runFullSweep()}async performSweep(options){let metadata=this.requireMetadata(),fsStore=this.requireFsStore();if(options.full){let rows=await metadata.listAllOrderedByLastAccessed(),freedAmounts=await Promise.all(rows.map(row=>fsStore.delete(path.join(this.cacheDir,row.fileName),row.sizeBytes)));await metadata.deleteAll();let orphanFreed=await this.deleteOrphanFiles(new Set(rows.map(r=>r.fileName)),fsStore);return{freedBytes:freedAmounts.reduce((sum,n)=>sum+n,0)+orphanFreed}}let totals=await metadata.getTotals();if(totals.totalSizeBytes<=this.maxSizeBytes)return{freedBytes:0};let rows=await metadata.listAllOrderedByLastAccessed(),remaining=totals.totalSizeBytes,rowsToEvict=[];for(let row of rows){if(remaining<=this.maxSizeBytes)break;rowsToEvict.push(row),remaining-=row.sizeBytes}return{freedBytes:(await Promise.all(rowsToEvict.map(async row=>{let freedForRow=await fsStore.delete(path.join(this.cacheDir,row.fileName),row.sizeBytes);return await metadata.deleteEntry(row.hash),freedForRow}))).reduce((sum,n)=>sum+n,0)}}async deleteOrphanFiles(knownFileNames,fsStore){let freed=0;try{let entries=await readdir(this.cacheDir);await Promise.all(entries.map(async entry=>{if(knownFileNames.has(entry)||!isCacheEntryFileName(entry))return;let filePath=path.join(this.cacheDir,entry);logDebug(`FsStore: removing orphaned cache file`,{filePath}),freed+=await fsStore.delete(filePath)}))}catch(error){logWarn(`RenderCache: failed to scan for orphaned files`,{error:String(error)})}return freed}async recoverMetadata(){if(Date.now()-this.lastRecoveryAt<RECOVERY_COOLDOWN_MS)logDebug(`RenderCache: skipping recovery — within cooldown window`,{cooldownMs:RECOVERY_COOLDOWN_MS});else{if(this.recoveryPromise)return this.recoveryPromise;this.recoveryPromise=this.doRecoverMetadata();try{await this.recoveryPromise}finally{this.recoveryPromise=null}}}async doRecoverMetadata(){this.lastRecoveryAt=Date.now(),this.metadata?.close(),this.metadata=new MetadataStore(this.cacheDir),await this.metadata.init()}requireMetadata(){if(!this.metadata)throw Error(`RenderCache.init() must be called before use.`);return this.metadata}requireFsStore(){if(!this.fsStore)throw Error(`RenderCache.init() must be called before use.`);return this.fsStore}requireEviction(){if(!this.eviction)throw Error(`RenderCache.init() must be called before use.`);return this.eviction}};const resolveProjectPaths=rootDir=>({configPath:path.resolve(rootDir,`studio.config.ts`),defaultCacheDir:path.resolve(rootDir,`.studio-cache`),localesDir:path.resolve(rootDir,`locales`),templatesDir:path.resolve(rootDir,`templates`)}),openCache=async(rootDir,options)=>{let cache=new RenderCache({cacheDir:options?.cacheDir??resolveProjectPaths(rootDir).defaultCacheDir,maxSizeMB:options?.maxSizeMB});return await cache.init(),cache},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,createFetchFontsFromConfig=(fontsConfig,configDir)=>{if(fontsConfig)return async function(localeId){let configs=fontsConfig[localeId];if(!configs||configs.length===0)return null;try{let localConfigs=configs.filter(isLocalConfig),googleConfigs=configs.filter(c=>!isLocalConfig(c)),localFonts=await Promise.all(localConfigs.map(c=>loadLocalFontDescriptor(c.family,c.path,c.weights?.[0],configDir))),googleFontDescriptors=[];if(googleConfigs.length>0){let families=googleConfigs.map(c=>({name:c.family,weight:c.weights??[400,700]}));googleFontDescriptors=await googleFonts(families)}let fonts=[...localFonts,...googleFontDescriptors],fontFamilies=fonts.filter(isFontObject).map(f=>String(f.name??``)).filter(Boolean);return logDebug(`fonts resolved from config`,{count:fonts.length,localeId,names:fontFamilies}),{fontFamilies,fonts}}catch(error){return logDebug(`fonts from config failed`,{error:String(error),localeId}),null}}},isFontDescriptor=f=>typeof f==`object`&&!(f instanceof Uint8Array),defaultFetchFonts=async localeId=>{try{let fonts=await googleFonts([{name:`Noto Sans Arabic`,weight:[400,700]}]),fontFamilies=fonts.filter(isFontDescriptor).map(f=>String(f.name??``)).filter(Boolean);return logDebug(`googleFonts resolved per locale`,{count:fonts.length,localeId,names:fonts.filter(isFontDescriptor).map(f=>f.name)}),{fontFamilies,fonts}}catch(error){return logDebug(`googleFonts failed for locale`,{error:String(error),localeId}),null}},mergeLocaleProps=(props,localeData)=>{if(!localeData)return{...props};let merged={...props};for(let[key,value]of Object.entries(localeData))(Array.isArray(value)&&Array.isArray(merged[key])||typeof value==`string`&&typeof merged[key]==`string`)&&(merged[key]=value);return merged},classifyLocale=localeId=>{let isArabic=localeId.startsWith(`ar`);return{lang:isArabic?`ar`:void 0,needsArabic:isArabic}},fontCache=/* @__PURE__ */ new Map,clearFontCache=()=>{fontCache.clear()},resolveAssetsForLocale=async(localeId,props,options)=>{let localeData=options?.localeData;if(options?.loadLocaleData)try{localeData=await options.loadLocaleData(localeId)}catch{localeData={}}let mergedProps=mergeLocaleProps(props,localeData),{lang,needsArabic}=classifyLocale(localeId),fonts=[],fontFamilies;if(options?.preResolvedFonts)fonts=options.preResolvedFonts,fontFamilies=options.preResolvedFontFamilies;else if(needsArabic){let fetch=options?.fetchFonts??defaultFetchFonts,cached=fontCache.get(localeId),resolution;if(cached)resolution=await cached;else{let promise=fetch(localeId);fontCache.set(localeId,promise),resolution=await promise,resolution||fontCache.delete(localeId)}resolution&&({fontFamilies,fonts}=resolution)}return{fontFamilies,fonts,lang,props:mergedProps}},templateCache=/* @__PURE__ */ new Map,loadTemplateForBatch=async(fullPath,templateId)=>{let content;try{content=await readFile(fullPath,`utf-8`)}catch{throw new AppError(ErrorCode.TEMPLATE_LOAD_FAILED,`Template file not found: ${fullPath}`,{componentPath:fullPath,templateId})}let contentHash=createHash(`sha256`).update(content).digest(`hex`).slice(0,16);logDebug(`Loading template`,{path:fullPath,templateId});let mod=await import(pathToFileURL(fullPath).href);if(typeof mod.default!=`function`)throw new AppError(ErrorCode.TEMPLATE_INVALID_EXPORT,`Template "${templateId}" must have a default export that is a component function.`,{templateId});let{propsSchema}=mod;if(!isZodObject(propsSchema))throw new AppError(ErrorCode.TEMPLATE_INVALID_EXPORT,`Template "${templateId}" must export a "propsSchema" that is a z.object({...}).`,{templateId});return{contentHash,module:mod}},importTemplateForBatch=async(rootDir,componentPath,templateId)=>{let fullPath=path.resolve(rootDir,`${componentPath}.tsx`),cached=templateCache.get(fullPath);if(cached)return cached;let promise=loadTemplateForBatch(fullPath,templateId);templateCache.set(fullPath,promise);try{return await promise}catch{throw templateCache.delete(fullPath),await promise.catch(error=>error)}},loadConfig=async rootDir=>{let{configPath}=resolveProjectPaths(rootDir);try{await readFile(configPath,`utf-8`)}catch{throw new AppError(ErrorCode.CONFIG_NOT_FOUND,`No studio.config.ts found in ${rootDir}.`)}logDebug(`Loading studio.config.ts`,{path:configPath});let mod=await import(pathToFileURL(configPath).href),raw=mod.default??mod,result=parseStudioConfig(raw);if(`issues`in result)throw new AppError(ErrorCode.CONFIG_INVALID,`studio.config.ts is invalid: ${result.issues}`);return result.config},parseDataFile=async filePath=>{let content=await readFile(filePath,`utf-8`);if(filePath.endsWith(`.json`)){let data=JSON.parse(content);if(Array.isArray(data))return data;throw new AppError(ErrorCode.API_ERROR,`JSON data file must contain an array of objects.`)}if(filePath.endsWith(`.csv`)){let{parse}=await import(`csv-parse/sync`);return parse(content,{columns:!0,skip_empty_lines:!0,trim:!0})}throw new AppError(ErrorCode.API_ERROR,`Data file must be .csv or .json.`)},resolveLocales=async(rootDir,localeCodes,templateId)=>{if(localeCodes.length===0)return[{id:`default`}];let{localesDir}=resolveProjectPaths(rootDir);return await Promise.all(localeCodes.map(async code=>{let localePath=path.resolve(localesDir,`${code}.json`);try{let content=await readFile(localePath,`utf-8`);return{data:JSON.parse(content)[templateId],id:code}}catch{return{id:code}}}))},generateOutputFilename=options=>{let{templateId,locale,presetId,rowKey,format=`png`}=options;return`${templateId}.${locale}.${presetId}.${rowKey}.${format}`};var BoundedImageFetchCache=class{maxEntries;store=/* @__PURE__ */ new Map;constructor(maxEntries=500){this.maxEntries=maxEntries}get size(){return this.store.size}get(url){let value=this.store.get(url);if(value!==void 0)return this.store.delete(url),this.store.set(url,value),value}set(url,data){if(this.store.delete(url),this.store.set(url,data),this.store.size>this.maxEntries){let oldest=this.store.keys().next().value;oldest!==void 0&&this.store.delete(oldest)}}delete(url){this.store.delete(url)}clear(){this.store.clear()}},ImagePolicyBlockedError=class extends Error{url;constructor(url){super(`Blocked by image fetch policy: ${url}`),this.name=`ImagePolicyBlockedError`,this.url=url}};const denyAll=()=>!1;let sharedFetchCache=null;const getSharedFetchCache=()=>(sharedFetchCache||=new BoundedImageFetchCache,sharedFetchCache),isAllowUrlBlockedError=error=>error instanceof Error&&error.message.startsWith(`URL blocked by allowUrl policy: `),resolveImages=async(node,policy,deps)=>{let prepareImagesFn=deps?.prepareImages??prepareImages,extractEmojisFn=deps?.extractEmojis??extractEmojis,fetchCache=deps?.fetchCache??getSharedFetchCache(),resolvedNode=policy?.emoji&&policy.emoji!==`from-font`?extractEmojisFn(node,policy.emoji):node;try{let images=await prepareImagesFn({allowUrl:policy?.allowUrl??denyAll,fetch:policy?.fetch,fetchCache,maxBytes:policy?.maxBytes,node:resolvedNode,timeout:policy?.timeout});return logDebug(`image-resolver: resolved images`,{count:images.length}),{images,node:resolvedNode}}catch(error){throw isAllowUrlBlockedError(error)?new ImagePolicyBlockedError(error.message.slice(32)):(logWarn(`image-resolver: image fetch failed`,{error:error instanceof Error?error.message:String(error)}),error)}},prepPipeline=async input=>{let{compiledTemplate:preCompiled,component,templateContentHash,templateId,props,propsSchema,locale,loadLocaleData,preResolvedFonts,preResolvedFontFamilies,fetchFonts,images,cache}=input,validated=propsSchema?validateProps(propsSchema,props):{...props},{fonts:resolvedFonts,fontFamilies:resolvedFontFamilies,lang:resolvedLang,props:merged}=await resolveAssetsForLocale(locale,validated,{fetchFonts,loadLocaleData,preResolvedFontFamilies,preResolvedFonts}),propsJSON=JSON.stringify(merged),compileKey=RenderCache.createCompileCacheKey({propsJSON,templateContentHash,templateId}),compiled=preCompiled;if(!compiled){if(!component)throw Error(`Must provide either compiledTemplate or component`);let cachedCompile=cache.getCompiled(compileKey);if(cachedCompile)compiled=cachedCompile;else{let element=createElement(component,merged);compiled=await compileTemplate(element),cache.setCompiledWithContentHash(compileKey,templateContentHash,compiled)}}let{node:resolvedNode,images:resolvedImages}=await resolveImages(compiled.node,images),renderReady=resolvedNode===compiled.node?compiled:{...compiled,node:resolvedNode};return{compileKey,compiled,mergedProps:merged,propsJSON,renderReady,resolvedFontFamilies,resolvedFonts,resolvedImages,resolvedLang}},orchestrateRender=async input=>{let start=performance.now(),{compiledTemplate,component,templateContentHash,templateId,props,propsSchema,locale:localeId,loadLocaleData,fetchFonts,fonts:preResolvedFonts,fontFamilies:preResolvedFontFamilies,images,width,height,cache,format=`png`,signal}=input,prep=await prepPipeline({cache,compiledTemplate,component,fetchFonts,images,loadLocaleData,locale:localeId??`default`,preResolvedFontFamilies,preResolvedFonts,props,propsSchema,templateContentHash,templateId}),pixelKey=RenderCache.createPixelCacheKey({format,height,propsJSON:prep.propsJSON,templateContentHash,width}),cached=await cache.getPixels(pixelKey);if(cached)return{bytes:cached,cacheHit:!0,durationMs:performance.now()-start,format,height,width};let output=await renderToPixels(prep.renderReady,{height,width},{fontFamilies:(prep.resolvedFontFamilies??[]).length>0?prep.resolvedFontFamilies:void 0,fonts:prep.resolvedFonts.length>0?prep.resolvedFonts:void 0,format,images:prep.resolvedImages.length>0?prep.resolvedImages:void 0,lang:prep.resolvedLang},signal);try{await cache.setPixels(pixelKey,output.bytes,width,height,format)}catch(error){logWarn(`Failed to write to pixel cache`,{error:String(error),pixelKey})}return{bytes:output.bytes,cacheHit:!1,durationMs:performance.now()-start,format,height:output.height,width:output.width}},orchestrateMeasure=async input=>{let start=performance.now(),{compiledTemplate,component,templateContentHash,templateId,props,propsSchema,locale:localeId,loadLocaleData,fetchFonts,images,cache,signal}=input,prep=await prepPipeline({cache,compiledTemplate,component,fetchFonts,images,loadLocaleData,locale:localeId??`default`,preResolvedFontFamilies:void 0,preResolvedFonts:void 0,props,propsSchema,templateContentHash,templateId}),measured=await measureTemplate(prep.renderReady,{fontFamilies:(prep.resolvedFontFamilies??[]).length>0?prep.resolvedFontFamilies:void 0,fonts:prep.resolvedFonts.length>0?prep.resolvedFonts:void 0,images:prep.resolvedImages.length>0?prep.resolvedImages:void 0,lang:prep.resolvedLang},signal);return{durationMs:performance.now()-start,height:measured.height,width:measured.width}},ensureDir=async dir=>{existsSync(dir)||await mkdir(dir,{recursive:!0})},tryWriteFile=async(filePath,data)=>{try{await writeFile(filePath,data)}catch(error){logWarn(`Failed to write output file`,{error:String(error),filePath})}},noop=()=>{},noopCache={getCompiled:()=>null,getPixels:()=>null,setCompiledWithContentHash:noop,setPixels:noop},renderComponent=async input=>{let{component,format=`png`,images,outDir=`./out`,preset,props,propsSchema}=input,result=await orchestrateRender({cache:noopCache,component,format,height:preset.height,images,props,propsSchema,templateContentHash:`single`,templateId:`single`,width:preset.width}),outputPath=`${outDir}/${generateOutputFilename({locale:`default`,presetId:preset.id,rowKey:`single`,templateId:`single`})}`;return await ensureDir(outDir),await writeFile(outputPath,result.bytes),{outputPath}},writeManifest=async(filePath,manifest)=>{await mkdir(path.dirname(filePath),{recursive:!0}),await writeFile(filePath,JSON.stringify(manifest,null,2))},expandMatrix=input=>{let{rows,locales,presets,baseProps={}}=input;if(rows.length===0)return[];let cells=[];for(let[rowIndex,row]of rows.entries()){let rowKey=String(row.key??row.id??row.slug??String(rowIndex)),rowProps={...baseProps,...row};delete rowProps.key,delete rowProps.id,delete rowProps.slug;for(let localeId of locales)for(let preset of presets)cells.push({locale:localeId,preset:{height:preset.height,id:preset.id,width:preset.width},props:{...rowProps,locale:localeId},rowIndex,rowKey})}return cells},runBatch=async input=>{let start=performance.now(),{baseProps={},dataFile,failFast=!1,cacheDir:cacheDirOverride,locales:filterLocales,outDir:outDirOverride,presets:filterPresets,rootDir,rows:inlineRows,templateId}=input,config=await loadConfig(rootDir),fetchFonts=createFetchFontsFromConfig(config.fonts,rootDir),entry=config.templates[templateId];if(!entry)throw Error(`Template not found: ${templateId}`);let{contentHash,module}=await importTemplateForBatch(rootDir,entry.componentPath,templateId),Component=module.default,presets=(filterPresets??config.presets.map(p=>p.id)).map(id=>{let p=config.presets.find(preset=>preset.id===id);if(!p)throw Error(`Preset not found: ${id}`);return{height:p.height,id:p.id,width:p.width}}),localeCodes=filterLocales??config.locales??[],resolvedLocales=await resolveLocales(rootDir,localeCodes,templateId),localeDataMap=new Map(resolvedLocales.map(l=>[l.id,l.data??{}])),loadLocaleData=code=>Promise.resolve(localeDataMap.get(code)??{}),rows=dataFile?await parseDataFile(path.resolve(rootDir,dataFile)):inlineRows??[],cells=expandMatrix({baseProps,locales:resolvedLocales.map(l=>l.id),presets,rows}),outDir=path.resolve(rootDir,outDirOverride??config.outDir??`./out`),cache=await openCache(rootDir,{cacheDir:cacheDirOverride}),succeeded=[],failed=[];try{for(let cell of cells)try{let filename=generateOutputFilename({locale:cell.locale,presetId:cell.preset.id,rowKey:cell.rowKey,templateId}),result=await orchestrateRender({cache,component:Component,fetchFonts,format:`png`,height:cell.preset.height,images:config.images,loadLocaleData,locale:cell.locale,props:cell.props,propsSchema:module.propsSchema,templateContentHash:contentHash,templateId,width:cell.preset.width}),outputPath=`${outDir}/${filename}`;await ensureDir(outDir),await tryWriteFile(outputPath,result.bytes),succeeded.push({cacheHit:result.cacheHit,locale:cell.locale,outputPath,preset:cell.preset.id,rowIndex:cell.rowIndex})}catch(error){if(failed.push({error:error instanceof Error?error.message:String(error),locale:cell.locale,preset:cell.preset.id,rowIndex:cell.rowIndex}),failFast)throw error}}finally{cache.close()}let manifest={completedCount:cells.length,failed,status:failFast&&failed.length>0?`aborted`:`completed`,succeeded,totalCount:cells.length};return await writeManifest(`${outDir}/manifest.json`,manifest),{elapsedMs:performance.now()-start,manifest}};export{AppError,ErrorCode,PropValidationError,RenderCache,clearFontCache,compileTemplate,createFetchFontsFromConfig,importTemplateForBatch,isZodObject,loadConfig,logDebug,logError,logInfo,logWarn,measureTemplate,openCache,orchestrateMeasure,orchestrateRender,parseStudioConfig,registerFont,renderAutoSized,renderComponent,renderToPixels,resolveAssetsForLocale,resolveProjectPaths,runBatch,studioConfigSchema,validateProps,zodToJsonSchemaShape};
|
package/dist/og/index.d.mts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { ReactElement } from "react";
|
|
2
|
-
//#region src/og/template.d.ts
|
|
3
2
|
interface OgTemplateProps {
|
|
4
3
|
title: string;
|
|
5
4
|
description?: string;
|
|
6
5
|
siteName?: string;
|
|
7
6
|
}
|
|
8
|
-
declare const StandardOgTemplate: ({ title, description, siteName }: OgTemplateProps) => import("react").JSX.Element;
|
|
9
|
-
//#endregion
|
|
10
|
-
//#region src/og/response.d.ts
|
|
7
|
+
export declare const StandardOgTemplate: ({ title, description, siteName }: OgTemplateProps) => import("react").JSX.Element;
|
|
11
8
|
interface OgResponseOptions {
|
|
12
9
|
locale?: string;
|
|
13
10
|
width?: number;
|
|
@@ -21,15 +18,12 @@ interface OgResponseOptions {
|
|
|
21
18
|
stylesheets?: string[];
|
|
22
19
|
devicePixelRatio?: number;
|
|
23
20
|
}
|
|
24
|
-
declare const createOgResponse: (element: ReactElement, options?: OgResponseOptions) => Promise<Response>;
|
|
25
|
-
//#endregion
|
|
26
|
-
//#region src/og/pipeline-response.d.ts
|
|
21
|
+
export declare const createOgResponse: (element: ReactElement, options?: OgResponseOptions) => Promise<Response>;
|
|
27
22
|
interface OgPipelineOptions {
|
|
28
23
|
width?: number;
|
|
29
24
|
height?: number;
|
|
30
25
|
format?: "webp" | "png" | "jpeg";
|
|
31
26
|
devicePixelRatio?: number;
|
|
32
27
|
}
|
|
33
|
-
declare const renderOgResponse: (element: ReactElement, options?: OgPipelineOptions) => Promise<Response>;
|
|
34
|
-
|
|
35
|
-
export { type OgPipelineOptions, type OgResponseOptions, type OgTemplateProps, StandardOgTemplate, createOgResponse, renderOgResponse };
|
|
28
|
+
export declare const renderOgResponse: (element: ReactElement, options?: OgPipelineOptions) => Promise<Response>;
|
|
29
|
+
export type { OgPipelineOptions, OgResponseOptions, OgTemplateProps };
|
package/dist/og/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{i as renderToPixels,l as compileTemplate}from"../renderer-
|
|
1
|
+
import{i as renderToPixels,l as compileTemplate}from"../renderer-DLY7ffyj.mjs";import{jsx,jsxs}from"react/jsx-runtime";import{ImageResponse}from"takumi-js/response";const getTitleSize=length=>length>60?`48px`:length>35?`60px`:`76px`,truncateText=(text,maxLength=130)=>text.length<=maxLength?text:`${text.slice(0,maxLength).trimEnd()}…`,StandardOgTemplate=({title,description,siteName})=>{let titleSize=getTitleSize(title.length),truncatedDescription=description?truncateText(description,130):null;return/* @__PURE__ */ jsxs(`div`,{style:{backgroundColor:`#0a0a0b`,color:`#e4e4e7`,display:`flex`,flexDirection:`column`,fontFamily:`system-ui, -apple-system, BlinkMacSystemFont, sans-serif`,height:`100%`,justifyContent:`space-between`,padding:`64px 72px 56px`,position:`relative`,width:`100%`},children:[/* @__PURE__ */ jsx(`div`,{style:{background:`linear-gradient(90deg, #6366f1 0%, #818cf8 100%)`,height:`4px`,left:`0`,position:`absolute`,right:`0`,top:`0`}}),/* @__PURE__ */ jsxs(`div`,{style:{alignItems:`center`,display:`flex`,gap:`10px`},children:[/* @__PURE__ */ jsx(`div`,{style:{backgroundColor:`#6366f1`,borderRadius:`50%`,flexShrink:`0`,height:`10px`,width:`10px`}}),siteName?/* @__PURE__ */ jsx(`span`,{style:{color:`#71717a`,fontSize:`18px`,fontWeight:`500`,letterSpacing:`0.04em`,textTransform:`uppercase`},children:siteName}):null]}),/* @__PURE__ */ jsxs(`div`,{style:{display:`flex`,flex:`1`,flexDirection:`column`,justifyContent:`center`,maxWidth:`860px`,paddingTop:`24px`},children:[/* @__PURE__ */ jsx(`h1`,{style:{color:`#fafafa`,fontSize:titleSize,fontWeight:`700`,letterSpacing:`-0.02em`,lineHeight:`1.1`,margin:`0`},children:title}),truncatedDescription?/* @__PURE__ */ jsx(`p`,{style:{color:`#a1a1aa`,display:`flex`,fontSize:`26px`,fontWeight:`400`,lineHeight:`1.5`,margin:`0`,marginTop:`24px`},children:truncatedDescription}):null]}),/* @__PURE__ */ jsx(`div`,{style:{backgroundColor:`#27272a`,height:`1px`,marginBottom:`0`,width:`100%`}})]})},OG_CACHE_CONTROL=`public, s-maxage=31536000, max-age=3600, immutable`,createOgErrorResponse=error=>(error&&console.error(`[og] render failed`,error),new Response(`Failed to generate OG image`,{headers:{"Cache-Control":`no-store`,"Content-Type":`text/plain; charset=utf-8`},status:500})),createOgResponse=async(element,options)=>{let{locale,format:explicitFormat,height:explicitHeight,width:explicitWidth,devicePixelRatio:explicitDpr,stylesheets,...rest}=options??{},response=new ImageResponse(element,{devicePixelRatio:explicitDpr??2,format:explicitFormat??`webp`,headers:{"Cache-Control":OG_CACHE_CONTROL,Vary:`Accept`},height:explicitHeight??630,...rest,css:stylesheets,lang:locale??rest.lang,onError:error=>{console.error(`[og] render failed`,error)},width:explicitWidth??1200});try{return await response.ready,response}catch(error){return console.error(`[og] returning 500 after render failure`,error),createOgErrorResponse()}},renderOgResponse=async(element,options)=>{let width=options?.width??1200,height=options?.height??630,format=options?.format??`webp`;try{let compiled=await compileTemplate(element),{bytes}=await renderToPixels(compiled,{height,width},{format});return new Response(new Uint8Array(bytes),{headers:{"Cache-Control":OG_CACHE_CONTROL,"Content-Type":`image/${format}`,Vary:`Accept`}})}catch{return createOgErrorResponse()}};export{StandardOgTemplate,createOgResponse,renderOgResponse};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{fromJsx}from"takumi-js/helpers/jsx";import{Renderer}from"takumi-js/node";const compileTemplate=async(element,options)=>{let result=await fromJsx(element,options);return{node:result.node,stylesheets:result.stylesheets}},timestamp=()=>(/* @__PURE__ */ new Date()).toISOString(),stringifyContext=context=>!context||Object.keys(context).length===0?``:` ${JSON.stringify(context)}`,logDebug=(message,context)=>{console.log(`[${timestamp()}] DEBUG ${message}${stringifyContext(context)}`)},logInfo=(message,context)=>{console.log(`[${timestamp()}] INFO ${message}${stringifyContext(context)}`)},logWarn=(message,context)=>{console.warn(`[${timestamp()}] WARN ${message}${stringifyContext(context)}`)},logError=(code,message,context)=>{console.error(`[${timestamp()}] ERROR [${code}] ${message}${stringifyContext(context)}`)},Renderer$1=Renderer;let activeRenderer=null;const __getRendererForTesting=()=>(activeRenderer||=new Renderer$1,activeRenderer),registerFont=async(font,signal)=>(await __getRendererForTesting().registerFont(font,null,signal)).map(family=>family.name),renderToPixels=async(template,dimensions,options,signal)=>{let renderer=__getRendererForTesting();logDebug(`renderer.render start`,{fontFamilies:options?.fontFamilies,fonts:options?.fonts?.filter(f=>typeof f==`object`&&`name`in f).map(f=>({dataType:typeof f.data,name:f.name})),fontsCount:options?.fonts?.length??0,lang:options?.lang});let{stylesheets,...nativeOptions}=options??{};return{bytes:await renderer.render(template.node,{...nativeOptions,css:[...stylesheets??[],...template.stylesheets],height:dimensions.height,width:dimensions.width},signal),format:options?.format??`png`,height:dimensions.height,width:dimensions.width}},measureTemplate=(template,options,signal)=>{let renderer=__getRendererForTesting(),{stylesheets,...nativeOptions}=options??{};return renderer.measure(template.node,{...nativeOptions,css:[...stylesheets??[],...template.stylesheets]},signal)},renderAutoSized=async(template,options,signal)=>{let measured=await measureTemplate(template,options,signal);return renderToPixels(template,{height:Math.ceil(measured.height),width:Math.ceil(measured.width)},options,signal)};export{logDebug as a,logWarn as c,renderToPixels as i,compileTemplate as l,registerFont as n,logError as o,renderAutoSized as r,logInfo as s,measureTemplate as t};
|
package/dist/schema.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { r as defineSchema, t as PropSchema } from "./validation-
|
|
1
|
+
import { r as defineSchema, t as PropSchema } from "./validation-2z23pVmR.mjs";
|
|
2
2
|
export { type PropSchema, defineSchema };
|
package/dist/schema.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
export{n as defineSchema}from"./validation-BzP0M2-C.mjs";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
//#region src/shared/validation.d.ts
|
|
3
2
|
/**
|
|
4
3
|
* Single source of truth for the prop-schema type. Previously declared
|
|
5
4
|
* identically in both `props.ts` and `validation.ts` -- consolidated here
|
|
@@ -35,5 +34,4 @@ declare class PropValidationError extends Error {
|
|
|
35
34
|
declare const validateProps: <T extends PropSchema>(schema: T, props: unknown) => z.infer<T>;
|
|
36
35
|
declare const isZodObject: (schema: unknown) => schema is z.ZodObject<Record<string, z.ZodTypeAny>>;
|
|
37
36
|
declare const zodToJsonSchemaShape: (schema: z.ZodTypeAny) => Record<string, unknown>;
|
|
38
|
-
//#endregion
|
|
39
37
|
export { validateProps as a, isZodObject as i, PropValidationError as n, zodToJsonSchemaShape as o, defineSchema as r, PropSchema as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stdout-design/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Core rendering engine for stdout-design — compile TSX templates to pixel-perfect images via Takumi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"design",
|
|
@@ -44,40 +44,43 @@
|
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsdown",
|
|
46
46
|
"dev": "tsdown --watch",
|
|
47
|
-
"test": "bun test",
|
|
47
|
+
"test": "bun test --isolate",
|
|
48
48
|
"test:integration": "bun test ./src/__tests__/full-pipeline.integration.ts",
|
|
49
49
|
"check-types": "tsc --noEmit",
|
|
50
50
|
"check-types:tsgo": "tsgo --noEmit",
|
|
51
51
|
"lint": "ultracite check"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@takumi-rs/core": "^2.
|
|
55
|
-
"@takumi-rs/helpers": "^2.
|
|
56
|
-
"csv-parse": "^7.0.
|
|
57
|
-
"kysely": "^0.29.
|
|
58
|
-
"p-limit": "^7.3.
|
|
54
|
+
"@takumi-rs/core": "^2.17.1",
|
|
55
|
+
"@takumi-rs/helpers": "^2.17.1",
|
|
56
|
+
"csv-parse": "^7.0.3",
|
|
57
|
+
"kysely": "^0.29.7",
|
|
58
|
+
"p-limit": "^7.3.3",
|
|
59
59
|
"proper-lockfile": "^4.1.2",
|
|
60
|
-
"takumi-js": "^2.
|
|
60
|
+
"takumi-js": "^2.17.1",
|
|
61
61
|
"write-file-atomic": "^8.0.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@sinonjs/fake-timers": "^15.4.0",
|
|
65
65
|
"@stdout-design/config": "workspace:*",
|
|
66
|
-
"@takumi-rs/wasm": "^2.
|
|
67
|
-
"@types/better-sqlite3": "^
|
|
66
|
+
"@takumi-rs/wasm": "^2.17.1",
|
|
67
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
68
68
|
"@types/proper-lockfile": "^4.1.4",
|
|
69
|
-
"@types/react": "^19.
|
|
69
|
+
"@types/react": "^19.3.0",
|
|
70
70
|
"@types/sinonjs__fake-timers": "^15.0.1",
|
|
71
71
|
"@types/write-file-atomic": "^4.0.3",
|
|
72
|
-
"tsdown": "^0.
|
|
73
|
-
"typescript": "
|
|
72
|
+
"tsdown": "^0.23.2",
|
|
73
|
+
"typescript": "7.1.0-beta"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"react": "^19.2.7",
|
|
77
77
|
"zod": "^4.4.3"
|
|
78
78
|
},
|
|
79
79
|
"optionalDependencies": {
|
|
80
|
-
"better-sqlite3": "^
|
|
80
|
+
"better-sqlite3": "^13.0.3",
|
|
81
81
|
"kysely-bun-sqlite": "^0.4.0"
|
|
82
|
+
},
|
|
83
|
+
"engines": {
|
|
84
|
+
"node": ">=22.22"
|
|
82
85
|
}
|
|
83
86
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{fromJsx}from"takumi-js/helpers/jsx";import{Renderer}from"takumi-js/node";const compileTemplate=async(element,options)=>{let result=await fromJsx(element,options);return{node:result.node,stylesheets:result.stylesheets}},timestamp=()=>new Date().toISOString(),stringifyContext=context=>!context||Object.keys(context).length===0?``:` ${JSON.stringify(context)}`,logDebug=(message,context)=>{console.log(`[${timestamp()}] DEBUG ${message}${stringifyContext(context)}`)},logInfo=(message,context)=>{console.log(`[${timestamp()}] INFO ${message}${stringifyContext(context)}`)},logWarn=(message,context)=>{console.warn(`[${timestamp()}] WARN ${message}${stringifyContext(context)}`)},logError=(code,message,context)=>{console.error(`[${timestamp()}] ERROR [${code}] ${message}${stringifyContext(context)}`)},Renderer$1=Renderer;let activeRenderer=null;const __getRendererForTesting=()=>(activeRenderer||=new Renderer$1,activeRenderer),registerFont=(font,signal)=>__getRendererForTesting().registerFont(font,signal),renderToPixels=async(template,dimensions,options,signal)=>{let renderer=__getRendererForTesting();return logDebug(`renderer.render start`,{fontFamilies:options?.fontFamilies,fonts:options?.fonts?.filter(f=>typeof f==`object`&&`name`in f).map(f=>({dataType:typeof f.data,name:f.name})),fontsCount:options?.fonts?.length??0,lang:options?.lang}),{bytes:await renderer.render(template.node,{...options,height:dimensions.height,stylesheets:[...options?.stylesheets??[],...template.stylesheets],width:dimensions.width},signal),format:options?.format??`png`,height:dimensions.height,width:dimensions.width}},measureTemplate=(template,options,signal)=>__getRendererForTesting().measure(template.node,{...options,stylesheets:[...options?.stylesheets??[],...template.stylesheets]},signal),renderAutoSized=async(template,options,signal)=>{let measured=await measureTemplate(template,options,signal);return renderToPixels(template,{height:Math.ceil(measured.height),width:Math.ceil(measured.width)},options,signal)};export{logDebug as a,logWarn as c,renderToPixels as i,compileTemplate as l,registerFont as n,logError as o,renderAutoSized as r,logInfo as s,measureTemplate as t};
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
//#region src/engine/takumi-types-shim.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Type-only shim for `@takumi-rs/core` (as re-exported by `takumi-js/node`).
|
|
4
|
-
*
|
|
5
|
-
* Migrated for Takumi v2. The v1 shim existed to work around a broken
|
|
6
|
-
* `export.d.ts` re-export chain in @takumi-rs/core@1.8.6 that hid
|
|
7
|
-
* `render`/`measure` and several option types from consumers on
|
|
8
|
-
* `moduleResolution: "nodenext"`. This file re-declares the v2 surface we
|
|
9
|
-
* actually use, backed by the real runtime export.
|
|
10
|
-
*
|
|
11
|
-
* TODO(verify-on-upgrade): the v1 packaging bug this shim worked around
|
|
12
|
-
* has not been independently re-verified against the v2 release actually
|
|
13
|
-
* installed. Once upgraded, run the same isolated-probe check described
|
|
14
|
-
* in the v1 version of this file (import the types directly from
|
|
15
|
-
* "takumi-js/node" in a throwaway file and see if `render`/`measure`/
|
|
16
|
-
* `registerFont` resolve). If they do, delete this file and switch all
|
|
17
|
-
* importers back to `takumi-js/node` directly. Tracked against the
|
|
18
|
-
* upstream repo: https://github.com/kane50613/takumi
|
|
19
|
-
*
|
|
20
|
-
* v2 breaking changes reflected below (see /docs/upgrade/v2):
|
|
21
|
-
* - `new Renderer(options)` -> `new Renderer()`. The constructor takes no
|
|
22
|
-
* arguments; fonts and images are now per-render options instead of
|
|
23
|
-
* construction-time state.
|
|
24
|
-
* - `loadFont` / `loadFonts` / `loadFontSync` -> `registerFont`. Use this
|
|
25
|
-
* only to preload a font once and reuse it across many renders; most
|
|
26
|
-
* callers should just pass `fonts` on the render/measure call instead.
|
|
27
|
-
* - The persistent image store and `GlobalContext` are gone.
|
|
28
|
-
* `putPersistentImage` / `clearImageStore` no longer exist. Every image
|
|
29
|
-
* a render needs must be passed through `images`, keyed by `src`.
|
|
30
|
-
* - `fetchedResources` -> `images`. `images` also accepts a group form
|
|
31
|
-
* (`{ sources, fetch, fetchCache }`) that combines pre-fetched entries,
|
|
32
|
-
* a custom fetch implementation, and a shared byte cache.
|
|
33
|
-
* - `format` stays a string, but `quality` (jpeg / lossy webp) and
|
|
34
|
-
* `lossless` (webp) are now separate optional fields instead of one
|
|
35
|
-
* combined quality argument.
|
|
36
|
-
* - `fonts` entries may be bare URL strings, fetched and cached on demand,
|
|
37
|
-
* in addition to loaded font descriptors / raw bytes.
|
|
38
|
-
*/
|
|
39
|
-
interface FontDescriptor {
|
|
40
|
-
name?: string;
|
|
41
|
-
data: Uint8Array | ArrayBuffer | Buffer | (() => Promise<ArrayBuffer | Uint8Array>);
|
|
42
|
-
weight?: number;
|
|
43
|
-
style?: "normal" | "italic" | "oblique" | `oblique ${number}deg` | (string & {});
|
|
44
|
-
}
|
|
45
|
-
/** A font supplied to a render/measure call: a loaded descriptor, raw bytes, or a bare URL fetched on demand. */
|
|
46
|
-
type Font = FontDescriptor | Uint8Array | ArrayBuffer | Buffer | string;
|
|
47
|
-
interface ImageSourceEntry {
|
|
48
|
-
src: string;
|
|
49
|
-
data: Uint8Array | ArrayBuffer;
|
|
50
|
-
}
|
|
51
|
-
/** Shared byte cache across renders -- dedupes concurrent fetches of the same URL. */
|
|
52
|
-
type ImageFetchCache = Map<string, Promise<ArrayBuffer>>;
|
|
53
|
-
type ImageFetchFn = (url: string, signal?: AbortSignal) => Promise<ArrayBuffer>;
|
|
54
|
-
interface ImageResourcesGroup {
|
|
55
|
-
/** Pre-fetched entries, keyed by src. Not re-fetched. */
|
|
56
|
-
sources?: ImageSourceEntry[];
|
|
57
|
-
fetch?: ImageFetchFn;
|
|
58
|
-
timeout?: number;
|
|
59
|
-
fetchCache?: ImageFetchCache;
|
|
60
|
-
}
|
|
61
|
-
/** Either a flat list of pre-fetched images, or the group form with fetch behavior + a shared cache. */
|
|
62
|
-
type ImagesOption = ImageSourceEntry[] | ImageResourcesGroup;
|
|
63
|
-
type OutputFormat = "webp" | "png" | "jpeg" | "ico" | "raw";
|
|
64
|
-
type DitheringAlgorithm = "none" | "ordered-bayer" | "floyd-steinberg";
|
|
65
|
-
interface RenderOptions {
|
|
66
|
-
width?: number;
|
|
67
|
-
height?: number;
|
|
68
|
-
format?: OutputFormat;
|
|
69
|
-
/** JPEG and lossy WebP only. */
|
|
70
|
-
quality?: number;
|
|
71
|
-
/** WebP only (napi binding). Ignored on wasm, where WebP is always lossless. */
|
|
72
|
-
lossless?: boolean;
|
|
73
|
-
drawDebugBorder?: boolean;
|
|
74
|
-
images?: ImagesOption;
|
|
75
|
-
/** Fonts needed for this render. Descriptors, raw bytes, or bare URLs. */
|
|
76
|
-
fonts?: Font[];
|
|
77
|
-
/** Ordered fallback chain of family names. Defaults to every registered family in registration order. */
|
|
78
|
-
fontFamilies?: string[];
|
|
79
|
-
/** BCP-47 language tag for locale-aware shaping. Inherited by children. */
|
|
80
|
-
lang?: string;
|
|
81
|
-
stylesheets?: string[];
|
|
82
|
-
devicePixelRatio?: number;
|
|
83
|
-
timeMs?: number;
|
|
84
|
-
dithering?: DitheringAlgorithm;
|
|
85
|
-
}
|
|
86
|
-
interface MeasuredTextRun {
|
|
87
|
-
text: string;
|
|
88
|
-
x: number;
|
|
89
|
-
y: number;
|
|
90
|
-
width: number;
|
|
91
|
-
height: number;
|
|
92
|
-
}
|
|
93
|
-
interface MeasuredNode {
|
|
94
|
-
width: number;
|
|
95
|
-
height: number;
|
|
96
|
-
transform: [number, number, number, number, number, number];
|
|
97
|
-
children: MeasuredNode[];
|
|
98
|
-
runs: MeasuredTextRun[];
|
|
99
|
-
}
|
|
100
|
-
//#endregion
|
|
101
|
-
export { MeasuredNode as n, RenderOptions as r, Font as t };
|