blockprint 0.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Triantafillos Terzidis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # blockprint
2
+
3
+ Typed, config-driven block rendering for React. See the [repository README](../../README.md) for the full introduction.
4
+
5
+ ## API
6
+
7
+ ### `createRegistry(blocks)`
8
+
9
+ Builds a typed registry from a map of block-type strings to definitions. A definition is either a bare component or `{ component, schema? }`, where `schema` is any [Standard Schema](https://standardschema.dev) validator (zod ≥ 3.24, valibot, arktype, …).
10
+
11
+ When a schema is present it is the source of truth: the component receives the schema's parsed output (so schema defaults become prop defaults), and a component that cannot accept that output is a compile error on the offending key.
12
+
13
+ ### `<Renderer config registry fallback? />`
14
+
15
+ Renders a config against a registry. `config` is `unknown` on purpose — the outer shape is validated internally at a single boundary. Every entry resolves to exactly one of:
16
+
17
+ 1. known type, valid props → the component renders with parsed props;
18
+ 2. unknown type → `fallback` with `{ reason: "unknown-type" }`;
19
+ 3. schema-rejected props → `fallback` with `{ reason: "invalid-props", issues }`.
20
+
21
+ The default fallback renders a warning box in development and `null` in production.
22
+
23
+ ### `parseConfig(input)`
24
+
25
+ The boundary validator, exported for consumers who want to fail at fetch time rather than render time. Throws `ConfigError` with the entry index and reason.
26
+
27
+ ### Types
28
+
29
+ - `Registry<T>` — the registry's captured shape.
30
+ - `BlockConfig` — one untrusted config entry: `{ type: string; props?: unknown; id?: string | number }`.
31
+ - `ConfigFor<R>` — the closed, fully typed config union for a registry; for configs authored in TypeScript.
32
+ - `FallbackProps` — the fallback contract.
33
+ - `StandardSchemaV1` — re-exported for custom schema implementations.
34
+
35
+ ## Notes
36
+
37
+ - Async schemas are not supported (React render is synchronous); an async validation routes the entry to the fallback with an explanatory issue.
38
+ - Entries use `id` for React keys when present, falling back to `type-index`.
@@ -0,0 +1,9 @@
1
+ import type { ComponentType, ReactNode } from "react";
2
+ import type { BlocksShape, FallbackProps, Registry } from "./types";
3
+ export interface RendererProps<T extends BlocksShape> {
4
+ /** Untrusted input; the outer shape is validated internally. */
5
+ config: unknown;
6
+ registry: Registry<T>;
7
+ fallback?: ComponentType<FallbackProps>;
8
+ }
9
+ export declare function Renderer<T extends BlocksShape>({ config, registry, fallback, }: RendererProps<T>): ReactNode;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { parseConfig } from "./parseConfig";
3
+ import { DefaultFallback } from "./fallback";
4
+ // `memo`/`forwardRef` components are exotic objects, not functions, so the
5
+ // bare-vs-definition split must discriminate on the `component` key.
6
+ function resolveDef(def) {
7
+ if (typeof def === "object" && def !== null && "component" in def) {
8
+ return def;
9
+ }
10
+ return { component: def };
11
+ }
12
+ export function Renderer({ config, registry, fallback, }) {
13
+ const Fallback = fallback ?? DefaultFallback;
14
+ const entries = parseConfig(config);
15
+ return entries.map((entry, index) => {
16
+ const key = entry.id ?? `${entry.type}-${index}`;
17
+ if (!Object.hasOwn(registry.blocks, entry.type)) {
18
+ return (_jsx(Fallback, { reason: "unknown-type", type: entry.type, props: entry.props }, key));
19
+ }
20
+ const { component: Component, schema } = resolveDef(registry.blocks[entry.type]);
21
+ let props = entry.props;
22
+ if (schema) {
23
+ const result = schema["~standard"].validate(entry.props);
24
+ // React render is synchronous; async schemas are a config mistake and
25
+ // are contained like any other invalid block.
26
+ if (result instanceof Promise) {
27
+ return (_jsx(Fallback, { reason: "invalid-props", type: entry.type, props: entry.props, issues: [
28
+ {
29
+ message: "schema validated asynchronously; blockprint only supports synchronous schemas",
30
+ },
31
+ ] }, key));
32
+ }
33
+ if (result.issues) {
34
+ return (_jsx(Fallback, { reason: "invalid-props", type: entry.type, props: entry.props, issues: result.issues }, key));
35
+ }
36
+ props = result.value;
37
+ }
38
+ return _jsx(Component, { ...props }, key);
39
+ });
40
+ }
@@ -0,0 +1,18 @@
1
+ import type { ComponentType } from "react";
2
+ import type { StandardSchemaV1 } from "./standard-schema";
3
+ import type { BlocksShape, Registry } from "./types";
4
+ /**
5
+ * Maps every compatible entry to `unknown` (an identity when intersected) and
6
+ * every entry whose schema output is not assignable to its component's props
7
+ * to an impossible shape — so the type error lands on the offending key with
8
+ * a message naming it.
9
+ */
10
+ export type ValidateBlocks<T extends BlocksShape> = {
11
+ [K in keyof T]: T[K] extends {
12
+ component: ComponentType<infer P>;
13
+ schema: StandardSchemaV1<unknown, infer S>;
14
+ } ? [S] extends [P] ? unknown : {
15
+ schema: `schema output is not assignable to props of '${K & string}'`;
16
+ } : unknown;
17
+ };
18
+ export declare function createRegistry<T extends BlocksShape>(blocks: T & ValidateBlocks<T>): Registry<T>;
@@ -0,0 +1,3 @@
1
+ export function createRegistry(blocks) {
2
+ return { blocks: blocks };
3
+ }
@@ -0,0 +1,2 @@
1
+ import type { FallbackProps } from "./types";
2
+ export declare function DefaultFallback(props: FallbackProps): import("react").JSX.Element | null;
@@ -0,0 +1,17 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function DefaultFallback(props) {
3
+ if (process.env.NODE_ENV === "production") {
4
+ return null;
5
+ }
6
+ const detail = props.reason === "invalid-props"
7
+ ? props.issues.map((issue) => issue.message).join("; ")
8
+ : "no component registered for this type";
9
+ return (_jsxs("div", { role: "note", style: {
10
+ border: "1px dashed #c00",
11
+ borderRadius: 4,
12
+ padding: "8px 12px",
13
+ fontFamily: "monospace",
14
+ fontSize: 13,
15
+ color: "#c00",
16
+ }, children: ["blockprint: block \"", props.type, "\" not rendered (", props.reason, "):", " ", detail] }));
17
+ }
@@ -0,0 +1,6 @@
1
+ export { createRegistry } from "./createRegistry";
2
+ export { Renderer } from "./Renderer";
3
+ export type { RendererProps } from "./Renderer";
4
+ export { parseConfig, ConfigError } from "./parseConfig";
5
+ export type { BlockConfig, BlockDef, BlocksShape, ConfigFor, FallbackProps, Registry, } from "./types";
6
+ export type { StandardSchemaV1 } from "./standard-schema";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createRegistry } from "./createRegistry";
2
+ export { Renderer } from "./Renderer";
3
+ export { parseConfig, ConfigError } from "./parseConfig";
@@ -0,0 +1,10 @@
1
+ import type { BlockConfig } from "./types";
2
+ export declare class ConfigError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /**
6
+ * The single boundary where untrusted input becomes a typed config. Validates
7
+ * the outer shape only; per-block props are validated against their registered
8
+ * schema at render time.
9
+ */
10
+ export declare function parseConfig(input: unknown): BlockConfig[];
@@ -0,0 +1,29 @@
1
+ export class ConfigError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "ConfigError";
5
+ }
6
+ }
7
+ /**
8
+ * The single boundary where untrusted input becomes a typed config. Validates
9
+ * the outer shape only; per-block props are validated against their registered
10
+ * schema at render time.
11
+ */
12
+ export function parseConfig(input) {
13
+ if (!Array.isArray(input)) {
14
+ throw new ConfigError(`config must be an array, received ${input === null ? "null" : typeof input}`);
15
+ }
16
+ return input.map((entry, index) => {
17
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
18
+ throw new ConfigError(`config[${index}] must be an object`);
19
+ }
20
+ const { type, props, id } = entry;
21
+ if (typeof type !== "string" || type.length === 0) {
22
+ throw new ConfigError(`config[${index}].type must be a non-empty string`);
23
+ }
24
+ if (id !== undefined && typeof id !== "string" && typeof id !== "number") {
25
+ throw new ConfigError(`config[${index}].id must be a string or number when present`);
26
+ }
27
+ return id === undefined ? { type, props } : { type, props, id };
28
+ });
29
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The Standard Schema interface (https://standardschema.dev), vendored so
3
+ * blockprint has zero runtime and zero type-level dependencies. Any library
4
+ * implementing the spec (zod >= 3.24, valibot, arktype, ...) satisfies it.
5
+ */
6
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
7
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
8
+ }
9
+ export declare namespace StandardSchemaV1 {
10
+ interface Props<Input = unknown, Output = Input> {
11
+ readonly version: 1;
12
+ readonly vendor: string;
13
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
14
+ readonly types?: Types<Input, Output> | undefined;
15
+ }
16
+ type Result<Output> = SuccessResult<Output> | FailureResult;
17
+ interface SuccessResult<Output> {
18
+ readonly value: Output;
19
+ readonly issues?: undefined;
20
+ }
21
+ interface FailureResult {
22
+ readonly issues: ReadonlyArray<Issue>;
23
+ }
24
+ interface Issue {
25
+ readonly message: string;
26
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
27
+ }
28
+ interface PathSegment {
29
+ readonly key: PropertyKey;
30
+ }
31
+ interface Types<Input = unknown, Output = Input> {
32
+ readonly input: Input;
33
+ readonly output: Output;
34
+ }
35
+ type InferInput<S extends StandardSchemaV1> = NonNullable<S["~standard"]["types"]>["input"];
36
+ type InferOutput<S extends StandardSchemaV1> = NonNullable<S["~standard"]["types"]>["output"];
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ import type { ComponentType } from "react";
2
+ import type { StandardSchemaV1 } from "./standard-schema";
3
+ export type AnyComponent = ComponentType<any>;
4
+ export type BlockDef = AnyComponent | {
5
+ component: AnyComponent;
6
+ schema?: StandardSchemaV1<unknown, any>;
7
+ };
8
+ export type BlocksShape = Record<string, BlockDef>;
9
+ /**
10
+ * The props a definition delivers at runtime. When a schema is present it is
11
+ * the source of truth: the component receives the schema's parsed output.
12
+ */
13
+ export type PropsOfDef<D> = D extends {
14
+ schema: StandardSchemaV1<unknown, infer S>;
15
+ } ? S : D extends {
16
+ component: ComponentType<infer P>;
17
+ } ? P : D extends ComponentType<infer P> ? P : never;
18
+ export interface Registry<T extends BlocksShape> {
19
+ readonly blocks: T;
20
+ }
21
+ /**
22
+ * A single entry as it arrives from a CMS. Deliberately open: `type` is any
23
+ * string, because untrusted configs may contain types the registry has never
24
+ * seen — those route to the fallback instead of crashing.
25
+ */
26
+ export interface BlockConfig {
27
+ type: string;
28
+ props?: unknown;
29
+ id?: string | number;
30
+ }
31
+ /**
32
+ * The closed, fully typed config union for a given registry — for configs
33
+ * authored in TypeScript rather than fetched from a CMS. Block names
34
+ * autocomplete and props are checked per block.
35
+ */
36
+ export type ConfigFor<R extends Registry<BlocksShape>> = R extends Registry<infer T> ? Array<{
37
+ [K in keyof T]: {
38
+ type: K;
39
+ props: PropsOfDef<T[K]>;
40
+ id?: string | number;
41
+ };
42
+ }[keyof T]> : never;
43
+ export type FallbackProps = {
44
+ reason: "unknown-type";
45
+ type: string;
46
+ props: unknown;
47
+ } | {
48
+ reason: "invalid-props";
49
+ type: string;
50
+ props: unknown;
51
+ issues: ReadonlyArray<StandardSchemaV1.Issue>;
52
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "blockprint",
3
+ "version": "0.0.1",
4
+ "description": "Typed, config-driven block renderer for React. Render arbitrary UI trees from untrusted JSON with compile-time registry safety and runtime schema validation.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/terzidest/blockprint.git",
9
+ "directory": "packages/blockprint"
10
+ },
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "LICENSE"
21
+ ],
22
+ "sideEffects": false,
23
+ "peerDependencies": {
24
+ "react": ">=18"
25
+ },
26
+ "keywords": [
27
+ "react",
28
+ "cms",
29
+ "renderer",
30
+ "blocks",
31
+ "page-builder",
32
+ "typescript",
33
+ "standard-schema"
34
+ ],
35
+ "devDependencies": {
36
+ "@types/react": "^19.2.18",
37
+ "@types/react-dom": "^19.2.4",
38
+ "react": "^19.2.8",
39
+ "react-dom": "^19.2.8",
40
+ "typescript": "^7.0.2",
41
+ "vitest": "^4.1.10",
42
+ "zod": "^4.4.3"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.build.json",
46
+ "typecheck": "tsc",
47
+ "test": "vitest run"
48
+ }
49
+ }