@stdout-design/core 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ import { i as defineSchema, t as PropSchema } from "./validation-BUNZnc-5.mjs";
2
+ export { type PropSchema, defineSchema };
@@ -0,0 +1,2 @@
1
+ import { n as defineSchema } from "./validation-CMTs74Vm.mjs";
2
+ export { defineSchema };
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ //#region src/shared/validation.d.ts
3
+ /**
4
+ * Single source of truth for the prop-schema type. Previously declared
5
+ * identically in both `props.ts` and `validation.ts` -- consolidated here
6
+ * so there is exactly one place this type can drift from zod's own types.
7
+ */
8
+ type PropSchema = z.ZodObject<Record<string, z.ZodTypeAny>>;
9
+ /** Identity helper purely for inference ergonomics at the template definition site. */
10
+ declare const defineSchema: <T extends PropSchema>(schema: T) => T;
11
+ interface PropValidationIssue {
12
+ field: string;
13
+ expected: string;
14
+ received: string;
15
+ message: string;
16
+ code: string;
17
+ suggestion?: string;
18
+ }
19
+ declare class PropValidationError extends Error {
20
+ readonly issues: PropValidationIssue[];
21
+ constructor(issues: PropValidationIssue[]);
22
+ }
23
+ /**
24
+ * Validates props against a template's zod schema, reshaping any failure
25
+ * into a structured, agent-consumable error rather than letting a raw
26
+ * ZodError (verbose, deeply nested, not designed for programmatic
27
+ * consumption) escape to CLI/HTTP/MCP callers.
28
+ *
29
+ * This is the ONLY validation entry point in the package -- it is the
30
+ * function `index.ts` re-exports at the package root, and every downstream
31
+ * consumer (CLI exit-code reporting, MCP "error strings returned to LLM",
32
+ * dev-server field-level error boundaries) is expected to catch
33
+ * `PropValidationError` and read `.issues`, not a raw `ZodError`.
34
+ */
35
+ declare const validateProps: <T extends PropSchema>(schema: T, props: unknown) => z.infer<T>;
36
+ declare const isZodObject: (schema: unknown) => schema is z.ZodObject<Record<string, z.ZodTypeAny>>;
37
+ declare const zodToJsonSchemaShape: (schema: z.ZodTypeAny) => Record<string, unknown>;
38
+ //#endregion
39
+ export { isZodObject as a, defineSchema as i, PropValidationError as n, validateProps as o, PropValidationIssue as r, zodToJsonSchemaShape as s, PropSchema as t };
@@ -0,0 +1,202 @@
1
+ import { z } from "zod";
2
+ //#region src/shared/validation.ts
3
+ /** Identity helper purely for inference ergonomics at the template definition site. */
4
+ const defineSchema = (schema) => schema;
5
+ var PropValidationError = class extends Error {
6
+ issues;
7
+ constructor(issues) {
8
+ super(`Invalid props: ${issues.map((i) => `${i.field || "(root)"} - ${i.message}${i.suggestion ? `. Suggestion: ${i.suggestion}` : ""}`).join("; ")}`);
9
+ this.name = "PropValidationError";
10
+ this.issues = issues;
11
+ }
12
+ };
13
+ const isObject = (schema) => schema.type === "object";
14
+ const isArray = (schema) => schema.type === "array";
15
+ const isEnum = (schema) => schema.type === "enum";
16
+ const isUnion = (schema) => schema.type === "union";
17
+ const isZodString = (schema) => schema.type === "string";
18
+ const isWrappable = (schema) => schema.type === "default" || schema.type === "optional" || schema.type === "nullable";
19
+ const resolveFieldSchema = (schema, path) => {
20
+ let current = schema;
21
+ for (let i = 0; i < path.length; i += 1) {
22
+ const segment = path[i];
23
+ if (!(i === path.length - 1) && isWrappable(current)) current = current.unwrap();
24
+ if (typeof segment === "number") {
25
+ if (isArray(current)) {
26
+ current = current.element;
27
+ continue;
28
+ }
29
+ return;
30
+ }
31
+ if (isObject(current)) {
32
+ const fieldType = current.shape[segment];
33
+ if (fieldType) {
34
+ current = fieldType;
35
+ continue;
36
+ }
37
+ }
38
+ return;
39
+ }
40
+ return current;
41
+ };
42
+ const primitiveLabels = {
43
+ any: "any value",
44
+ bigint: "bigint",
45
+ boolean: "boolean",
46
+ date: "date",
47
+ never: "never",
48
+ null: "null",
49
+ number: "number",
50
+ string: "string",
51
+ symbol: "symbol",
52
+ undefined: "undefined",
53
+ unknown: "unknown value",
54
+ void: "void"
55
+ };
56
+ const describeType = (schema) => {
57
+ const { type } = schema;
58
+ if (isWrappable(schema)) {
59
+ const desc = describeType(schema.unwrap());
60
+ if (type === "optional") return `${desc} (optional)`;
61
+ if (type === "nullable") return `${desc} | null`;
62
+ return desc;
63
+ }
64
+ if (type in primitiveLabels) return primitiveLabels[type];
65
+ if (isObject(schema)) return `object (${Object.entries(schema.shape).map(([key, val]) => {
66
+ const inner = isWrappable(val) ? val.unwrap() : val;
67
+ return `${key}: ${describeType(inner)}`;
68
+ }).join(", ")})`;
69
+ if (isArray(schema)) return `array of ${describeType(schema.element)}`;
70
+ if (isEnum(schema)) return `one of: ${schema.options.join(", ")}`;
71
+ if (isUnion(schema)) return `one of: ${schema.options.map((o) => {
72
+ const inner = isWrappable(o) ? o.unwrap() : o;
73
+ return describeType(inner);
74
+ }).join(" | ")}`;
75
+ if (schema.type === "literal") {
76
+ const val = schema.value;
77
+ return JSON.stringify(val ?? "?");
78
+ }
79
+ if (schema.type === "record") return "object (record)";
80
+ if (isZodString(schema)) {
81
+ if (schema.format === "url") return "URL string";
82
+ if (schema.format === "email") return "email string";
83
+ if (schema.format === "uuid") return "UUID string";
84
+ if (schema.minLength !== null) return `string (min ${schema.minLength} characters)`;
85
+ return "string";
86
+ }
87
+ return type;
88
+ };
89
+ const getObjectShapeKeys = (schema) => {
90
+ const inner = isWrappable(schema) ? schema.unwrap() : schema;
91
+ if (!isObject(inner)) return;
92
+ return Object.keys(inner.shape);
93
+ };
94
+ const handleInvalidType = (issue, fieldSchema) => {
95
+ const received = "received" in issue ? String(issue.received) : "unknown";
96
+ if (isWrappable(fieldSchema)) {
97
+ const inner = fieldSchema.unwrap();
98
+ return `expected ${describeType(inner)} but received ${received} — this field has a default value configured, ensure props are merged correctly before validation`;
99
+ }
100
+ if (isObject(fieldSchema) || isWrappable(fieldSchema) && isObject(fieldSchema.unwrap())) {
101
+ const keys = getObjectShapeKeys(fieldSchema);
102
+ return `expected object${keys ? ` with properties (${keys.join(", ")})` : ""} but received ${received} — add .default({}) to make this field optional`;
103
+ }
104
+ if (isArray(fieldSchema)) return `expected ${describeType(fieldSchema)} but received ${received} — add .default([]) to make this field optional`;
105
+ return issue.message;
106
+ };
107
+ const handleInvalidFormat = (issue, field) => {
108
+ const { format: fmt } = issue;
109
+ if (fmt === "url") return `invalid URL format for field '${field}' — expected a valid URL (e.g., https://example.com/image.png)`;
110
+ if (fmt === "email") return `invalid email format for field '${field}' — expected a valid email address`;
111
+ return issue.message;
112
+ };
113
+ const handleInvalidValue = (issue) => {
114
+ const { values } = issue;
115
+ return `expected one of: ${values?.join(", ") ?? "unknown"}`;
116
+ };
117
+ const handleTooSmall = (issue) => {
118
+ const { minimum: min, origin } = issue;
119
+ if (min !== null) {
120
+ let suffix = "";
121
+ if (origin === "string") suffix = "character(s)";
122
+ else if (origin === "array") suffix = "item(s)";
123
+ return `must be at least ${min} ${suffix}`;
124
+ }
125
+ return issue.message;
126
+ };
127
+ const enhanceMessage = (issue, fieldSchema) => {
128
+ const field = issue.path.join(".");
129
+ if (!fieldSchema) return issue.message;
130
+ switch (issue.code) {
131
+ case "invalid_type": return handleInvalidType(issue, fieldSchema);
132
+ case "invalid_format": return handleInvalidFormat(issue, field);
133
+ case "invalid_value": return handleInvalidValue(issue);
134
+ case "too_small": return handleTooSmall(issue);
135
+ case "too_big": return issue.message;
136
+ case "unrecognized_keys": {
137
+ const { keys } = issue;
138
+ return `unexpected field '${keys?.join(", ") ?? field}' — not recognized by the schema, check for typos or remove it`;
139
+ }
140
+ case "invalid_union": {
141
+ const inner = isWrappable(fieldSchema) ? fieldSchema.unwrap() : fieldSchema;
142
+ if (isUnion(inner)) return `did not match any valid type — expected ${describeType(inner)}`;
143
+ return issue.message;
144
+ }
145
+ default: return issue.message;
146
+ }
147
+ };
148
+ const getSuggestion = (issue, fieldSchema) => {
149
+ if (!fieldSchema) return;
150
+ if (issue.code === "invalid_type") {
151
+ if (isObject(fieldSchema)) return "add .default({}) to make this object optional";
152
+ if (isArray(fieldSchema)) return "add .default([]) to make this array optional";
153
+ if (isWrappable(fieldSchema)) return "ensure props are merged correctly before validation";
154
+ }
155
+ if (issue.code === "invalid_value") {
156
+ const { values } = issue;
157
+ if (values) return `use one of: ${values.join(", ")}`;
158
+ }
159
+ if (issue.code === "invalid_format") {
160
+ const { format: fmt } = issue;
161
+ if (fmt === "url") return "provide a valid URL starting with http:// or https://";
162
+ if (fmt === "email") return "provide a valid email address";
163
+ }
164
+ };
165
+ /**
166
+ * Validates props against a template's zod schema, reshaping any failure
167
+ * into a structured, agent-consumable error rather than letting a raw
168
+ * ZodError (verbose, deeply nested, not designed for programmatic
169
+ * consumption) escape to CLI/HTTP/MCP callers.
170
+ *
171
+ * This is the ONLY validation entry point in the package -- it is the
172
+ * function `index.ts` re-exports at the package root, and every downstream
173
+ * consumer (CLI exit-code reporting, MCP "error strings returned to LLM",
174
+ * dev-server field-level error boundaries) is expected to catch
175
+ * `PropValidationError` and read `.issues`, not a raw `ZodError`.
176
+ */
177
+ const validateProps = (schema, props) => {
178
+ const result = schema.safeParse(props);
179
+ if (result.success) return result.data;
180
+ throw new PropValidationError(result.error.issues.map((issue) => {
181
+ const path = issue.path;
182
+ const fieldSchema = resolveFieldSchema(schema, path);
183
+ return {
184
+ code: String(issue.code),
185
+ expected: "expected" in issue ? String(issue.expected) : "unknown",
186
+ field: path.join("."),
187
+ message: enhanceMessage(issue, fieldSchema),
188
+ received: "received" in issue ? String(issue.received) : "unknown",
189
+ suggestion: getSuggestion(issue, fieldSchema)
190
+ };
191
+ }));
192
+ };
193
+ const isZodObject = (schema) => {
194
+ if (typeof schema !== "object" || schema === null) return false;
195
+ return schema.type === "object";
196
+ };
197
+ const zodToJsonSchemaShape = (schema) => {
198
+ if (typeof schema !== "object" || !schema || !("shape" in schema)) return {};
199
+ return z.toJSONSchema(schema);
200
+ };
201
+ //#endregion
202
+ export { zodToJsonSchemaShape as a, validateProps as i, defineSchema as n, isZodObject as r, PropValidationError as t };
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@stdout-design/core",
3
+ "version": "0.1.1",
4
+ "description": "Core rendering engine for stdout-design — compile TSX templates to pixel-perfect images via Takumi",
5
+ "keywords": [
6
+ "design",
7
+ "image-generation",
8
+ "rendering",
9
+ "social-media",
10
+ "takumi",
11
+ "tsx"
12
+ ],
13
+ "homepage": "https://github.com/m7md1alaa/stdout-design#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/m7md1alaa/stdout-design/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": "Mohammed alaa",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/m7md1alaa/stdout-design.git"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "type": "module",
27
+ "exports": {
28
+ ".": {
29
+ "development": {
30
+ "import": "./src/index.ts",
31
+ "types": "./src/index.ts"
32
+ },
33
+ "import": "./dist/index.mjs",
34
+ "types": "./dist/index.d.mts"
35
+ },
36
+ "./schema": {
37
+ "development": {
38
+ "import": "./src/schema.ts",
39
+ "types": "./src/schema.ts"
40
+ },
41
+ "import": "./dist/schema.mjs",
42
+ "types": "./dist/schema.d.mts"
43
+ }
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "scripts": {
49
+ "build": "tsdown",
50
+ "dev": "tsdown --watch",
51
+ "test": "bun test",
52
+ "check-types": "tsc --noEmit",
53
+ "check-types:tsgo": "tsgo --noEmit",
54
+ "lint": "ultracite check"
55
+ },
56
+ "dependencies": {
57
+ "@takumi-rs/core": "^2.0.0",
58
+ "@takumi-rs/helpers": "^2.0.0",
59
+ "csv-parse": "^7.0.1",
60
+ "kysely": "^0.29.3",
61
+ "kysely-bun-sqlite": "^0.4.0",
62
+ "p-limit": "^7.0.0",
63
+ "proper-lockfile": "^4.1.2",
64
+ "react": "^19.2.7",
65
+ "takumi-js": "^2.0.0",
66
+ "write-file-atomic": "^8.0.0",
67
+ "zod": "^4.4.3"
68
+ },
69
+ "devDependencies": {
70
+ "@sinonjs/fake-timers": "^15.4.0",
71
+ "@stdout-design/config": "workspace:*",
72
+ "@takumi-rs/wasm": "^2.0.0",
73
+ "@types/proper-lockfile": "^4.1.4",
74
+ "@types/react": "^19.2.17",
75
+ "@types/sinonjs__fake-timers": "^15.0.1",
76
+ "@types/write-file-atomic": "^4.0.3",
77
+ "tsdown": "^0.22.4",
78
+ "typescript": "^7"
79
+ }
80
+ }