@stdout-design/core 0.1.1 → 0.1.2
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 +104 -87
- package/dist/index.mjs +351 -226
- package/dist/schema.d.mts +1 -1
- package/dist/{validation-BUNZnc-5.d.mts → validation-MigzRuji.d.mts} +1 -1
- package/package.json +9 -8
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as validateProps, i as isZodObject, n as PropValidationError, o as zodToJsonSchemaShape, t as PropSchema } from "./validation-MigzRuji.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { fromJsx } from "takumi-js/helpers/jsx";
|
|
4
4
|
import { ComponentType } from "react";
|
|
@@ -42,8 +42,13 @@ declare const studioConfigSchema: z.ZodObject<{
|
|
|
42
42
|
description: z.ZodOptional<z.ZodString>;
|
|
43
43
|
}, z.core.$strip>>;
|
|
44
44
|
}, z.core.$strip>;
|
|
45
|
+
declare const parseStudioConfig: (raw: unknown) => {
|
|
46
|
+
config: StudioConfig;
|
|
47
|
+
} | {
|
|
48
|
+
issues: string;
|
|
49
|
+
};
|
|
45
50
|
//#endregion
|
|
46
|
-
//#region src/
|
|
51
|
+
//#region src/engine/render.d.ts
|
|
47
52
|
interface CompiledTemplate {
|
|
48
53
|
node: Node;
|
|
49
54
|
stylesheets: string[];
|
|
@@ -85,7 +90,7 @@ declare const logInfo: (message: string, context?: Record<string, unknown>) => v
|
|
|
85
90
|
declare const logWarn: (message: string, context?: Record<string, unknown>) => void;
|
|
86
91
|
declare const logError: (code: ErrorCode, message: string, context?: Record<string, unknown>) => void;
|
|
87
92
|
//#endregion
|
|
88
|
-
//#region src/
|
|
93
|
+
//#region src/cache/cache-keys.d.ts
|
|
89
94
|
/**
|
|
90
95
|
* Cache key derivation, shared by every component that needs to name a
|
|
91
96
|
* cache entry consistently. Pulled out of the old monolithic RenderCache
|
|
@@ -108,7 +113,7 @@ interface PixelCacheKeyInput {
|
|
|
108
113
|
declare const createCompileCacheKey: (input: CompileCacheKeyInput) => string;
|
|
109
114
|
declare const createPixelCacheKey: (input: PixelCacheKeyInput) => string;
|
|
110
115
|
//#endregion
|
|
111
|
-
//#region src/
|
|
116
|
+
//#region src/cache/render-cache.d.ts
|
|
112
117
|
interface CacheStats {
|
|
113
118
|
compiled: {
|
|
114
119
|
entries: number;
|
|
@@ -176,7 +181,7 @@ declare class RenderCache {
|
|
|
176
181
|
private requireEviction;
|
|
177
182
|
}
|
|
178
183
|
//#endregion
|
|
179
|
-
//#region src/
|
|
184
|
+
//#region src/engine/takumi-types-shim.d.ts
|
|
180
185
|
/**
|
|
181
186
|
* Type-only shim for `@takumi-rs/core` (as re-exported by `takumi-js/node`).
|
|
182
187
|
*
|
|
@@ -276,7 +281,7 @@ interface MeasuredNode {
|
|
|
276
281
|
runs: MeasuredTextRun[];
|
|
277
282
|
}
|
|
278
283
|
//#endregion
|
|
279
|
-
//#region src/
|
|
284
|
+
//#region src/engine/renderer.d.ts
|
|
280
285
|
/**
|
|
281
286
|
* Preload a font once and reuse it across many renders without re-passing
|
|
282
287
|
* its bytes on every call. Optional -- per Takumi's v2 guidance, passing
|
|
@@ -317,35 +322,51 @@ declare const measureTemplate: (template: CompiledTemplate, options?: RenderOpti
|
|
|
317
322
|
*/
|
|
318
323
|
declare const renderAutoSized: (template: CompiledTemplate, options?: RenderOptions, signal?: AbortSignal) => Promise<RenderOutput>;
|
|
319
324
|
//#endregion
|
|
320
|
-
//#region src/
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
325
|
+
//#region src/project/project-paths.d.ts
|
|
326
|
+
interface ProjectPaths {
|
|
327
|
+
configPath: string;
|
|
328
|
+
defaultCacheDir: string;
|
|
329
|
+
localesDir: string;
|
|
330
|
+
templatesDir: string;
|
|
331
|
+
}
|
|
332
|
+
declare const resolveProjectPaths: (rootDir: string) => ProjectPaths;
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/cache/open-cache.d.ts
|
|
335
|
+
interface OpenCacheOptions {
|
|
336
|
+
cacheDir?: string;
|
|
337
|
+
maxSizeMB?: number;
|
|
328
338
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
339
|
+
declare const openCache: (rootDir: string, options?: OpenCacheOptions) => Promise<RenderCache>;
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region src/assets/assets-resolver.d.ts
|
|
342
|
+
interface AssetsForLocale {
|
|
343
|
+
fonts: Font[];
|
|
344
|
+
fontFamilies?: string[];
|
|
345
|
+
lang: string | undefined;
|
|
333
346
|
props: Record<string, unknown>;
|
|
334
|
-
width: number;
|
|
335
|
-
height: number;
|
|
336
|
-
locale?: string;
|
|
337
|
-
format?: OutputFormat;
|
|
338
|
-
cache: RenderCache;
|
|
339
|
-
outDir: string;
|
|
340
|
-
filename: string;
|
|
341
|
-
signal?: AbortSignal;
|
|
342
|
-
renderOptions?: Pick<RenderOptions, "fonts" | "fontFamilies" | "lang"> & Record<string, unknown>;
|
|
343
347
|
}
|
|
344
|
-
interface
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
durationMs: number;
|
|
348
|
+
interface FontResolutionResult {
|
|
349
|
+
fonts: Font[];
|
|
350
|
+
fontFamilies: string[];
|
|
348
351
|
}
|
|
352
|
+
interface AssetsResolverOptions {
|
|
353
|
+
fetchFonts?: (localeId: string) => Promise<FontResolutionResult | null>;
|
|
354
|
+
localeData?: Record<string, unknown>;
|
|
355
|
+
loadLocaleData?: (locale: string) => Promise<Record<string, unknown>>;
|
|
356
|
+
preResolvedFonts?: Font[];
|
|
357
|
+
preResolvedFontFamilies?: string[];
|
|
358
|
+
}
|
|
359
|
+
declare const resolveAssetsForLocale: (localeId: string, props: Record<string, unknown>, options?: AssetsResolverOptions) => Promise<AssetsForLocale>;
|
|
360
|
+
//#endregion
|
|
361
|
+
//#region src/batch/loaders.d.ts
|
|
362
|
+
interface LoadedTemplate {
|
|
363
|
+
module: TemplateModule;
|
|
364
|
+
contentHash: string;
|
|
365
|
+
}
|
|
366
|
+
declare const importTemplateForBatch: (rootDir: string, componentPath: string, templateId: string) => Promise<LoadedTemplate>;
|
|
367
|
+
declare const loadConfig: (rootDir: string) => Promise<StudioConfig>;
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/batch/types.d.ts
|
|
349
370
|
interface ManifestEntry {
|
|
350
371
|
rowIndex: number;
|
|
351
372
|
locale: string;
|
|
@@ -366,49 +387,9 @@ interface Manifest {
|
|
|
366
387
|
}[];
|
|
367
388
|
}
|
|
368
389
|
//#endregion
|
|
369
|
-
//#region src/
|
|
370
|
-
interface MatrixLocaleInput {
|
|
371
|
-
id: string;
|
|
372
|
-
data?: Record<string, unknown>;
|
|
373
|
-
}
|
|
374
|
-
interface DataRow {
|
|
375
|
-
key?: string;
|
|
376
|
-
id?: string;
|
|
377
|
-
slug?: string;
|
|
378
|
-
[key: string]: unknown;
|
|
379
|
-
}
|
|
380
|
-
interface MatrixInput {
|
|
381
|
-
rows: DataRow[];
|
|
382
|
-
locales: MatrixLocaleInput[];
|
|
383
|
-
presets: Pick<Preset, "id" | "width" | "height">[];
|
|
384
|
-
baseProps?: Record<string, unknown>;
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Expands rows × locales × presets into a flat array of matrix cells.
|
|
388
|
-
* Locale data is merged into props during expansion so each cell carries
|
|
389
|
-
* fully resolved props.
|
|
390
|
-
*/
|
|
391
|
-
declare const expandMatrix: (input: MatrixInput) => MatrixCell[];
|
|
392
|
-
//#endregion
|
|
393
|
-
//#region src/batch/render-one.d.ts
|
|
394
|
-
declare const renderOne: (input: RenderOneInput) => Promise<RenderOneOutput>;
|
|
395
|
-
//#endregion
|
|
396
|
-
//#region src/batch/naming.d.ts
|
|
397
|
-
interface NamingOptions {
|
|
398
|
-
templateId: string;
|
|
399
|
-
locale: string;
|
|
400
|
-
presetId: string;
|
|
401
|
-
rowKey: string;
|
|
402
|
-
format?: string;
|
|
403
|
-
}
|
|
404
|
-
declare const generateOutputFilename: (options: NamingOptions) => string;
|
|
405
|
-
//#endregion
|
|
406
|
-
//#region src/batch/manifest.d.ts
|
|
407
|
-
declare const writeManifest: (filePath: string, manifest: Manifest) => Promise<void>;
|
|
408
|
-
//#endregion
|
|
409
|
-
//#region src/batch/single.d.ts
|
|
390
|
+
//#region src/orchestrate/single.d.ts
|
|
410
391
|
interface SingleInput {
|
|
411
|
-
component: ComponentType<
|
|
392
|
+
component: ComponentType<Record<string, unknown>>;
|
|
412
393
|
propsSchema: PropSchema;
|
|
413
394
|
props: Record<string, unknown>;
|
|
414
395
|
preset: {
|
|
@@ -424,6 +405,55 @@ interface SingleOutput {
|
|
|
424
405
|
}
|
|
425
406
|
declare const renderComponent: (input: SingleInput) => Promise<SingleOutput>;
|
|
426
407
|
//#endregion
|
|
408
|
+
//#region src/orchestrate/types.d.ts
|
|
409
|
+
type OutputFormat = "webp" | "png" | "jpeg" | "ico" | "raw";
|
|
410
|
+
interface OrchestrateRenderInput {
|
|
411
|
+
component?: ComponentType<Record<string, unknown>>;
|
|
412
|
+
compiledTemplate?: CompiledTemplate;
|
|
413
|
+
templateContentHash: string;
|
|
414
|
+
templateId: string;
|
|
415
|
+
props: Record<string, unknown>;
|
|
416
|
+
propsSchema?: PropSchema;
|
|
417
|
+
locale?: string;
|
|
418
|
+
loadLocaleData?: (locale: string) => Promise<Record<string, unknown>>;
|
|
419
|
+
fonts?: Font[];
|
|
420
|
+
fontFamilies?: string[];
|
|
421
|
+
width: number;
|
|
422
|
+
height: number;
|
|
423
|
+
cache: RenderCache;
|
|
424
|
+
format?: OutputFormat;
|
|
425
|
+
signal?: AbortSignal;
|
|
426
|
+
}
|
|
427
|
+
interface OrchestrateRenderResult {
|
|
428
|
+
bytes: Buffer;
|
|
429
|
+
width: number;
|
|
430
|
+
height: number;
|
|
431
|
+
format: string;
|
|
432
|
+
cacheHit: boolean;
|
|
433
|
+
durationMs: number;
|
|
434
|
+
}
|
|
435
|
+
interface OrchestrateMeasureInput {
|
|
436
|
+
component?: ComponentType<Record<string, unknown>>;
|
|
437
|
+
compiledTemplate?: CompiledTemplate;
|
|
438
|
+
templateContentHash: string;
|
|
439
|
+
templateId: string;
|
|
440
|
+
props: Record<string, unknown>;
|
|
441
|
+
propsSchema?: PropSchema;
|
|
442
|
+
locale?: string;
|
|
443
|
+
loadLocaleData?: (locale: string) => Promise<Record<string, unknown>>;
|
|
444
|
+
cache: RenderCache;
|
|
445
|
+
signal?: AbortSignal;
|
|
446
|
+
}
|
|
447
|
+
interface OrchestrateMeasureResult {
|
|
448
|
+
width: number;
|
|
449
|
+
height: number;
|
|
450
|
+
durationMs: number;
|
|
451
|
+
}
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region src/orchestrate/orchestrate.d.ts
|
|
454
|
+
declare const orchestrateRender: (input: OrchestrateRenderInput) => Promise<OrchestrateRenderResult>;
|
|
455
|
+
declare const orchestrateMeasure: (input: OrchestrateMeasureInput) => Promise<OrchestrateMeasureResult>;
|
|
456
|
+
//#endregion
|
|
427
457
|
//#region src/batch/batch.d.ts
|
|
428
458
|
interface BatchInput {
|
|
429
459
|
rootDir: string;
|
|
@@ -444,17 +474,4 @@ interface BatchOutput {
|
|
|
444
474
|
}
|
|
445
475
|
declare const runBatch: (input: BatchInput) => Promise<BatchOutput>;
|
|
446
476
|
//#endregion
|
|
447
|
-
|
|
448
|
-
interface LoadedTemplate {
|
|
449
|
-
module: TemplateModule;
|
|
450
|
-
contentHash: string;
|
|
451
|
-
}
|
|
452
|
-
declare const loadConfig: (rootDir: string) => Promise<StudioConfig>;
|
|
453
|
-
declare const importTemplateForBatch: (rootDir: string, componentPath: string, templateId: string) => Promise<LoadedTemplate>;
|
|
454
|
-
declare const parseDataFile: (filePath: string) => Promise<Record<string, unknown>[]>;
|
|
455
|
-
declare const resolveLocales: (rootDir: string, localeCodes: string[], templateId: string) => Promise<{
|
|
456
|
-
id: string;
|
|
457
|
-
data?: Record<string, unknown>;
|
|
458
|
-
}[]>;
|
|
459
|
-
//#endregion
|
|
460
|
-
export { AppError, type BatchInput, type BatchOutput, type CacheStats, type CompileCacheKeyInput, type CompiledTemplate, type DataRow, ErrorCode, type ErrorCode as ErrorCodeType, type Font, type LoadedTemplate, type LogLevel, type Manifest, type ManifestEntry, type MatrixCell, type MatrixInput, type MatrixLocaleInput, type NamingOptions, type PixelCacheKeyInput, type Preset, PropValidationError, type PropValidationIssue, RenderCache, type RenderCacheOptions, type RenderOneInput, type RenderOneOutput, type RenderOptions, type RenderOutput, type SingleInput, type SingleOutput, type StudioConfig, type TemplateEntry, type TemplateModule, compileTemplate, expandMatrix, generateOutputFilename, importTemplateForBatch, isZodObject, loadConfig, logDebug, logError, logInfo, logWarn, measureTemplate, parseDataFile, registerFont, renderAutoSized, renderComponent, renderOne, renderToPixels, resolveLocales, runBatch, studioConfigSchema, validateProps, writeManifest, zodToJsonSchemaShape };
|
|
477
|
+
export { AppError, type AssetsForLocale, type AssetsResolverOptions, type BatchInput, type BatchOutput, type CacheStats, type CompiledTemplate, ErrorCode, type ErrorCode as ErrorCodeType, 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, compileTemplate, importTemplateForBatch, isZodObject, loadConfig, logDebug, logError, logInfo, logWarn, measureTemplate, openCache, orchestrateMeasure, orchestrateRender, parseStudioConfig, registerFont, renderAutoSized, renderComponent, renderToPixels, resolveAssetsForLocale, resolveProjectPaths, runBatch, studioConfigSchema, validateProps, zodToJsonSchemaShape };
|
package/dist/index.mjs
CHANGED
|
@@ -12,9 +12,9 @@ import { Database } from "bun:sqlite";
|
|
|
12
12
|
import { Kysely, sql } from "kysely";
|
|
13
13
|
import { BunSqliteDialect } from "kysely-bun-sqlite";
|
|
14
14
|
import { Renderer } from "takumi-js/node";
|
|
15
|
-
import { createElement } from "react";
|
|
16
15
|
import { googleFonts } from "@takumi-rs/helpers";
|
|
17
16
|
import { pathToFileURL } from "node:url";
|
|
17
|
+
import { createElement } from "react";
|
|
18
18
|
//#region src/shared/config-schema.ts
|
|
19
19
|
const templateEntrySchema = z.object({
|
|
20
20
|
componentPath: z.string().min(1, "componentPath is required"),
|
|
@@ -33,8 +33,13 @@ const studioConfigSchema = z.object({
|
|
|
33
33
|
presets: z.array(presetSchema),
|
|
34
34
|
templates: z.record(z.string(), templateEntrySchema)
|
|
35
35
|
});
|
|
36
|
+
const parseStudioConfig = (raw) => {
|
|
37
|
+
const parsed = studioConfigSchema.safeParse(raw);
|
|
38
|
+
if (!parsed.success) return { issues: parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ") };
|
|
39
|
+
return { config: parsed.data };
|
|
40
|
+
};
|
|
36
41
|
//#endregion
|
|
37
|
-
//#region src/
|
|
42
|
+
//#region src/engine/render.ts
|
|
38
43
|
const compileTemplate = async (element, options) => {
|
|
39
44
|
const result = await fromJsx(element, options);
|
|
40
45
|
return {
|
|
@@ -94,11 +99,11 @@ const logError = (code, message, context) => {
|
|
|
94
99
|
console.error(`[${timestamp()}] ERROR [${code}] ${message}${stringifyContext(context)}`);
|
|
95
100
|
};
|
|
96
101
|
//#endregion
|
|
97
|
-
//#region src/
|
|
102
|
+
//#region src/cache/cache-keys.ts
|
|
98
103
|
const createCompileCacheKey = (input) => createHash("sha256").update(`${input.templateId}\u0000${input.templateContentHash}\u0000${input.propsJSON}`).digest("hex").slice(0, 16);
|
|
99
104
|
const createPixelCacheKey = (input) => createHash("sha256").update(`${input.templateContentHash}\u0000${input.propsJSON}\u0000${input.width}x${input.height}\u0000${input.format ?? "png"}`).digest("hex").slice(0, 32);
|
|
100
105
|
//#endregion
|
|
101
|
-
//#region src/
|
|
106
|
+
//#region src/cache/compile-cache.ts
|
|
102
107
|
/**
|
|
103
108
|
* CompileCache: in-memory compile-result cache.
|
|
104
109
|
*
|
|
@@ -176,7 +181,7 @@ var CompileCache = class {
|
|
|
176
181
|
}
|
|
177
182
|
};
|
|
178
183
|
//#endregion
|
|
179
|
-
//#region src/
|
|
184
|
+
//#region src/cache/concurrency-limiter.ts
|
|
180
185
|
/**
|
|
181
186
|
* Bounds how many concurrent operations (writes/reads through FsStore)
|
|
182
187
|
* run at once. Exists to cap the "N concurrent large-buffer renders spike
|
|
@@ -210,7 +215,7 @@ var ConcurrencyLimiter = class {
|
|
|
210
215
|
}
|
|
211
216
|
};
|
|
212
217
|
//#endregion
|
|
213
|
-
//#region src/
|
|
218
|
+
//#region src/cache/eviction-coordinator.ts
|
|
214
219
|
const DEFAULT_DEBOUNCE_MS = 250;
|
|
215
220
|
/**
|
|
216
221
|
* Coordinates eviction across both time (debouncing) and processes
|
|
@@ -296,7 +301,7 @@ var EvictionCoordinator = class {
|
|
|
296
301
|
}
|
|
297
302
|
};
|
|
298
303
|
//#endregion
|
|
299
|
-
//#region src/
|
|
304
|
+
//#region src/cache/fs-store.ts
|
|
300
305
|
const defaultFsPrimitives = {
|
|
301
306
|
existsSync,
|
|
302
307
|
readFile,
|
|
@@ -415,7 +420,7 @@ var FsStore = class {
|
|
|
415
420
|
}
|
|
416
421
|
};
|
|
417
422
|
//#endregion
|
|
418
|
-
//#region src/
|
|
423
|
+
//#region src/cache/metadata-store.ts
|
|
419
424
|
const BUSY_TIMEOUT_MS = 5e3;
|
|
420
425
|
const toRow = (r) => ({
|
|
421
426
|
contentHash: r.content_hash,
|
|
@@ -598,7 +603,7 @@ var MetadataStore = class {
|
|
|
598
603
|
}
|
|
599
604
|
};
|
|
600
605
|
//#endregion
|
|
601
|
-
//#region src/
|
|
606
|
+
//#region src/cache/render-cache.ts
|
|
602
607
|
const DEFAULT_MAX_SIZE_MB = 500;
|
|
603
608
|
/**
|
|
604
609
|
* Content-addressed two-stage render cache -- coordinator.
|
|
@@ -781,7 +786,7 @@ var RenderCache = class {
|
|
|
781
786
|
}
|
|
782
787
|
};
|
|
783
788
|
//#endregion
|
|
784
|
-
//#region src/
|
|
789
|
+
//#region src/engine/takumi-types-shim.ts
|
|
785
790
|
/**
|
|
786
791
|
* Type-only shim for `@takumi-rs/core` (as re-exported by `takumi-js/node`).
|
|
787
792
|
*
|
|
@@ -821,7 +826,7 @@ var RenderCache = class {
|
|
|
821
826
|
*/
|
|
822
827
|
const Renderer$1 = Renderer;
|
|
823
828
|
//#endregion
|
|
824
|
-
//#region src/
|
|
829
|
+
//#region src/engine/renderer.ts
|
|
825
830
|
/**
|
|
826
831
|
* Renderer lifecycle -- v2.
|
|
827
832
|
*
|
|
@@ -911,175 +916,108 @@ const renderAutoSized = async (template, options, signal) => {
|
|
|
911
916
|
}, options, signal);
|
|
912
917
|
};
|
|
913
918
|
//#endregion
|
|
914
|
-
//#region src/
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
};
|
|
922
|
-
/**
|
|
923
|
-
* Expands rows × locales × presets into a flat array of matrix cells.
|
|
924
|
-
* Locale data is merged into props during expansion so each cell carries
|
|
925
|
-
* fully resolved props.
|
|
926
|
-
*/
|
|
927
|
-
const expandMatrix = (input) => {
|
|
928
|
-
const { rows, locales, presets, baseProps = {} } = input;
|
|
929
|
-
if (rows.length === 0) return [];
|
|
930
|
-
const cells = [];
|
|
931
|
-
for (const [rowIndex, row] of rows.entries()) {
|
|
932
|
-
const rowKey = String(row.key ?? row.id ?? row.slug ?? String(rowIndex));
|
|
933
|
-
const rowProps = {
|
|
934
|
-
...baseProps,
|
|
935
|
-
...row
|
|
936
|
-
};
|
|
937
|
-
delete rowProps.key;
|
|
938
|
-
delete rowProps.id;
|
|
939
|
-
delete rowProps.slug;
|
|
940
|
-
for (const locale of locales) {
|
|
941
|
-
const localeProps = mergeLocale(rowProps, locale.data);
|
|
942
|
-
for (const preset of presets) cells.push({
|
|
943
|
-
locale: locale.id,
|
|
944
|
-
preset: {
|
|
945
|
-
height: preset.height,
|
|
946
|
-
id: preset.id,
|
|
947
|
-
width: preset.width
|
|
948
|
-
},
|
|
949
|
-
props: {
|
|
950
|
-
...localeProps,
|
|
951
|
-
locale: locale.id
|
|
952
|
-
},
|
|
953
|
-
rowIndex,
|
|
954
|
-
rowKey
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
return cells;
|
|
959
|
-
};
|
|
919
|
+
//#region src/project/project-paths.ts
|
|
920
|
+
const resolveProjectPaths = (rootDir) => ({
|
|
921
|
+
configPath: path.resolve(rootDir, "studio.config.ts"),
|
|
922
|
+
defaultCacheDir: path.resolve(rootDir, ".studio-cache"),
|
|
923
|
+
localesDir: path.resolve(rootDir, "locales"),
|
|
924
|
+
templatesDir: path.resolve(rootDir, "templates")
|
|
925
|
+
});
|
|
960
926
|
//#endregion
|
|
961
|
-
//#region src/
|
|
962
|
-
const
|
|
963
|
-
|
|
927
|
+
//#region src/cache/open-cache.ts
|
|
928
|
+
const openCache = async (rootDir, options) => {
|
|
929
|
+
const cache = new RenderCache({
|
|
930
|
+
cacheDir: options?.cacheDir ?? resolveProjectPaths(rootDir).defaultCacheDir,
|
|
931
|
+
maxSizeMB: options?.maxSizeMB
|
|
932
|
+
});
|
|
933
|
+
await cache.init();
|
|
934
|
+
return cache;
|
|
964
935
|
};
|
|
965
|
-
|
|
936
|
+
//#endregion
|
|
937
|
+
//#region src/assets/assets-resolver.ts
|
|
938
|
+
const isFontDescriptor = (f) => typeof f === "object" && !(f instanceof Uint8Array);
|
|
939
|
+
const defaultFetchFonts = async (localeId) => {
|
|
966
940
|
try {
|
|
967
|
-
await
|
|
941
|
+
const fonts = await googleFonts([{
|
|
942
|
+
name: "Noto Sans Arabic",
|
|
943
|
+
weight: [400, 700]
|
|
944
|
+
}]);
|
|
945
|
+
const fontFamilies = fonts.filter(isFontDescriptor).map((f) => String(f.name ?? "")).filter(Boolean);
|
|
946
|
+
logDebug("googleFonts resolved per locale", {
|
|
947
|
+
count: fonts.length,
|
|
948
|
+
localeId,
|
|
949
|
+
names: fonts.filter(isFontDescriptor).map((f) => f.name)
|
|
950
|
+
});
|
|
951
|
+
return {
|
|
952
|
+
fontFamilies,
|
|
953
|
+
fonts
|
|
954
|
+
};
|
|
968
955
|
} catch (error) {
|
|
969
|
-
|
|
956
|
+
logDebug("googleFonts failed for locale", {
|
|
970
957
|
error: String(error),
|
|
971
|
-
|
|
958
|
+
localeId
|
|
972
959
|
});
|
|
960
|
+
return null;
|
|
973
961
|
}
|
|
974
962
|
};
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
const
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
propsJSON,
|
|
985
|
-
templateContentHash: contentHash,
|
|
986
|
-
width
|
|
987
|
-
});
|
|
988
|
-
const cached = await cache.getPixels(pixelKey);
|
|
989
|
-
if (cached) {
|
|
990
|
-
const outputPath = `${outDir}/${filename}`;
|
|
991
|
-
await ensureDir(outDir);
|
|
992
|
-
await tryWriteFile(outputPath, cached);
|
|
993
|
-
return {
|
|
994
|
-
cacheHit: true,
|
|
995
|
-
durationMs: performance.now() - start,
|
|
996
|
-
outputPath
|
|
997
|
-
};
|
|
998
|
-
}
|
|
999
|
-
const lang = locale?.startsWith("ar") ? "ar" : void 0;
|
|
1000
|
-
logDebug("renderOne -> renderToPixels", {
|
|
1001
|
-
fontFamilies: renderOptions?.fontFamilies,
|
|
1002
|
-
fontNames: renderOptions?.fonts?.map((f) => typeof f === "object" && "name" in f ? f.name : "raw"),
|
|
1003
|
-
fontsCount: renderOptions?.fonts?.length ?? 0,
|
|
1004
|
-
lang,
|
|
1005
|
-
locale
|
|
1006
|
-
});
|
|
1007
|
-
const output = await renderToPixels(compiledTemplate, {
|
|
1008
|
-
height,
|
|
1009
|
-
width
|
|
1010
|
-
}, {
|
|
1011
|
-
fontFamilies: renderOptions?.fontFamilies,
|
|
1012
|
-
fonts: renderOptions?.fonts,
|
|
1013
|
-
format,
|
|
1014
|
-
lang
|
|
1015
|
-
}, signal);
|
|
1016
|
-
await cache.setPixels(pixelKey, output.bytes, width, height, format);
|
|
1017
|
-
const outputPath = `${outDir}/${filename}`;
|
|
1018
|
-
await ensureDir(outDir);
|
|
1019
|
-
await tryWriteFile(outputPath, output.bytes);
|
|
963
|
+
const mergeLocaleProps = (props, localeData) => {
|
|
964
|
+
if (!localeData) return { ...props };
|
|
965
|
+
const merged = { ...props };
|
|
966
|
+
for (const [key, value] of Object.entries(localeData)) if (Array.isArray(value) && Array.isArray(merged[key])) merged[key] = value;
|
|
967
|
+
else if (typeof value === "string" && typeof merged[key] === "string") merged[key] = value;
|
|
968
|
+
return merged;
|
|
969
|
+
};
|
|
970
|
+
const classifyLocale = (localeId) => {
|
|
971
|
+
const isArabic = localeId.startsWith("ar");
|
|
1020
972
|
return {
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
outputPath
|
|
973
|
+
lang: isArabic ? "ar" : void 0,
|
|
974
|
+
needsArabic: isArabic
|
|
1024
975
|
};
|
|
1025
976
|
};
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
};
|
|
1032
|
-
//#endregion
|
|
1033
|
-
//#region src/batch/manifest.ts
|
|
1034
|
-
const writeManifest = async (filePath, manifest) => {
|
|
1035
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
1036
|
-
await writeFile(filePath, JSON.stringify(manifest, null, 2));
|
|
1037
|
-
};
|
|
1038
|
-
//#endregion
|
|
1039
|
-
//#region src/batch/single.ts
|
|
1040
|
-
const renderComponent = async (input) => {
|
|
1041
|
-
const { component, format = "png", outDir = "./out", preset, props, propsSchema } = input;
|
|
1042
|
-
const { bytes } = await renderToPixels(await compileTemplate(createElement(component, validateProps(propsSchema, props))), {
|
|
1043
|
-
height: preset.height,
|
|
1044
|
-
width: preset.width
|
|
1045
|
-
}, { format });
|
|
1046
|
-
const outputPath = `${outDir}/${generateOutputFilename({
|
|
1047
|
-
locale: "default",
|
|
1048
|
-
presetId: preset.id,
|
|
1049
|
-
rowKey: "single",
|
|
1050
|
-
templateId: "single"
|
|
1051
|
-
})}`;
|
|
1052
|
-
await ensureDir(outDir);
|
|
1053
|
-
await Bun.write(outputPath, bytes);
|
|
1054
|
-
return { outputPath };
|
|
1055
|
-
};
|
|
1056
|
-
//#endregion
|
|
1057
|
-
//#region src/batch/loaders.ts
|
|
1058
|
-
const loadConfig = async (rootDir) => {
|
|
1059
|
-
const configPath = path.resolve(rootDir, "studio.config.ts");
|
|
1060
|
-
try {
|
|
1061
|
-
await readFile(configPath, "utf-8");
|
|
977
|
+
const fontCache = /* @__PURE__ */ new Map();
|
|
978
|
+
const resolveAssetsForLocale = async (localeId, props, options) => {
|
|
979
|
+
let localeData = options?.localeData;
|
|
980
|
+
if (options?.loadLocaleData) try {
|
|
981
|
+
localeData = await options.loadLocaleData(localeId);
|
|
1062
982
|
} catch {
|
|
1063
|
-
|
|
983
|
+
localeData = {};
|
|
1064
984
|
}
|
|
1065
|
-
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1071
|
-
|
|
985
|
+
const mergedProps = mergeLocaleProps(props, localeData);
|
|
986
|
+
const { lang, needsArabic } = classifyLocale(localeId);
|
|
987
|
+
let fonts = [];
|
|
988
|
+
let fontFamilies;
|
|
989
|
+
if (options?.preResolvedFonts) {
|
|
990
|
+
fonts = options.preResolvedFonts;
|
|
991
|
+
fontFamilies = options.preResolvedFontFamilies;
|
|
992
|
+
} else if (needsArabic) {
|
|
993
|
+
const fetch = options?.fetchFonts ?? defaultFetchFonts;
|
|
994
|
+
const cached = fontCache.get(localeId);
|
|
995
|
+
let resolution;
|
|
996
|
+
if (cached) resolution = await cached;
|
|
997
|
+
else {
|
|
998
|
+
const promise = fetch(localeId);
|
|
999
|
+
fontCache.set(localeId, promise);
|
|
1000
|
+
resolution = await promise;
|
|
1001
|
+
}
|
|
1002
|
+
if (resolution) ({fontFamilies, fonts} = resolution);
|
|
1072
1003
|
}
|
|
1073
|
-
return
|
|
1004
|
+
return {
|
|
1005
|
+
fontFamilies,
|
|
1006
|
+
fonts,
|
|
1007
|
+
lang,
|
|
1008
|
+
props: mergedProps
|
|
1009
|
+
};
|
|
1074
1010
|
};
|
|
1075
|
-
|
|
1076
|
-
|
|
1011
|
+
//#endregion
|
|
1012
|
+
//#region src/batch/loaders.ts
|
|
1013
|
+
const templateCache = /* @__PURE__ */ new Map();
|
|
1014
|
+
const loadTemplateForBatch = async (fullPath, templateId) => {
|
|
1077
1015
|
let content;
|
|
1078
1016
|
try {
|
|
1079
1017
|
content = await readFile(fullPath, "utf-8");
|
|
1080
1018
|
} catch {
|
|
1081
1019
|
throw new AppError(ErrorCode.TEMPLATE_LOAD_FAILED, `Template file not found: ${fullPath}`, {
|
|
1082
|
-
componentPath,
|
|
1020
|
+
componentPath: fullPath,
|
|
1083
1021
|
templateId
|
|
1084
1022
|
});
|
|
1085
1023
|
}
|
|
@@ -1097,6 +1035,32 @@ const importTemplateForBatch = async (rootDir, componentPath, templateId) => {
|
|
|
1097
1035
|
module: mod
|
|
1098
1036
|
};
|
|
1099
1037
|
};
|
|
1038
|
+
const importTemplateForBatch = async (rootDir, componentPath, templateId) => {
|
|
1039
|
+
const fullPath = path.resolve(rootDir, `${componentPath}.tsx`);
|
|
1040
|
+
const cached = templateCache.get(fullPath);
|
|
1041
|
+
if (cached) return cached;
|
|
1042
|
+
const promise = loadTemplateForBatch(fullPath, templateId);
|
|
1043
|
+
templateCache.set(fullPath, promise);
|
|
1044
|
+
try {
|
|
1045
|
+
return await promise;
|
|
1046
|
+
} catch {
|
|
1047
|
+
templateCache.delete(fullPath);
|
|
1048
|
+
throw await promise.catch((error) => error);
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
const loadConfig = async (rootDir) => {
|
|
1052
|
+
const { configPath } = resolveProjectPaths(rootDir);
|
|
1053
|
+
try {
|
|
1054
|
+
await readFile(configPath, "utf-8");
|
|
1055
|
+
} catch {
|
|
1056
|
+
throw new AppError(ErrorCode.CONFIG_NOT_FOUND, `No studio.config.ts found in ${rootDir}.`);
|
|
1057
|
+
}
|
|
1058
|
+
logDebug("Loading studio.config.ts", { path: configPath });
|
|
1059
|
+
const mod = await import(pathToFileURL(configPath).href);
|
|
1060
|
+
const result = parseStudioConfig(mod.default ?? mod);
|
|
1061
|
+
if ("issues" in result) throw new AppError(ErrorCode.CONFIG_INVALID, `studio.config.ts is invalid: ${result.issues}`);
|
|
1062
|
+
return result.config;
|
|
1063
|
+
};
|
|
1100
1064
|
const parseDataFile = async (filePath) => {
|
|
1101
1065
|
const content = await readFile(filePath, "utf-8");
|
|
1102
1066
|
if (filePath.endsWith(".json")) {
|
|
@@ -1116,8 +1080,9 @@ const parseDataFile = async (filePath) => {
|
|
|
1116
1080
|
};
|
|
1117
1081
|
const resolveLocales = async (rootDir, localeCodes, templateId) => {
|
|
1118
1082
|
if (localeCodes.length === 0) return [{ id: "default" }];
|
|
1083
|
+
const { localesDir } = resolveProjectPaths(rootDir);
|
|
1119
1084
|
return await Promise.all(localeCodes.map(async (code) => {
|
|
1120
|
-
const localePath = path.resolve(
|
|
1085
|
+
const localePath = path.resolve(localesDir, `${code}.json`);
|
|
1121
1086
|
try {
|
|
1122
1087
|
const content = await readFile(localePath, "utf-8");
|
|
1123
1088
|
return {
|
|
@@ -1130,36 +1095,211 @@ const resolveLocales = async (rootDir, localeCodes, templateId) => {
|
|
|
1130
1095
|
}));
|
|
1131
1096
|
};
|
|
1132
1097
|
//#endregion
|
|
1133
|
-
//#region src/batch/
|
|
1134
|
-
const
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1098
|
+
//#region src/batch/naming.ts
|
|
1099
|
+
const generateOutputFilename = (options) => {
|
|
1100
|
+
const { templateId, locale, presetId, rowKey, format = "png" } = options;
|
|
1101
|
+
return `${templateId}.${locale}.${presetId}.${rowKey}.${format}`;
|
|
1102
|
+
};
|
|
1103
|
+
//#endregion
|
|
1104
|
+
//#region src/orchestrate/orchestrate.ts
|
|
1105
|
+
const prepPipeline = async (input) => {
|
|
1106
|
+
const { compiledTemplate: preCompiled, component, templateContentHash, templateId, props, propsSchema, locale, loadLocaleData, preResolvedFonts, preResolvedFontFamilies, cache } = input;
|
|
1107
|
+
const { fonts: resolvedFonts, fontFamilies: resolvedFontFamilies, lang: resolvedLang, props: merged } = await resolveAssetsForLocale(locale, propsSchema ? validateProps(propsSchema, props) : { ...props }, {
|
|
1108
|
+
loadLocaleData,
|
|
1109
|
+
preResolvedFontFamilies,
|
|
1110
|
+
preResolvedFonts
|
|
1111
|
+
});
|
|
1112
|
+
const propsJSON = JSON.stringify(merged);
|
|
1113
|
+
const compileKey = RenderCache.createCompileCacheKey({
|
|
1114
|
+
propsJSON,
|
|
1115
|
+
templateContentHash,
|
|
1116
|
+
templateId
|
|
1117
|
+
});
|
|
1118
|
+
let compiled = preCompiled;
|
|
1119
|
+
if (!compiled) {
|
|
1120
|
+
if (!component) throw new Error("Must provide either compiledTemplate or component");
|
|
1121
|
+
const cachedCompile = cache.getCompiled(compileKey);
|
|
1122
|
+
if (cachedCompile) compiled = cachedCompile;
|
|
1123
|
+
else {
|
|
1124
|
+
compiled = await compileTemplate(createElement(component, merged));
|
|
1125
|
+
cache.setCompiledWithContentHash(compileKey, templateContentHash, compiled);
|
|
1126
|
+
}
|
|
1157
1127
|
}
|
|
1158
1128
|
return {
|
|
1159
|
-
|
|
1160
|
-
|
|
1129
|
+
compileKey,
|
|
1130
|
+
compiled,
|
|
1131
|
+
mergedProps: merged,
|
|
1132
|
+
propsJSON,
|
|
1133
|
+
resolvedFontFamilies,
|
|
1134
|
+
resolvedFonts,
|
|
1135
|
+
resolvedLang
|
|
1136
|
+
};
|
|
1137
|
+
};
|
|
1138
|
+
const orchestrateRender = async (input) => {
|
|
1139
|
+
const start = performance.now();
|
|
1140
|
+
const { compiledTemplate, component, templateContentHash, templateId, props, propsSchema, locale: localeId, loadLocaleData, fonts: preResolvedFonts, fontFamilies: preResolvedFontFamilies, width, height, cache, format = "png", signal } = input;
|
|
1141
|
+
const prep = await prepPipeline({
|
|
1142
|
+
cache,
|
|
1143
|
+
compiledTemplate,
|
|
1144
|
+
component,
|
|
1145
|
+
loadLocaleData,
|
|
1146
|
+
locale: localeId ?? "default",
|
|
1147
|
+
preResolvedFontFamilies,
|
|
1148
|
+
preResolvedFonts,
|
|
1149
|
+
props,
|
|
1150
|
+
propsSchema,
|
|
1151
|
+
templateContentHash,
|
|
1152
|
+
templateId
|
|
1153
|
+
});
|
|
1154
|
+
const pixelKey = RenderCache.createPixelCacheKey({
|
|
1155
|
+
format,
|
|
1156
|
+
height,
|
|
1157
|
+
propsJSON: prep.propsJSON,
|
|
1158
|
+
templateContentHash,
|
|
1159
|
+
width
|
|
1160
|
+
});
|
|
1161
|
+
const cached = await cache.getPixels(pixelKey);
|
|
1162
|
+
if (cached) return {
|
|
1163
|
+
bytes: cached,
|
|
1164
|
+
cacheHit: true,
|
|
1165
|
+
durationMs: performance.now() - start,
|
|
1166
|
+
format,
|
|
1167
|
+
height,
|
|
1168
|
+
width
|
|
1169
|
+
};
|
|
1170
|
+
const output = await renderToPixels(prep.compiled, {
|
|
1171
|
+
height,
|
|
1172
|
+
width
|
|
1173
|
+
}, {
|
|
1174
|
+
fontFamilies: (prep.resolvedFontFamilies ?? []).length > 0 ? prep.resolvedFontFamilies : void 0,
|
|
1175
|
+
fonts: prep.resolvedFonts.length > 0 ? prep.resolvedFonts : void 0,
|
|
1176
|
+
format,
|
|
1177
|
+
lang: prep.resolvedLang
|
|
1178
|
+
}, signal);
|
|
1179
|
+
await cache.setPixels(pixelKey, output.bytes, width, height, format);
|
|
1180
|
+
return {
|
|
1181
|
+
bytes: output.bytes,
|
|
1182
|
+
cacheHit: false,
|
|
1183
|
+
durationMs: performance.now() - start,
|
|
1184
|
+
format,
|
|
1185
|
+
height: output.height,
|
|
1186
|
+
width: output.width
|
|
1161
1187
|
};
|
|
1162
1188
|
};
|
|
1189
|
+
const orchestrateMeasure = async (input) => {
|
|
1190
|
+
const start = performance.now();
|
|
1191
|
+
const { compiledTemplate, component, templateContentHash, templateId, props, propsSchema, locale: localeId, loadLocaleData, cache, signal } = input;
|
|
1192
|
+
const prep = await prepPipeline({
|
|
1193
|
+
cache,
|
|
1194
|
+
compiledTemplate,
|
|
1195
|
+
component,
|
|
1196
|
+
loadLocaleData,
|
|
1197
|
+
locale: localeId ?? "default",
|
|
1198
|
+
preResolvedFontFamilies: void 0,
|
|
1199
|
+
preResolvedFonts: void 0,
|
|
1200
|
+
props,
|
|
1201
|
+
propsSchema,
|
|
1202
|
+
templateContentHash,
|
|
1203
|
+
templateId
|
|
1204
|
+
});
|
|
1205
|
+
const measured = await measureTemplate(prep.compiled, {
|
|
1206
|
+
fontFamilies: (prep.resolvedFontFamilies ?? []).length > 0 ? prep.resolvedFontFamilies : void 0,
|
|
1207
|
+
fonts: prep.resolvedFonts.length > 0 ? prep.resolvedFonts : void 0,
|
|
1208
|
+
lang: prep.resolvedLang
|
|
1209
|
+
}, signal);
|
|
1210
|
+
return {
|
|
1211
|
+
durationMs: performance.now() - start,
|
|
1212
|
+
height: measured.height,
|
|
1213
|
+
width: measured.width
|
|
1214
|
+
};
|
|
1215
|
+
};
|
|
1216
|
+
//#endregion
|
|
1217
|
+
//#region src/orchestrate/utils.ts
|
|
1218
|
+
const ensureDir = async (dir) => {
|
|
1219
|
+
if (!existsSync(dir)) await mkdir(dir, { recursive: true });
|
|
1220
|
+
};
|
|
1221
|
+
const tryWriteFile = async (filePath, data) => {
|
|
1222
|
+
try {
|
|
1223
|
+
await writeFile(filePath, data);
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
logWarn("Failed to write output file", {
|
|
1226
|
+
error: String(error),
|
|
1227
|
+
filePath
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
//#endregion
|
|
1232
|
+
//#region src/orchestrate/single.ts
|
|
1233
|
+
const noop = () => {};
|
|
1234
|
+
const noopCache = {
|
|
1235
|
+
getCompiled: () => null,
|
|
1236
|
+
getPixels: () => null,
|
|
1237
|
+
setCompiledWithContentHash: noop,
|
|
1238
|
+
setPixels: noop
|
|
1239
|
+
};
|
|
1240
|
+
const renderComponent = async (input) => {
|
|
1241
|
+
const { component, format = "png", outDir = "./out", preset, props, propsSchema } = input;
|
|
1242
|
+
const result = await orchestrateRender({
|
|
1243
|
+
cache: noopCache,
|
|
1244
|
+
component,
|
|
1245
|
+
format,
|
|
1246
|
+
height: preset.height,
|
|
1247
|
+
props,
|
|
1248
|
+
propsSchema,
|
|
1249
|
+
templateContentHash: "single",
|
|
1250
|
+
templateId: "single",
|
|
1251
|
+
width: preset.width
|
|
1252
|
+
});
|
|
1253
|
+
const outputPath = `${outDir}/${generateOutputFilename({
|
|
1254
|
+
locale: "default",
|
|
1255
|
+
presetId: preset.id,
|
|
1256
|
+
rowKey: "single",
|
|
1257
|
+
templateId: "single"
|
|
1258
|
+
})}`;
|
|
1259
|
+
await ensureDir(outDir);
|
|
1260
|
+
await Bun.write(outputPath, result.bytes);
|
|
1261
|
+
return { outputPath };
|
|
1262
|
+
};
|
|
1263
|
+
//#endregion
|
|
1264
|
+
//#region src/batch/manifest.ts
|
|
1265
|
+
const writeManifest = async (filePath, manifest) => {
|
|
1266
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
1267
|
+
await writeFile(filePath, JSON.stringify(manifest, null, 2));
|
|
1268
|
+
};
|
|
1269
|
+
//#endregion
|
|
1270
|
+
//#region src/batch/matrix.ts
|
|
1271
|
+
const expandMatrix = (input) => {
|
|
1272
|
+
const { rows, locales, presets, baseProps = {} } = input;
|
|
1273
|
+
if (rows.length === 0) return [];
|
|
1274
|
+
const cells = [];
|
|
1275
|
+
for (const [rowIndex, row] of rows.entries()) {
|
|
1276
|
+
const rowKey = String(row.key ?? row.id ?? row.slug ?? String(rowIndex));
|
|
1277
|
+
const rowProps = {
|
|
1278
|
+
...baseProps,
|
|
1279
|
+
...row
|
|
1280
|
+
};
|
|
1281
|
+
delete rowProps.key;
|
|
1282
|
+
delete rowProps.id;
|
|
1283
|
+
delete rowProps.slug;
|
|
1284
|
+
for (const localeId of locales) for (const preset of presets) cells.push({
|
|
1285
|
+
locale: localeId,
|
|
1286
|
+
preset: {
|
|
1287
|
+
height: preset.height,
|
|
1288
|
+
id: preset.id,
|
|
1289
|
+
width: preset.width
|
|
1290
|
+
},
|
|
1291
|
+
props: {
|
|
1292
|
+
...rowProps,
|
|
1293
|
+
locale: localeId
|
|
1294
|
+
},
|
|
1295
|
+
rowIndex,
|
|
1296
|
+
rowKey
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
return cells;
|
|
1300
|
+
};
|
|
1301
|
+
//#endregion
|
|
1302
|
+
//#region src/batch/batch.ts
|
|
1163
1303
|
const runBatch = async (input) => {
|
|
1164
1304
|
const start = performance.now();
|
|
1165
1305
|
const { baseProps = {}, dataFile, failFast = false, cacheDir: cacheDirOverride, locales: filterLocales, outDir: outDirOverride, presets: filterPresets, rootDir, rows: inlineRows, templateId } = input;
|
|
@@ -1177,63 +1317,48 @@ const runBatch = async (input) => {
|
|
|
1177
1317
|
width: p.width
|
|
1178
1318
|
};
|
|
1179
1319
|
});
|
|
1180
|
-
const
|
|
1320
|
+
const resolvedLocales = await resolveLocales(rootDir, filterLocales ?? config.locales ?? [], templateId);
|
|
1321
|
+
const localeDataMap = new Map(resolvedLocales.map((l) => [l.id, l.data ?? {}]));
|
|
1322
|
+
const loadLocaleData = (code) => Promise.resolve(localeDataMap.get(code) ?? {});
|
|
1323
|
+
const rows = dataFile ? await parseDataFile(path.resolve(rootDir, dataFile)) : inlineRows ?? [];
|
|
1181
1324
|
const cells = expandMatrix({
|
|
1182
1325
|
baseProps,
|
|
1183
|
-
locales,
|
|
1326
|
+
locales: resolvedLocales.map((l) => l.id),
|
|
1184
1327
|
presets,
|
|
1185
|
-
rows
|
|
1328
|
+
rows
|
|
1186
1329
|
});
|
|
1187
1330
|
const outDir = path.resolve(rootDir, outDirOverride ?? config.outDir ?? "./out");
|
|
1188
|
-
const cache =
|
|
1189
|
-
await cache.init();
|
|
1190
|
-
const arabicLocales = locales.filter((l) => l.id.startsWith("ar"));
|
|
1191
|
-
logDebug("Arabic locale detection", {
|
|
1192
|
-
arabicCount: arabicLocales.length,
|
|
1193
|
-
localeIds: locales.map((l) => l.id)
|
|
1194
|
-
});
|
|
1195
|
-
let fonts = [];
|
|
1196
|
-
let fontFamilies;
|
|
1197
|
-
if (arabicLocales.length > 0) {
|
|
1198
|
-
const { fonts: resolvedFonts, fontFamilies: resolvedFamilies } = await resolveArabicFonts(locales);
|
|
1199
|
-
fonts = resolvedFonts;
|
|
1200
|
-
fontFamilies = resolvedFamilies;
|
|
1201
|
-
}
|
|
1331
|
+
const cache = await openCache(rootDir, { cacheDir: cacheDirOverride });
|
|
1202
1332
|
const succeeded = [];
|
|
1203
1333
|
const failed = [];
|
|
1204
|
-
const renderOptions = fonts.length > 0 ? {
|
|
1205
|
-
fontFamilies,
|
|
1206
|
-
fonts
|
|
1207
|
-
} : void 0;
|
|
1208
|
-
logDebug("renderOptions before loop", {
|
|
1209
|
-
fontFamilies,
|
|
1210
|
-
fontsCount: fonts.length,
|
|
1211
|
-
hasOptions: renderOptions !== void 0
|
|
1212
|
-
});
|
|
1213
1334
|
try {
|
|
1214
1335
|
for (const cell of cells) try {
|
|
1215
|
-
const
|
|
1336
|
+
const filename = generateOutputFilename({
|
|
1337
|
+
locale: cell.locale,
|
|
1338
|
+
presetId: cell.preset.id,
|
|
1339
|
+
rowKey: cell.rowKey,
|
|
1340
|
+
templateId
|
|
1341
|
+
});
|
|
1342
|
+
const result = await orchestrateRender({
|
|
1216
1343
|
cache,
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
filename: generateOutputFilename({
|
|
1220
|
-
locale: cell.locale,
|
|
1221
|
-
presetId: cell.preset.id,
|
|
1222
|
-
rowKey: cell.rowKey,
|
|
1223
|
-
templateId
|
|
1224
|
-
}),
|
|
1344
|
+
component: Component,
|
|
1345
|
+
format: "png",
|
|
1225
1346
|
height: cell.preset.height,
|
|
1347
|
+
loadLocaleData,
|
|
1226
1348
|
locale: cell.locale,
|
|
1227
|
-
outDir,
|
|
1228
1349
|
props: cell.props,
|
|
1229
|
-
|
|
1350
|
+
propsSchema: module.propsSchema,
|
|
1351
|
+
templateContentHash: contentHash,
|
|
1230
1352
|
templateId,
|
|
1231
1353
|
width: cell.preset.width
|
|
1232
1354
|
});
|
|
1355
|
+
const outputPath = `${outDir}/${filename}`;
|
|
1356
|
+
await ensureDir(outDir);
|
|
1357
|
+
await tryWriteFile(outputPath, result.bytes);
|
|
1233
1358
|
succeeded.push({
|
|
1234
1359
|
cacheHit: result.cacheHit,
|
|
1235
1360
|
locale: cell.locale,
|
|
1236
|
-
outputPath
|
|
1361
|
+
outputPath,
|
|
1237
1362
|
preset: cell.preset.id,
|
|
1238
1363
|
rowIndex: cell.rowIndex
|
|
1239
1364
|
});
|
|
@@ -1263,4 +1388,4 @@ const runBatch = async (input) => {
|
|
|
1263
1388
|
};
|
|
1264
1389
|
};
|
|
1265
1390
|
//#endregion
|
|
1266
|
-
export { AppError, ErrorCode, PropValidationError, RenderCache, compileTemplate,
|
|
1391
|
+
export { AppError, ErrorCode, PropValidationError, RenderCache, compileTemplate, importTemplateForBatch, isZodObject, loadConfig, logDebug, logError, logInfo, logWarn, measureTemplate, openCache, orchestrateMeasure, orchestrateRender, parseStudioConfig, registerFont, renderAutoSized, renderComponent, renderToPixels, resolveAssetsForLocale, resolveProjectPaths, runBatch, studioConfigSchema, validateProps, zodToJsonSchemaShape };
|
package/dist/schema.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as defineSchema, t as PropSchema } from "./validation-MigzRuji.mjs";
|
|
2
2
|
export { type PropSchema, defineSchema };
|
|
@@ -36,4 +36,4 @@ declare const validateProps: <T extends PropSchema>(schema: T, props: unknown) =
|
|
|
36
36
|
declare const isZodObject: (schema: unknown) => schema is z.ZodObject<Record<string, z.ZodTypeAny>>;
|
|
37
37
|
declare const zodToJsonSchemaShape: (schema: z.ZodTypeAny) => Record<string, unknown>;
|
|
38
38
|
//#endregion
|
|
39
|
-
export {
|
|
39
|
+
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.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Core rendering engine for stdout-design — compile TSX templates to pixel-perfect images via Takumi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"design",
|
|
@@ -49,32 +49,33 @@
|
|
|
49
49
|
"build": "tsdown",
|
|
50
50
|
"dev": "tsdown --watch",
|
|
51
51
|
"test": "bun test",
|
|
52
|
+
"test:integration": "bun test ./src/__tests__/full-pipeline.integration.ts",
|
|
52
53
|
"check-types": "tsc --noEmit",
|
|
53
54
|
"check-types:tsgo": "tsgo --noEmit",
|
|
54
55
|
"lint": "ultracite check"
|
|
55
56
|
},
|
|
56
57
|
"dependencies": {
|
|
57
|
-
"@takumi-rs/core": "^2.
|
|
58
|
-
"@takumi-rs/helpers": "^2.
|
|
58
|
+
"@takumi-rs/core": "^2.1.1",
|
|
59
|
+
"@takumi-rs/helpers": "^2.1.1",
|
|
59
60
|
"csv-parse": "^7.0.1",
|
|
60
61
|
"kysely": "^0.29.3",
|
|
61
62
|
"kysely-bun-sqlite": "^0.4.0",
|
|
62
|
-
"p-limit": "^7.
|
|
63
|
+
"p-limit": "^7.3.0",
|
|
63
64
|
"proper-lockfile": "^4.1.2",
|
|
64
65
|
"react": "^19.2.7",
|
|
65
|
-
"takumi-js": "^2.
|
|
66
|
+
"takumi-js": "^2.1.1",
|
|
66
67
|
"write-file-atomic": "^8.0.0",
|
|
67
68
|
"zod": "^4.4.3"
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
70
71
|
"@sinonjs/fake-timers": "^15.4.0",
|
|
71
72
|
"@stdout-design/config": "workspace:*",
|
|
72
|
-
"@takumi-rs/wasm": "^2.
|
|
73
|
+
"@takumi-rs/wasm": "^2.1.1",
|
|
73
74
|
"@types/proper-lockfile": "^4.1.4",
|
|
74
75
|
"@types/react": "^19.2.17",
|
|
75
76
|
"@types/sinonjs__fake-timers": "^15.0.1",
|
|
76
77
|
"@types/write-file-atomic": "^4.0.3",
|
|
77
|
-
"tsdown": "^0.22.
|
|
78
|
-
"typescript": "^7"
|
|
78
|
+
"tsdown": "^0.22.5",
|
|
79
|
+
"typescript": "^7.0.2"
|
|
79
80
|
}
|
|
80
81
|
}
|