blockprint 0.1.0 → 0.2.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/README.md +80 -7
- package/dist/Renderer.d.ts +1 -1
- package/dist/Renderer.js +2 -2
- package/dist/chunk-4JB5QQUK.js +37 -0
- package/dist/chunk-4JB5QQUK.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -0
- package/dist/createRegistry.d.ts +2 -2
- package/dist/fallback.d.ts +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -0
- package/dist/parseConfig.d.ts +1 -1
- package/dist/types.d.ts +12 -1
- package/package.json +14 -1
package/README.md
CHANGED
|
@@ -1,16 +1,52 @@
|
|
|
1
1
|
# blockprint
|
|
2
2
|
|
|
3
|
-
Typed, config-driven block rendering for React
|
|
3
|
+
**Typed, config-driven block rendering for React.** Render arbitrary UI trees from CMS-controlled JSON — with compile-time safety between your components and the config, and runtime validation at the boundary.
|
|
4
|
+
|
|
5
|
+
**Live demo: [blockprint.terzidest.com](https://blockprint.terzidest.com)** — three completely different pages (commerce, SaaS, editorial) from one renderer and one registry, plus a deliberately broken config showing graceful fallback. · [GitHub](https://github.com/terzidest/blockprint)
|
|
6
|
+
|
|
7
|
+
## The problem
|
|
8
|
+
|
|
9
|
+
Content teams compose pages from a fixed set of block types; your app receives JSON like `[{ "type": "hero", "props": { … } }, …]`. Consuming that in React usually collapses into a `switch` on `block.type` — every block imported into every page, props typed `any`, unknown types crashing the page.
|
|
10
|
+
|
|
11
|
+
blockprint inverts it: you define the blocks and their prop types, register them once, and render any config — including configs containing block types or malformed props you've never seen.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { createRegistry, Renderer } from "blockprint";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
const HeroBanner = ({ title, subtitle }: { title: string; subtitle?: string }) => (
|
|
20
|
+
<header><h1>{title}</h1>{subtitle && <p>{subtitle}</p>}</header>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const registry = createRegistry({
|
|
24
|
+
hero: HeroBanner, // compile-time trust
|
|
25
|
+
gameGrid: { // runtime guarantee
|
|
26
|
+
component: GameGrid,
|
|
27
|
+
schema: z.object({ games: z.array(gameSchema) }),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// config typically arrives from a CMS at runtime
|
|
32
|
+
<Renderer config={pageConfig} registry={registry} />;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- A registered component whose props don't match its schema is a **compile error on that key**, with a message naming the block — not a runtime crash.
|
|
36
|
+
- Unknown block types and schema-rejected props render a configurable **fallback**; the rest of the page renders normally.
|
|
37
|
+
- Configs authored in TypeScript get block-name autocomplete and per-block prop checking via `ConfigFor<typeof registry>`.
|
|
38
|
+
- **Zero runtime dependencies.** Schemas are typed against [Standard Schema](https://standardschema.dev), so zod (≥ 3.24), valibot, and arktype all work — blockprint depends on none of them.
|
|
39
|
+
- Framework-agnostic: React only. No Next.js/Remix imports; the library ships the mechanism, not the content.
|
|
4
40
|
|
|
5
41
|
## API
|
|
6
42
|
|
|
7
43
|
### `createRegistry(blocks)`
|
|
8
44
|
|
|
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
|
|
45
|
+
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 validator.
|
|
10
46
|
|
|
11
47
|
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
48
|
|
|
13
|
-
### `<Renderer config registry fallback? />`
|
|
49
|
+
### `<Renderer config registry fallback? wrapBlock? />`
|
|
14
50
|
|
|
15
51
|
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
52
|
|
|
@@ -18,7 +54,7 @@ Renders a config against a registry. `config` is `unknown` on purpose — the ou
|
|
|
18
54
|
2. unknown type → `fallback` with `{ reason: "unknown-type" }`;
|
|
19
55
|
3. schema-rejected props → `fallback` with `{ reason: "invalid-props", issues }`.
|
|
20
56
|
|
|
21
|
-
The default fallback renders a warning box in development and `null` in production
|
|
57
|
+
The default fallback renders a warning box in development and `null` in production — a page that wants visible failure in production must pass its own `fallback`.
|
|
22
58
|
|
|
23
59
|
The optional `wrapBlock={(node, entry, index) => …}` prop wraps every rendered entry — blocks and fallbacks alike — with access to the config entry and its index. This is the hook point for visual-editing overlays, analytics boundaries, or per-block chrome.
|
|
24
60
|
|
|
@@ -26,15 +62,52 @@ The optional `wrapBlock={(node, entry, index) => …}` prop wraps every rendered
|
|
|
26
62
|
|
|
27
63
|
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.
|
|
28
64
|
|
|
65
|
+
### `blockprint/config` — the React-free entry point
|
|
66
|
+
|
|
67
|
+
Backends that store and validate configs want exactly `parseConfig` and nothing React-shaped. The `blockprint/config` subpath exports `parseConfig`, `ConfigError`, and the `BlockConfig` / `StandardSchemaV1` types with **no React requirement** (the React peer dependency is marked optional):
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// in a Node server — no react in node_modules required
|
|
71
|
+
import { parseConfig, ConfigError } from "blockprint/config";
|
|
72
|
+
|
|
73
|
+
app.put("/pages/:slug", (req, res) => {
|
|
74
|
+
const config = parseConfig(req.body); // throws ConfigError with entry index
|
|
75
|
+
…
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Validate structure on the server, validate props at render time in the client — the registry schemas are the real trust boundary, so the server never needs to know the blocks.
|
|
80
|
+
|
|
29
81
|
### Types
|
|
30
82
|
|
|
31
83
|
- `Registry<T>` — the registry's captured shape.
|
|
32
84
|
- `BlockConfig` — one untrusted config entry: `{ type: string; props?: unknown; id?: string | number }`.
|
|
33
|
-
- `ConfigFor<R>` — the closed, fully typed config union for a registry
|
|
85
|
+
- `ConfigFor<R>` — the closed, fully typed config union for a registry, for configs authored in TypeScript. Typed as each schema's *input*, so fields with defaults are optional to write.
|
|
86
|
+
- `PropsFor<R, K>` / `InputFor<R, K>` — one block type's rendered props (schema output) / authorable props (schema input). For building editors and tooling against a registry.
|
|
34
87
|
- `FallbackProps` — the fallback contract.
|
|
35
88
|
- `StandardSchemaV1` — re-exported for custom schema implementations.
|
|
36
89
|
|
|
37
|
-
##
|
|
90
|
+
## Sharing schemas with an editor app
|
|
91
|
+
|
|
92
|
+
An admin/editor app usually can't import the rendering app's registry — that would drag every block component (and its styling, routing, data hooks) into the editor bundle. The pattern that works:
|
|
93
|
+
|
|
94
|
+
1. Put the **zod schemas in a shared package** (`packages/blocks-contract`), importable by both apps.
|
|
95
|
+
2. The rendering app joins schemas with components at the edge: `createRegistry({ hero: { component: Hero, schema: heroSchema } })`.
|
|
96
|
+
3. The editor imports only the schemas (labels, defaults, field shapes) and `blockprint/config` for structure — never the components.
|
|
97
|
+
|
|
98
|
+
`PropsFor`/`InputFor` give the editor the exact prop types per block without any runtime import of the registry.
|
|
38
99
|
|
|
39
|
-
|
|
100
|
+
## Config storage notes
|
|
101
|
+
|
|
102
|
+
- **Entry-level key order is irrelevant** — the array order is the only order that matters. Storage layers that canonicalize JSON (e.g. Postgres `jsonb` reorders object keys) are fully compatible; don't be alarmed when a round-tripped config diffs at the key level.
|
|
103
|
+
- `parseConfig` strips unknown entry-level keys (only `type`, `props`, `id` survive) and deliberately allows unknown `type` values — unknown block types are the renderer's fallback case, which is what makes configs forward-compatible across deploys.
|
|
104
|
+
|
|
105
|
+
## Gotchas
|
|
106
|
+
|
|
107
|
+
- **`exactOptionalPropertyTypes` + zod's `.optional()`**: with that compiler flag enabled, a schema field like `z.string().optional()` produces `prop?: string | undefined`, which is not assignable to a component prop declared `prop?: string` — so the registry's compile-time check (correctly) rejects the registration. Either declare such component props as `prop?: string | undefined`, or leave the flag off in the consuming app.
|
|
108
|
+
- **Async schemas are not supported** (React render is synchronous); an async validation routes the entry to the fallback with an explanatory issue.
|
|
40
109
|
- Entries use `id` for React keys when present, falling back to `type-index`.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT © [Triantafillos Terzidis](https://github.com/terzidest)
|
package/dist/Renderer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
|
-
import type { BlockConfig, BlocksShape, FallbackProps, Registry } from "./types";
|
|
2
|
+
import type { BlockConfig, BlocksShape, FallbackProps, Registry } from "./types.ts";
|
|
3
3
|
export interface RendererProps<T extends BlocksShape> {
|
|
4
4
|
/** Untrusted input; the outer shape is validated internally. */
|
|
5
5
|
config: unknown;
|
package/dist/Renderer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment } from "react";
|
|
3
|
-
import { parseConfig } from "./parseConfig";
|
|
4
|
-
import { DefaultFallback } from "./fallback";
|
|
3
|
+
import { parseConfig } from "./parseConfig.js";
|
|
4
|
+
import { DefaultFallback } from "./fallback.js";
|
|
5
5
|
// `memo`/`forwardRef` components are exotic objects, not functions, so the
|
|
6
6
|
// bare-vs-definition split must discriminate on the `component` key.
|
|
7
7
|
function resolveDef(def) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/parseConfig.ts
|
|
2
|
+
var ConfigError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "ConfigError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
function parseConfig(input) {
|
|
9
|
+
if (!Array.isArray(input)) {
|
|
10
|
+
throw new ConfigError(
|
|
11
|
+
`config must be an array, received ${input === null ? "null" : typeof input}`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return input.map((entry, index) => {
|
|
15
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
16
|
+
throw new ConfigError(`config[${index}] must be an object`);
|
|
17
|
+
}
|
|
18
|
+
const { type, props, id } = entry;
|
|
19
|
+
if (typeof type !== "string" || type.length === 0) {
|
|
20
|
+
throw new ConfigError(
|
|
21
|
+
`config[${index}].type must be a non-empty string`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if (id !== void 0 && typeof id !== "string" && typeof id !== "number") {
|
|
25
|
+
throw new ConfigError(
|
|
26
|
+
`config[${index}].id must be a string or number when present`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return id === void 0 ? { type, props } : { type, props, id };
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
ConfigError,
|
|
35
|
+
parseConfig
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=chunk-4JB5QQUK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/parseConfig.ts"],"sourcesContent":["import type { BlockConfig } from \"./types\";\n\nexport class ConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ConfigError\";\n }\n}\n\n/**\n * The single boundary where untrusted input becomes a typed config. Validates\n * the outer shape only; per-block props are validated against their registered\n * schema at render time.\n */\nexport function parseConfig(input: unknown): BlockConfig[] {\n if (!Array.isArray(input)) {\n throw new ConfigError(\n `config must be an array, received ${input === null ? \"null\" : typeof input}`,\n );\n }\n\n return input.map((entry, index) => {\n if (typeof entry !== \"object\" || entry === null || Array.isArray(entry)) {\n throw new ConfigError(`config[${index}] must be an object`);\n }\n const { type, props, id } = entry as Record<string, unknown>;\n if (typeof type !== \"string\" || type.length === 0) {\n throw new ConfigError(\n `config[${index}].type must be a non-empty string`,\n );\n }\n if (id !== undefined && typeof id !== \"string\" && typeof id !== \"number\") {\n throw new ConfigError(\n `config[${index}].id must be a string or number when present`,\n );\n }\n return id === undefined ? { type, props } : { type, props, id };\n });\n}\n"],"mappings":";AAEO,IAAM,cAAN,cAA0B,MAAM;AAAA,EACrC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAOO,SAAS,YAAY,OAA+B;AACzD,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,UAAM,IAAI;AAAA,MACR,qCAAqC,UAAU,OAAO,SAAS,OAAO,KAAK;AAAA,IAC7E;AAAA,EACF;AAEA,SAAO,MAAM,IAAI,CAAC,OAAO,UAAU;AACjC,QAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG;AACvE,YAAM,IAAI,YAAY,UAAU,KAAK,qBAAqB;AAAA,IAC5D;AACA,UAAM,EAAE,MAAM,OAAO,GAAG,IAAI;AAC5B,QAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAAG;AACjD,YAAM,IAAI;AAAA,QACR,UAAU,KAAK;AAAA,MACjB;AAAA,IACF;AACA,QAAI,OAAO,UAAa,OAAO,OAAO,YAAY,OAAO,OAAO,UAAU;AACxE,YAAM,IAAI;AAAA,QACR,UAAU,KAAK;AAAA,MACjB;AAAA,IACF;AACA,WAAO,OAAO,SAAY,EAAE,MAAM,MAAM,IAAI,EAAE,MAAM,OAAO,GAAG;AAAA,EAChE,CAAC;AACH;","names":[]}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React-free entry point (`blockprint/config`): the boundary validator and
|
|
3
|
+
* config types, importable from Node backends without a React peer. Keep
|
|
4
|
+
* this module (and its imports) free of React — servers validating stored
|
|
5
|
+
* configs want exactly this and nothing more.
|
|
6
|
+
*/
|
|
7
|
+
export { parseConfig, ConfigError } from "./parseConfig.ts";
|
|
8
|
+
export type { BlockConfig } from "./types.ts";
|
|
9
|
+
export type { StandardSchemaV1 } from "./standard-schema.ts";
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React-free entry point (`blockprint/config`): the boundary validator and
|
|
3
|
+
* config types, importable from Node backends without a React peer. Keep
|
|
4
|
+
* this module (and its imports) free of React — servers validating stored
|
|
5
|
+
* configs want exactly this and nothing more.
|
|
6
|
+
*/
|
|
7
|
+
export { parseConfig, ConfigError } from "./parseConfig.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/createRegistry.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentType } from "react";
|
|
2
|
-
import type { StandardSchemaV1 } from "./standard-schema";
|
|
3
|
-
import type { BlocksShape, Registry } from "./types";
|
|
2
|
+
import type { StandardSchemaV1 } from "./standard-schema.ts";
|
|
3
|
+
import type { BlocksShape, Registry } from "./types.ts";
|
|
4
4
|
/**
|
|
5
5
|
* Maps every compatible entry to `unknown` (an identity when intersected) and
|
|
6
6
|
* every entry whose schema output is not assignable to its component's props
|
package/dist/fallback.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { FallbackProps } from "./types";
|
|
1
|
+
import type { FallbackProps } from "./types.ts";
|
|
2
2
|
export declare function DefaultFallback(props: FallbackProps): import("react").JSX.Element | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +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";
|
|
1
|
+
export { createRegistry } from "./createRegistry.ts";
|
|
2
|
+
export { Renderer } from "./Renderer.tsx";
|
|
3
|
+
export type { RendererProps } from "./Renderer.tsx";
|
|
4
|
+
export { parseConfig, ConfigError } from "./parseConfig.ts";
|
|
5
|
+
export type { BlockConfig, BlockDef, BlocksShape, ConfigFor, FallbackProps, InputFor, PropsFor, Registry, } from "./types.ts";
|
|
6
|
+
export type { StandardSchemaV1 } from "./standard-schema.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { createRegistry } from "./createRegistry";
|
|
2
|
-
export { Renderer } from "./Renderer";
|
|
3
|
-
export { parseConfig, ConfigError } from "./parseConfig";
|
|
1
|
+
export { createRegistry } from "./createRegistry.js";
|
|
2
|
+
export { Renderer } from "./Renderer.js";
|
|
3
|
+
export { parseConfig, ConfigError } from "./parseConfig.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/createRegistry.ts","../src/Renderer.tsx","../src/fallback.tsx"],"sourcesContent":["import type { ComponentType } from \"react\";\nimport type { StandardSchemaV1 } from \"./standard-schema\";\nimport type { BlocksShape, Registry } from \"./types\";\n\n/**\n * Maps every compatible entry to `unknown` (an identity when intersected) and\n * every entry whose schema output is not assignable to its component's props\n * to an impossible shape — so the type error lands on the offending key with\n * a message naming it.\n */\nexport type ValidateBlocks<T extends BlocksShape> = {\n [K in keyof T]: T[K] extends {\n component: ComponentType<infer P>;\n schema: StandardSchemaV1<unknown, infer S>;\n }\n ? [S] extends [P]\n ? unknown\n : {\n schema: `schema output is not assignable to props of '${K & string}'`;\n }\n : unknown;\n};\n\nexport function createRegistry<T extends BlocksShape>(\n blocks: T & ValidateBlocks<T>,\n): Registry<T> {\n return { blocks: blocks as T };\n}\n","import { Fragment } from \"react\";\nimport type { ComponentType, ReactNode } from \"react\";\nimport type { StandardSchemaV1 } from \"./standard-schema\";\nimport type {\n AnyComponent,\n BlockConfig,\n BlocksShape,\n FallbackProps,\n Registry,\n} from \"./types\";\nimport { parseConfig } from \"./parseConfig\";\nimport { DefaultFallback } from \"./fallback\";\n\nexport interface RendererProps<T extends BlocksShape> {\n /** Untrusted input; the outer shape is validated internally. */\n config: unknown;\n registry: Registry<T>;\n fallback?: ComponentType<FallbackProps>;\n /**\n * Wraps every rendered entry — blocks and fallbacks alike — with access to\n * the config entry and its index. The hook point for visual-editing\n * overlays, analytics boundaries, or per-block chrome.\n */\n wrapBlock?: (node: ReactNode, entry: BlockConfig, index: number) => ReactNode;\n}\n\ninterface ResolvedDef {\n component: AnyComponent;\n schema?: StandardSchemaV1<unknown, any>;\n}\n\n// `memo`/`forwardRef` components are exotic objects, not functions, so the\n// bare-vs-definition split must discriminate on the `component` key.\nfunction resolveDef(def: BlocksShape[string]): ResolvedDef {\n if (typeof def === \"object\" && def !== null && \"component\" in def) {\n return def;\n }\n return { component: def };\n}\n\nexport function Renderer<T extends BlocksShape>({\n config,\n registry,\n fallback,\n wrapBlock,\n}: RendererProps<T>): ReactNode {\n const Fallback = fallback ?? DefaultFallback;\n const entries = parseConfig(config);\n\n return entries.map((entry, index) => {\n const key = entry.id ?? `${entry.type}-${index}`;\n const node = renderEntry(entry, registry, Fallback);\n // The key lives on a Fragment so it survives whatever wrapBlock returns.\n return (\n <Fragment key={key}>\n {wrapBlock ? wrapBlock(node, entry, index) : node}\n </Fragment>\n );\n });\n}\n\nfunction renderEntry(\n entry: BlockConfig,\n registry: Registry<BlocksShape>,\n Fallback: ComponentType<FallbackProps>,\n): ReactNode {\n if (!Object.hasOwn(registry.blocks, entry.type)) {\n return (\n <Fallback reason=\"unknown-type\" type={entry.type} props={entry.props} />\n );\n }\n\n const { component: Component, schema } = resolveDef(\n registry.blocks[entry.type] as BlocksShape[string],\n );\n\n let props = entry.props;\n if (schema) {\n const result = schema[\"~standard\"].validate(entry.props);\n // React render is synchronous; async schemas are a config mistake and\n // are contained like any other invalid block.\n if (result instanceof Promise) {\n return (\n <Fallback\n reason=\"invalid-props\"\n type={entry.type}\n props={entry.props}\n issues={[\n {\n message:\n \"schema validated asynchronously; blockprint only supports synchronous schemas\",\n },\n ]}\n />\n );\n }\n if (result.issues) {\n return (\n <Fallback\n reason=\"invalid-props\"\n type={entry.type}\n props={entry.props}\n issues={result.issues}\n />\n );\n }\n props = result.value;\n }\n\n return <Component {...(props as object | undefined)} />;\n}\n","import type { FallbackProps } from \"./types\";\n\n// Ambient declaration instead of @types/node: bundlers replace\n// process.env.NODE_ENV statically, and the library has no other Node surface.\ndeclare const process: { env: { NODE_ENV?: string } };\n\nexport function DefaultFallback(props: FallbackProps) {\n if (process.env.NODE_ENV === \"production\") {\n return null;\n }\n\n const detail =\n props.reason === \"invalid-props\"\n ? props.issues.map((issue) => issue.message).join(\"; \")\n : \"no component registered for this type\";\n\n return (\n <div\n role=\"note\"\n style={{\n border: \"1px dashed #c00\",\n borderRadius: 4,\n padding: \"8px 12px\",\n fontFamily: \"monospace\",\n fontSize: 13,\n color: \"#c00\",\n }}\n >\n blockprint: block "{props.type}" not rendered ({props.reason}):{\" \"}\n {detail}\n </div>\n );\n}\n"],"mappings":";;;;;;AAuBO,SAAS,eACd,QACa;AACb,SAAO,EAAE,OAAoB;AAC/B;;;AC3BA,SAAS,gBAAgB;;;ACiBrB;AAXG,SAAS,gBAAgB,OAAsB;AACpD,MAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,WAAO;AAAA,EACT;AAEA,QAAM,SACJ,MAAM,WAAW,kBACb,MAAM,OAAO,IAAI,CAAC,UAAU,MAAM,OAAO,EAAE,KAAK,IAAI,IACpD;AAEN,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,OAAO;AAAA,QACL,QAAQ;AAAA,QACR,cAAc;AAAA,QACd,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,UAAU;AAAA,QACV,OAAO;AAAA,MACT;AAAA,MACD;AAAA;AAAA,QAC0B,MAAM;AAAA,QAAK;AAAA,QAAsB,MAAM;AAAA,QAAO;AAAA,QAAG;AAAA,QACzE;AAAA;AAAA;AAAA,EACH;AAEJ;;;ADsBM;AArBN,SAAS,WAAW,KAAuC;AACzD,MAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,eAAe,KAAK;AACjE,WAAO;AAAA,EACT;AACA,SAAO,EAAE,WAAW,IAAI;AAC1B;AAEO,SAAS,SAAgC;AAAA,EAC9C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAgC;AAC9B,QAAM,WAAW,YAAY;AAC7B,QAAM,UAAU,YAAY,MAAM;AAElC,SAAO,QAAQ,IAAI,CAAC,OAAO,UAAU;AACnC,UAAM,MAAM,MAAM,MAAM,GAAG,MAAM,IAAI,IAAI,KAAK;AAC9C,UAAM,OAAO,YAAY,OAAO,UAAU,QAAQ;AAElD,WACE,oBAAC,YACE,sBAAY,UAAU,MAAM,OAAO,KAAK,IAAI,QADhC,GAEf;AAAA,EAEJ,CAAC;AACH;AAEA,SAAS,YACP,OACA,UACA,UACW;AACX,MAAI,CAAC,OAAO,OAAO,SAAS,QAAQ,MAAM,IAAI,GAAG;AAC/C,WACE,oBAAC,YAAS,QAAO,gBAAe,MAAM,MAAM,MAAM,OAAO,MAAM,OAAO;AAAA,EAE1E;AAEA,QAAM,EAAE,WAAW,WAAW,OAAO,IAAI;AAAA,IACvC,SAAS,OAAO,MAAM,IAAI;AAAA,EAC5B;AAEA,MAAI,QAAQ,MAAM;AAClB,MAAI,QAAQ;AACV,UAAM,SAAS,OAAO,WAAW,EAAE,SAAS,MAAM,KAAK;AAGvD,QAAI,kBAAkB,SAAS;AAC7B,aACE;AAAA,QAAC;AAAA;AAAA,UACC,QAAO;AAAA,UACP,MAAM,MAAM;AAAA,UACZ,OAAO,MAAM;AAAA,UACb,QAAQ;AAAA,YACN;AAAA,cACE,SACE;AAAA,YACJ;AAAA,UACF;AAAA;AAAA,MACF;AAAA,IAEJ;AACA,QAAI,OAAO,QAAQ;AACjB,aACE;AAAA,QAAC;AAAA;AAAA,UACC,QAAO;AAAA,UACP,MAAM,MAAM;AAAA,UACZ,OAAO,MAAM;AAAA,UACb,QAAQ,OAAO;AAAA;AAAA,MACjB;AAAA,IAEJ;AACA,YAAQ,OAAO;AAAA,EACjB;AAEA,SAAO,oBAAC,aAAW,GAAI,OAA8B;AACvD;","names":[]}
|
package/dist/parseConfig.d.ts
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentType } from "react";
|
|
2
|
-
import type { StandardSchemaV1 } from "./standard-schema";
|
|
2
|
+
import type { StandardSchemaV1 } from "./standard-schema.ts";
|
|
3
3
|
export type AnyComponent = ComponentType<any>;
|
|
4
4
|
export type BlockDef = AnyComponent | {
|
|
5
5
|
component: AnyComponent;
|
|
@@ -49,6 +49,17 @@ export type ConfigFor<R extends Registry<BlocksShape>> = R extends Registry<infe
|
|
|
49
49
|
id?: string | number;
|
|
50
50
|
};
|
|
51
51
|
}[keyof T]> : never;
|
|
52
|
+
/**
|
|
53
|
+
* The props a given block type's component receives at render time (schema
|
|
54
|
+
* OUTPUT when a schema exists). For building editors and tooling against a
|
|
55
|
+
* registry without importing its components' modules at runtime.
|
|
56
|
+
*/
|
|
57
|
+
export type PropsFor<R extends Registry<BlocksShape>, K extends keyof R["blocks"]> = PropsOfDef<R["blocks"][K]>;
|
|
58
|
+
/**
|
|
59
|
+
* The props a given block type accepts when authoring a config (schema INPUT
|
|
60
|
+
* when a schema exists — defaulted fields optional).
|
|
61
|
+
*/
|
|
62
|
+
export type InputFor<R extends Registry<BlocksShape>, K extends keyof R["blocks"]> = InputOfDef<R["blocks"][K]>;
|
|
52
63
|
export type FallbackProps = {
|
|
53
64
|
reason: "unknown-type";
|
|
54
65
|
type: string;
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blockprint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
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
5
|
"license": "MIT",
|
|
6
|
+
"homepage": "https://blockprint.terzidest.com",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/terzidest/blockprint/issues"
|
|
9
|
+
},
|
|
6
10
|
"repository": {
|
|
7
11
|
"type": "git",
|
|
8
12
|
"url": "git+https://github.com/terzidest/blockprint.git",
|
|
@@ -13,6 +17,10 @@
|
|
|
13
17
|
".": {
|
|
14
18
|
"types": "./dist/index.d.ts",
|
|
15
19
|
"import": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./config": {
|
|
22
|
+
"types": "./dist/config.d.ts",
|
|
23
|
+
"import": "./dist/config.js"
|
|
16
24
|
}
|
|
17
25
|
},
|
|
18
26
|
"files": [
|
|
@@ -41,6 +49,11 @@
|
|
|
41
49
|
"vitest": "^4.1.10",
|
|
42
50
|
"zod": "^4.4.3"
|
|
43
51
|
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"react": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
44
57
|
"scripts": {
|
|
45
58
|
"build": "tsc -p tsconfig.build.json",
|
|
46
59
|
"typecheck": "tsc",
|