@notionhq/custom-blocks 0.1.2 → 0.1.3
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/HOST.md +16 -14
- package/README.md +25 -9
- package/dist/bridge/loadManifest.d.ts +9 -8
- package/dist/bridge/loadManifest.d.ts.map +1 -1
- package/dist/bridge/loadManifest.js +6 -14
- package/dist/bridge/manifest.d.ts +3 -8
- package/dist/bridge/manifest.d.ts.map +1 -1
- package/dist/bridge/manifest.js +0 -8
- package/dist/init.d.ts +5 -6
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +19 -10
- package/dist/version.js +1 -1
- package/docs/deployment.md +41 -0
- package/docs/vite-plugin.md +5 -29
- package/package.json +1 -1
- package/src/bridge/loadManifest.ts +8 -17
- package/src/bridge/manifest.ts +3 -9
- package/src/init.ts +19 -10
- package/vite-plugin/index.d.ts +2 -2
- package/vite-plugin/index.js +8 -7
package/HOST.md
CHANGED
|
@@ -73,6 +73,8 @@ Initialization has three steps. A sandbox-generated `initializationId` identifie
|
|
|
73
73
|
{ type: "connect", initializationId, status: "success", bridgeProtocolVersion, sdkVersion, manifest: { /* ... */ } }
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
The `manifest` property is optional for `connect` messages. If provided, hosts can ignore it and still return a different `manifest` in the `init` message. This sandbox-defined manifest option is included for locally-hosted custom blocks.
|
|
77
|
+
|
|
76
78
|
If `connect.status` is `"error"`, reply with `init.error` using the same `initializationId` and forward its complete error payload unchanged.
|
|
77
79
|
|
|
78
80
|
#### 2. Host sends `init`
|
|
@@ -181,18 +183,18 @@ Schemas:
|
|
|
181
183
|
|
|
182
184
|
Messages sent from the sandbox to the host. Parse `window` `message` events with `sandboxToHostMessageSchema` (or per-message schemas). The type / schema column points to the payload shape.
|
|
183
185
|
|
|
184
|
-
| Wire type | Type / schema | Behavior
|
|
185
|
-
| -------------------- | --------------------------------------------------------- |
|
|
186
|
-
| `connect` | `ConnectMessage` / `connectMessageSchema` | Starts the one-shot handshake with a sandbox-generated `initializationId`; carries `bridgeProtocolVersion`, `sdkVersion`, and the manifest.
|
|
187
|
-
| `initResult` | `InitResultMessage` / `initResultMessageSchema` | Echoes the handshake `initializationId` and reports whether the sandbox applied `init.success`.
|
|
188
|
-
| `queryDataSource` | `QueryDataSourceMessage` / `queryDataSourceMessageSchema` | Starts or updates a `subscriptionId`-keyed subscription for rows in a raw `dataSourceId`. Later results for the same subscription replace its current value.
|
|
189
|
-
| `createPage` | `CreatePageMessage` / `createPageMessageSchema` | `requestId`-keyed page creation with properties and optional position; parent is `page_id` or `data_source_id`. Create-time icon and cover are not supported.
|
|
190
|
-
| `getPage` | `GetPageMessage` / `getPageMessageSchema` | `requestId`-keyed page fetch by page id.
|
|
191
|
-
| `updatePage` | `UpdatePageMessage` / `updatePageMessageSchema` | `requestId`-keyed patch (properties, icon, cover, or `archived`).
|
|
192
|
-
| `getUser` | `GetUserMessage` / `getUserMessageSchema` | `requestId`-keyed user fetch by user id.
|
|
193
|
-
| `listUsers` | `ListUsersMessage` / `listUsersMessageSchema` | `requestId`-keyed user list with optional `startCursor` and `pageSize`.
|
|
194
|
-
| `resize` | `ResizeMessage` / `resizeMessageSchema` | Latest measured content height from auto-resize.
|
|
195
|
-
| `invalidHostMessage` | `InvalidHostMessage` / `invalidHostMessageSchema` | Sandbox-side NACK for a host message it could not parse.
|
|
186
|
+
| Wire type | Type / schema | Behavior |
|
|
187
|
+
| -------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
188
|
+
| `connect` | `ConnectMessage` / `connectMessageSchema` | Starts the one-shot handshake with a sandbox-generated `initializationId`; carries `bridgeProtocolVersion`, `sdkVersion`, and the manifest. |
|
|
189
|
+
| `initResult` | `InitResultMessage` / `initResultMessageSchema` | Echoes the handshake `initializationId` and reports whether the sandbox applied `init.success`. |
|
|
190
|
+
| `queryDataSource` | `QueryDataSourceMessage` / `queryDataSourceMessageSchema` | Starts or updates a `subscriptionId`-keyed subscription for rows in a raw `dataSourceId`. Later results for the same subscription replace its current value. |
|
|
191
|
+
| `createPage` | `CreatePageMessage` / `createPageMessageSchema` | `requestId`-keyed page creation with properties and optional position; parent is `page_id` or `data_source_id`. Create-time icon and cover are not supported. |
|
|
192
|
+
| `getPage` | `GetPageMessage` / `getPageMessageSchema` | `requestId`-keyed page fetch by page id. |
|
|
193
|
+
| `updatePage` | `UpdatePageMessage` / `updatePageMessageSchema` | `requestId`-keyed patch (properties, icon, cover, or `archived`). |
|
|
194
|
+
| `getUser` | `GetUserMessage` / `getUserMessageSchema` | `requestId`-keyed user fetch by user id. |
|
|
195
|
+
| `listUsers` | `ListUsersMessage` / `listUsersMessageSchema` | `requestId`-keyed user list with optional `startCursor` and `pageSize`. |
|
|
196
|
+
| `resize` | `ResizeMessage` / `resizeMessageSchema` | Latest measured content height from auto-resize. |
|
|
197
|
+
| `invalidHostMessage` | `InvalidHostMessage` / `invalidHostMessageSchema` | Sandbox-side NACK for a host message it could not parse. |
|
|
196
198
|
|
|
197
199
|
`SandboxToHostMessage` / `sandboxToHostMessageSchema` is the discriminated union over all of the above.
|
|
198
200
|
|
|
@@ -209,13 +211,13 @@ Messages sent from the host to the sandbox. Same `{ wire type, type / schema, be
|
|
|
209
211
|
| `pageChanged` | `PageChangedMessage` / `pageChangedMessageSchema` | Replaces the nearest page ancestor without disturbing theme, block ID, parent, user, or query state. |
|
|
210
212
|
| `currentUserChanged` | `CurrentUserChangedMessage` / `currentUserChangedMessageSchema` | Replaces the current viewer record. Send when any viewer field changes (name, avatar, email). |
|
|
211
213
|
| `dataSourcesChanged` | `DataSourcesChangedMessage` / `dataSourcesChangedMessageSchema` | Replaces data-source bindings; the sandbox preserves cached query state for keys that still exist and drops removed keys. |
|
|
212
|
-
| `queryDataSourceResult` | `QueryDataSourceResultMessage` / `queryDataSourceResultMessageSchema` | `subscriptionId`-keyed update with `status: "success"`, `items`, and `hasMore`, or `status: "error"` and `error`.
|
|
214
|
+
| `queryDataSourceResult` | `QueryDataSourceResultMessage` / `queryDataSourceResultMessageSchema` | `subscriptionId`-keyed update with `status: "success"`, `items`, and `hasMore`, or `status: "error"` and `error`. |
|
|
213
215
|
| `createPageResult` | `CreatePageResultMessage` / `createPageResultMessageSchema` | `requestId`-keyed page response with `status: "success"` or `status: "error"`. |
|
|
214
216
|
| `getPageResult` | `GetPageResultMessage` / `getPageResultMessageSchema` | Same success/error shape as `createPageResult`. |
|
|
215
217
|
| `updatePageResult` | `UpdatePageResultMessage` / `updatePageResultMessageSchema` | Same success/error shape as `createPageResult`. |
|
|
216
218
|
| `getUserResult` | `GetUserResultMessage` / `getUserResultMessageSchema` | `requestId`-keyed user response with `status: "success"` or `status: "error"`. |
|
|
217
219
|
| `listUsersResult` | `ListUsersResultMessage` / `listUsersResultMessageSchema` | Same success/error shape as `getUserResult`. |
|
|
218
|
-
| `invalidSandboxMessage` | `InvalidSandboxMessage` / `invalidSandboxMessageSchema` | Host-side NACK for a sandbox message it could not parse.
|
|
220
|
+
| `invalidSandboxMessage` | `InvalidSandboxMessage` / `invalidSandboxMessageSchema` | Host-side NACK for a sandbox message it could not parse. |
|
|
219
221
|
|
|
220
222
|
`HostToSandboxMessage` / `hostToSandboxMessageSchema` is the discriminated union over all of the above.
|
|
221
223
|
|
package/README.md
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
# @notionhq/custom-blocks
|
|
2
2
|
|
|
3
3
|
> [!NOTE]
|
|
4
|
-
>
|
|
4
|
+
> **Pre-release.** Custom blocks are in private alpha. Breaking changes may land at any time.
|
|
5
5
|
|
|
6
6
|
SDK for building Notion custom blocks.
|
|
7
7
|
|
|
8
|
-
A custom block runs as a sandboxed `<iframe>` inside
|
|
9
|
-
|
|
10
|
-
> **Pre-release.** Breaking changes may land at any time before 1.0.
|
|
8
|
+
A custom block runs as a sandboxed `<iframe>` inside the Notion app that has no direct access to the internet. The only channel between your block and Notion is a local `postMessage` bridge. This library implements the sandbox side of the bridge protocol and wraps it in a framework-neutral TypeScript API (`@notionhq/custom-blocks`) and typed React hooks (`@notionhq/custom-blocks/react`).
|
|
11
9
|
|
|
12
10
|
## Quick start
|
|
13
11
|
|
|
12
|
+
Declare your custom block along with the rest of your worker definition:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// src/index.ts
|
|
16
|
+
import { Worker } from "@notionhq/workers";
|
|
17
|
+
|
|
18
|
+
const worker = new Worker();
|
|
19
|
+
export default worker;
|
|
20
|
+
|
|
21
|
+
worker.customBlock("hello", {
|
|
22
|
+
path: "./blocks/hello",
|
|
23
|
+
command: "npx vite build",
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
In the block's entry point defined above, wrap your React code in `<NotionCustomBlock>` so the SDK connects with Notion before anything renders:
|
|
28
|
+
|
|
14
29
|
```tsx
|
|
15
|
-
// src/index.tsx
|
|
30
|
+
// blocks/hello/src/index.tsx
|
|
16
31
|
import "@notionhq/custom-blocks/nds.css";
|
|
17
32
|
import {
|
|
18
33
|
NotionCustomBlock,
|
|
@@ -30,8 +45,10 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
|
30
45
|
);
|
|
31
46
|
```
|
|
32
47
|
|
|
48
|
+
Hooks can then read live content directly from Notion:
|
|
49
|
+
|
|
33
50
|
```tsx
|
|
34
|
-
// src/App.tsx
|
|
51
|
+
// blocks/hello/src/App.tsx
|
|
35
52
|
import { useBlockId } from "@notionhq/custom-blocks/react";
|
|
36
53
|
|
|
37
54
|
export function App() {
|
|
@@ -72,9 +89,8 @@ API surface, one page per category. Import framework-neutral APIs from `@notionh
|
|
|
72
89
|
- [`docs/pages.md`](./docs/pages.md) — `pages.create / get / update / delete`, parent variants (including the recommended `data_source_key`), property input shapes.
|
|
73
90
|
- [`docs/users.md`](./docs/users.md) — `users.list / get`, the `NotionUser` shape, paging.
|
|
74
91
|
- [`docs/errors.md`](./docs/errors.md) — request results, error format, error codes, retries, and initialization failures.
|
|
75
|
-
- [`docs/
|
|
76
|
-
|
|
77
|
-
Declare the block source and data-source schema in the worker file with `worker.customBlock(...)`, then build and deploy it with `ntn workers deploy`. The worker definition is the source of truth; the iframe frontend uses semantic keys from that definition with APIs such as `useDataSource`. For local previews, the Vite plugin serves the block's `custom_blocks.json` — see [`docs/vite-plugin.md`](./docs/vite-plugin.md).
|
|
92
|
+
- [`docs/deployment.md`](./docs/deployment.md) — worker-backed deploys, localhost self-hosted fallback, where the manifest comes from.
|
|
93
|
+
- [`docs/vite-plugin.md`](./docs/vite-plugin.md) — the `notionCustomBlock()` Vite plugin.
|
|
78
94
|
|
|
79
95
|
## Forbidden APIs
|
|
80
96
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CustomBlockErrorInfo } from "./errors.js";
|
|
2
2
|
import { type CustomBlockManifest } from "./manifest.js";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
type CustomBlockConnectErrorCode = "manifest_unavailable" | "manifest_invalid" | (string & {});
|
|
4
|
+
type CustomBlockConnectError = CustomBlockErrorInfo<CustomBlockConnectErrorCode>;
|
|
5
5
|
export type ManifestLoadResult = {
|
|
6
6
|
manifest: CustomBlockManifest | null;
|
|
7
7
|
error?: undefined;
|
|
@@ -10,11 +10,12 @@ export type ManifestLoadResult = {
|
|
|
10
10
|
error: CustomBlockConnectError;
|
|
11
11
|
};
|
|
12
12
|
/**
|
|
13
|
-
* Attempts to load
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* Attempts to load the custom block's optional self-hosted manifest, co-located with the bundle at
|
|
14
|
+
* `/custom_blocks.json`. This is to support local development where the block is served from a
|
|
15
|
+
* local dev server.
|
|
16
|
+
*
|
|
17
|
+
* It is up to the host to decide whether to use this manifest or provide its own persisted manifest.
|
|
18
18
|
*/
|
|
19
|
-
export declare function
|
|
19
|
+
export declare function attemptToLoadSelfHostedManifest(): Promise<ManifestLoadResult>;
|
|
20
|
+
export {};
|
|
20
21
|
//# sourceMappingURL=loadManifest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loadManifest.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/loadManifest.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACvD,OAAO,EAAE,KAAK,mBAAmB,EAAkB,MAAM,eAAe,CAAA;AAIxE,
|
|
1
|
+
{"version":3,"file":"loadManifest.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/loadManifest.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACvD,OAAO,EAAE,KAAK,mBAAmB,EAAkB,MAAM,eAAe,CAAA;AAIxE,KAAK,2BAA2B,GAC7B,sBAAsB,GACtB,kBAAkB,GAClB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEhB,KAAK,uBAAuB,GAAG,oBAAoB,CAAC,2BAA2B,CAAC,CAAA;AAEhF,MAAM,MAAM,kBAAkB,GAC3B;IACA,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACpC,KAAK,CAAC,EAAE,SAAS,CAAA;CAChB,GACD;IACA,QAAQ,EAAE,IAAI,CAAA;IACd,KAAK,EAAE,uBAAuB,CAAA;CAC7B,CAAA;AAEJ;;;;;;GAMG;AACH,wBAAsB,+BAA+B,IAAI,OAAO,CAAC,kBAAkB,CAAC,CA0DnF"}
|
|
@@ -2,21 +2,13 @@ import * as v from "valibot";
|
|
|
2
2
|
import { manifestSchema } from "./manifest.js";
|
|
3
3
|
const MANIFEST_URL = "custom_blocks.json";
|
|
4
4
|
/**
|
|
5
|
-
* Attempts to load
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Attempts to load the custom block's optional self-hosted manifest, co-located with the bundle at
|
|
6
|
+
* `/custom_blocks.json`. This is to support local development where the block is served from a
|
|
7
|
+
* local dev server.
|
|
8
|
+
*
|
|
9
|
+
* It is up to the host to decide whether to use this manifest or provide its own persisted manifest.
|
|
10
10
|
*/
|
|
11
|
-
export async function
|
|
12
|
-
if (typeof fetch !== "function") {
|
|
13
|
-
const message = `No fetch API available; cannot load ${MANIFEST_URL}.`;
|
|
14
|
-
console.warn(`[custom-blocks-sdk] ${message}`);
|
|
15
|
-
return {
|
|
16
|
-
manifest: null,
|
|
17
|
-
error: { code: "manifest_unavailable", message, isRetryable: true },
|
|
18
|
-
};
|
|
19
|
-
}
|
|
11
|
+
export async function attemptToLoadSelfHostedManifest() {
|
|
20
12
|
let response;
|
|
21
13
|
try {
|
|
22
14
|
response = await fetch(MANIFEST_URL, { credentials: "omit" });
|
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
|
-
/**
|
|
3
|
-
* User-authored manifest declaring the data sources the custom block expects.
|
|
4
|
-
* Lives at `custom_blocks.json` in the project root. The sandbox may send it in
|
|
5
|
-
* `connect`, and the host returns the authoritative manifest in `init`. The
|
|
6
|
-
* `notionCustomBlock` Vite plugin from
|
|
7
|
-
* `@notionhq/custom-blocks/vite` wires the JSON file into the dev server and
|
|
8
|
-
* the build output.
|
|
9
|
-
*/
|
|
10
2
|
/**
|
|
11
3
|
* Decorative icon attached to a manifest data source. Mirrors the
|
|
12
4
|
* `emoji` / `external` icon variants the public Notion API uses, so the host
|
|
@@ -62,5 +54,8 @@ export declare const manifestSchema: v.ObjectSchema<{
|
|
|
62
54
|
}, undefined>, undefined>, {}>;
|
|
63
55
|
}, undefined>, undefined>;
|
|
64
56
|
}, undefined>;
|
|
57
|
+
/**
|
|
58
|
+
* Manifest declaring the data sources required by a custom block.
|
|
59
|
+
*/
|
|
65
60
|
export type CustomBlockManifest = v.InferOutput<typeof manifestSchema>;
|
|
66
61
|
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAG5B
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAG5B;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;;;;0BAS7B,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAEnE,eAAO,MAAM,sBAAsB;;;;aAIjC,CAAA;AAEF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,sBAAsB,CAAC,CAAA;AAE3E,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;aAKnC,CAAA;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,wBAAwB,CAAC,CAAA;AAE/E,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;aAGzB,CAAA;AAEF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,cAAc,CAAC,CAAA"}
|
package/dist/bridge/manifest.js
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
2
|
import { notionPropertyTypeSchema } from "./dataSources/propertySchema.js";
|
|
3
|
-
/**
|
|
4
|
-
* User-authored manifest declaring the data sources the custom block expects.
|
|
5
|
-
* Lives at `custom_blocks.json` in the project root. The sandbox may send it in
|
|
6
|
-
* `connect`, and the host returns the authoritative manifest in `init`. The
|
|
7
|
-
* `notionCustomBlock` Vite plugin from
|
|
8
|
-
* `@notionhq/custom-blocks/vite` wires the JSON file into the dev server and
|
|
9
|
-
* the build output.
|
|
10
|
-
*/
|
|
11
3
|
/**
|
|
12
4
|
* Decorative icon attached to a manifest data source. Mirrors the
|
|
13
5
|
* `emoji` / `external` icon variants the public Notion API uses, so the host
|
package/dist/init.d.ts
CHANGED
|
@@ -41,15 +41,14 @@ export type InitCustomBlockOptions = {
|
|
|
41
41
|
timeoutMs?: number;
|
|
42
42
|
};
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
* `connect`, then awaits the host's `init` message. Resolves after the sandbox
|
|
46
|
-
* applies the payload and returns `initResult.success`.
|
|
44
|
+
* Initializes the custom block by running the SDK <-> host handshake.
|
|
47
45
|
*
|
|
48
|
-
*
|
|
46
|
+
* Resolves with the block's initial context (theme, block location, current
|
|
47
|
+
* user, data sources) once the host has initialized the block. Rejects with a
|
|
48
|
+
* `CustomBlockInitializationError` if initialization fails.
|
|
49
49
|
*
|
|
50
50
|
* Idempotent: subsequent calls return the same promise as the first and ignore any new options.
|
|
51
|
-
* Mount your React tree (or call any SDK hook
|
|
52
|
-
* returned promise resolves.
|
|
51
|
+
* Mount your React tree (or call any SDK hook) only after the returned promise resolves.
|
|
53
52
|
*/
|
|
54
53
|
export declare function initCustomBlock(opts?: InitCustomBlockOptions): Promise<CustomBlockInitPayload>;
|
|
55
54
|
//# sourceMappingURL=init.d.ts.map
|
package/dist/init.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAA;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,EAAE,8BAA8B,EAAE,MAAM,2BAA2B,CAAA;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAGxD;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACpC,KAAK,EAAE,WAAW,CAAA;IAClB,YAAY,EAAE,kBAAkB,CAAA;IAChC,OAAO,EAAE,aAAa,CAAA;IACtB,MAAM,EAAE,YAAY,CAAA;IACpB,IAAI,EAAE,eAAe,CAAA;IACrB,WAAW,EAAE,UAAU,CAAA;IACvB,WAAW,EAAE,gBAAgB,EAAE,CAAA;CAC/B,CAAA;AAED;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,8BAA8B;gBACvD,OAAO,GAAE,MAA8B;IAS3C,IAAI,EAAE,eAAe,CAAA;CAC7B;AAED;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACpC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AASD
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAA;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,EAAE,8BAA8B,EAAE,MAAM,2BAA2B,CAAA;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAGxD;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACpC,KAAK,EAAE,WAAW,CAAA;IAClB,YAAY,EAAE,kBAAkB,CAAA;IAChC,OAAO,EAAE,aAAa,CAAA;IACtB,MAAM,EAAE,YAAY,CAAA;IACpB,IAAI,EAAE,eAAe,CAAA;IACrB,WAAW,EAAE,UAAU,CAAA;IACvB,WAAW,EAAE,gBAAgB,EAAE,CAAA;CAC/B,CAAA;AAED;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,8BAA8B;gBACvD,OAAO,GAAE,MAA8B;IAS3C,IAAI,EAAE,eAAe,CAAA;CAC7B;AAED;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACpC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AASD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC9B,IAAI,GAAE,sBAA2B,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAKjC"}
|
package/dist/init.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { attemptToLoadSelfHostedManifest } from "./bridge/loadManifest.js";
|
|
2
2
|
import { CustomBlockInitializationError } from "./bridge/messages/init.js";
|
|
3
3
|
import { customBlockHost } from "./bridge/sandboxClient.js";
|
|
4
4
|
import { unreachable } from "./utils.js";
|
|
@@ -22,23 +22,32 @@ const DEFAULT_INIT_TIMEOUT_MS = 15000;
|
|
|
22
22
|
const NOT_IN_IFRAME_MESSAGE = "<NotionCustomBlock> only works inside an iframe — use the dev shell or deploy to Notion.";
|
|
23
23
|
let initPromise;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
* `connect`, then awaits the host's `init` message. Resolves after the sandbox
|
|
27
|
-
* applies the payload and returns `initResult.success`.
|
|
25
|
+
* Initializes the custom block by running the SDK <-> host handshake.
|
|
28
26
|
*
|
|
29
|
-
*
|
|
27
|
+
* Resolves with the block's initial context (theme, block location, current
|
|
28
|
+
* user, data sources) once the host has initialized the block. Rejects with a
|
|
29
|
+
* `CustomBlockInitializationError` if initialization fails.
|
|
30
30
|
*
|
|
31
31
|
* Idempotent: subsequent calls return the same promise as the first and ignore any new options.
|
|
32
|
-
* Mount your React tree (or call any SDK hook
|
|
33
|
-
* returned promise resolves.
|
|
32
|
+
* Mount your React tree (or call any SDK hook) only after the returned promise resolves.
|
|
34
33
|
*/
|
|
35
34
|
export function initCustomBlock(opts = {}) {
|
|
36
35
|
if (initPromise === undefined) {
|
|
37
|
-
initPromise =
|
|
36
|
+
initPromise = performHandshake(opts);
|
|
38
37
|
}
|
|
39
38
|
return initPromise;
|
|
40
39
|
}
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Performs the host <-> sandbox SDK handshake:
|
|
42
|
+
* 1. Attempts to load an optional self-hosted `custom_blocks.json`
|
|
43
|
+
* 2. Sends `connect` with the manifest
|
|
44
|
+
* 3. Awaits the host's `init` message
|
|
45
|
+
* 4. Applies the `init` payload and acknowledges with `initResult` (fire-and-forget)
|
|
46
|
+
*
|
|
47
|
+
* Resolves with the `init` payload after the process above. Rejects with a
|
|
48
|
+
* `CustomBlockInitializationError` if any step fails.
|
|
49
|
+
*/
|
|
50
|
+
async function performHandshake(opts) {
|
|
42
51
|
try {
|
|
43
52
|
// Fail fast with a typed error when rendered as a standalone tab and not in a parent frame.
|
|
44
53
|
// Otherwise, it would eventually hit the timeout, since `postMessage` to `window.parent`
|
|
@@ -47,7 +56,7 @@ async function initializeCustomBlock(opts) {
|
|
|
47
56
|
throw new NotInIframeError();
|
|
48
57
|
}
|
|
49
58
|
// Load the manifest and send it to the host.
|
|
50
|
-
const manifestResult = await
|
|
59
|
+
const manifestResult = await attemptToLoadSelfHostedManifest();
|
|
51
60
|
customBlockHost.sendConnect(manifestResult);
|
|
52
61
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS;
|
|
53
62
|
await awaitHostInitWithTimeout(timeoutMs);
|
package/dist/version.js
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Deployment and manifests
|
|
2
|
+
|
|
3
|
+
A custom block is a static web bundle — an `index.html` plus optional assets — that Notion loads into a sandboxed `<iframe>`, so the bundle needs to be hosted somewhere Notion can reach. Notion does that hosting: the block ships as part of a worker. Declare the block source and data-source schema with `worker.customBlock(...)` in the worker's `src/index.ts`, deploy with `ntn workers deploy`, and Notion serves the built bundle. Serving the bundle from your own dev server is provided as a local-development fallback, not an officially supported deployment target.
|
|
4
|
+
|
|
5
|
+
The difference between the two paths is where the manifest lives:
|
|
6
|
+
|
|
7
|
+
- **Worker-backed** — deploying persists the manifest on the block's definition record in Notion. The host reads it from there. The bundle carries no manifest.
|
|
8
|
+
- **Self-hosted (localhost)** — there is no persisted record, so the block serves its manifest dynamically at `/custom_blocks.json` and passes the manifest to the host in the `connect` handshake.
|
|
9
|
+
|
|
10
|
+
For a self-hosted block, the sandbox can propose the initial manifest in `connect`. The host decides whether to use it and sends the authoritative manifest back in `init`. The SDK always uses the host-provided manifest from `init` even if it differs from the one sent during `connect`.
|
|
11
|
+
|
|
12
|
+
## Self-hosted blocks (localhost fallback)
|
|
13
|
+
|
|
14
|
+
During development, a Notion host can point a custom block at a local dev server instead of a deployed worker. The block describes itself with an optional `custom_blocks.json` at the bundle root:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"version": 1,
|
|
19
|
+
"dataSources": {
|
|
20
|
+
"tasks": {
|
|
21
|
+
"name": "Tasks",
|
|
22
|
+
"description": "The collection of tasks to render",
|
|
23
|
+
"properties": {
|
|
24
|
+
"title": { "name": "Title", "type": "title" },
|
|
25
|
+
"dueDate": { "name": "Due date", "type": "date" }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
At startup the SDK fetches this file from the bundle's origin and forwards it in `connect`. If the file is missing, the SDK omits `manifest` from `connect`. If it is present but unreadable or invalid, initialization fails.
|
|
33
|
+
|
|
34
|
+
The [Vite plugin](./vite-plugin.md) keeps this file working in dev and emits it into `dist/` on build.
|
|
35
|
+
|
|
36
|
+
## Types
|
|
37
|
+
|
|
38
|
+
- `CustomBlockManifest` — the parsed shape of a block manifest.
|
|
39
|
+
- `ManifestDataSource` — a single entry in `dataSources`.
|
|
40
|
+
- `ManifestProperty` — a single property declaration inside a `ManifestDataSource`.
|
|
41
|
+
- `ManifestIcon` — the icon variant accepted on a `ManifestDataSource`.
|
package/docs/vite-plugin.md
CHANGED
|
@@ -12,35 +12,11 @@ export default defineConfig({
|
|
|
12
12
|
});
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
It does two things:
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
- **Forces `base: "./"`** (and rejects any other explicit value). Custom blocks are served from a content-addressed path, so asset URLs in the built bundle must be relative.
|
|
18
|
+
- **Passes through `custom_blocks.json`** when the optional file exists in the project root: served by middleware in dev so HMR and the SDK handshake see the same file, and emitted into `dist/` as a separate asset on `vite build`.
|
|
18
19
|
|
|
19
|
-
`custom_blocks.json`
|
|
20
|
+
The `custom_blocks.json` manifest file only applies when a block is served from `localhost` during development instead of deployed as a worker. Worker-backed blocks declare their manifest with `worker.customBlock(...)` and should not maintain a `custom_blocks.json`, but they still use the plugin for the `base` enforcement. See [deployment.md](./deployment.md) for more information.
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
"version": 1,
|
|
24
|
-
"dataSources": {
|
|
25
|
-
"tasks": {
|
|
26
|
-
"name": "Tasks",
|
|
27
|
-
"description": "The collection of tasks to render",
|
|
28
|
-
"properties": {
|
|
29
|
-
"title": { "name": "Title", "type": "title" },
|
|
30
|
-
"dueDate": { "name": "Due date", "type": "date" }
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
For a worker-backed block, the `worker.customBlock(...)` declaration in the worker's `src/index.ts` is the source of truth at deploy time. But local previews (pasting the Vite dev URL into a live Notion block, or the dev shell) don't read the worker declaration — the SDK fetches `custom_blocks.json` from the plugin and forwards it on `connect`, and the host binds data sources against those keys. Keep the file mirroring the worker's `dataSources` schema so previews see the same slots the deployed block will; leave it as `{ "version": 1, "dataSources": {} }` if the block reads no host data.
|
|
38
|
-
|
|
39
|
-
If the file is missing, the SDK omits `manifest` from `connect`. If it is unreadable or invalid, the SDK sends `connect` with `status: "error"` and an `error` payload. The host returns its authoritative manifest in `init`, and the SDK uses that one even when it differs from what was sent in `connect`.
|
|
40
|
-
|
|
41
|
-
## Types
|
|
42
|
-
|
|
43
|
-
- `CustomBlockManifest` — the parsed shape of `custom_blocks.json`.
|
|
44
|
-
- `ManifestDataSource` — a single entry in `dataSources` (name, description, properties).
|
|
45
|
-
- `ManifestProperty` — a single property declaration inside a `ManifestDataSource`.
|
|
46
|
-
- `ManifestIcon` — the icon variant accepted on a `ManifestDataSource`.
|
|
22
|
+
The plugin does not validate the manifest. The SDK validates it on receipt.
|
package/package.json
CHANGED
|
@@ -4,13 +4,12 @@ import { type CustomBlockManifest, manifestSchema } from "./manifest.js"
|
|
|
4
4
|
|
|
5
5
|
const MANIFEST_URL = "custom_blocks.json"
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
type CustomBlockConnectErrorCode =
|
|
8
8
|
| "manifest_unavailable"
|
|
9
9
|
| "manifest_invalid"
|
|
10
10
|
| (string & {})
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
CustomBlockErrorInfo<CustomBlockConnectErrorCode>
|
|
12
|
+
type CustomBlockConnectError = CustomBlockErrorInfo<CustomBlockConnectErrorCode>
|
|
14
13
|
|
|
15
14
|
export type ManifestLoadResult =
|
|
16
15
|
| {
|
|
@@ -23,21 +22,13 @@ export type ManifestLoadResult =
|
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
|
-
* Attempts to load
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
25
|
+
* Attempts to load the custom block's optional self-hosted manifest, co-located with the bundle at
|
|
26
|
+
* `/custom_blocks.json`. This is to support local development where the block is served from a
|
|
27
|
+
* local dev server.
|
|
28
|
+
*
|
|
29
|
+
* It is up to the host to decide whether to use this manifest or provide its own persisted manifest.
|
|
31
30
|
*/
|
|
32
|
-
export async function
|
|
33
|
-
if (typeof fetch !== "function") {
|
|
34
|
-
const message = `No fetch API available; cannot load ${MANIFEST_URL}.`
|
|
35
|
-
console.warn(`[custom-blocks-sdk] ${message}`)
|
|
36
|
-
return {
|
|
37
|
-
manifest: null,
|
|
38
|
-
error: { code: "manifest_unavailable", message, isRetryable: true },
|
|
39
|
-
}
|
|
40
|
-
}
|
|
31
|
+
export async function attemptToLoadSelfHostedManifest(): Promise<ManifestLoadResult> {
|
|
41
32
|
let response: Response
|
|
42
33
|
try {
|
|
43
34
|
response = await fetch(MANIFEST_URL, { credentials: "omit" })
|
package/src/bridge/manifest.ts
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import * as v from "valibot"
|
|
2
2
|
import { notionPropertyTypeSchema } from "./dataSources/propertySchema.js"
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* User-authored manifest declaring the data sources the custom block expects.
|
|
6
|
-
* Lives at `custom_blocks.json` in the project root. The sandbox may send it in
|
|
7
|
-
* `connect`, and the host returns the authoritative manifest in `init`. The
|
|
8
|
-
* `notionCustomBlock` Vite plugin from
|
|
9
|
-
* `@notionhq/custom-blocks/vite` wires the JSON file into the dev server and
|
|
10
|
-
* the build output.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
4
|
/**
|
|
14
5
|
* Decorative icon attached to a manifest data source. Mirrors the
|
|
15
6
|
* `emoji` / `external` icon variants the public Notion API uses, so the host
|
|
@@ -50,4 +41,7 @@ export const manifestSchema = v.object({
|
|
|
50
41
|
dataSources: v.record(v.string(), manifestDataSourceSchema),
|
|
51
42
|
})
|
|
52
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Manifest declaring the data sources required by a custom block.
|
|
46
|
+
*/
|
|
53
47
|
export type CustomBlockManifest = v.InferOutput<typeof manifestSchema>
|
package/src/init.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NotionContrastMode } from "./bridge/contrast.js"
|
|
2
2
|
import type { NotionDataSource } from "./bridge/dataSources/dataSource.js"
|
|
3
3
|
import type { NotionBlockId } from "./bridge/ids.js"
|
|
4
|
-
import {
|
|
4
|
+
import { attemptToLoadSelfHostedManifest } from "./bridge/loadManifest.js"
|
|
5
5
|
import { CustomBlockInitializationError } from "./bridge/messages/init.js"
|
|
6
6
|
import type { CustomBlockPage } from "./bridge/pages/page.js"
|
|
7
7
|
import type { NotionParent } from "./bridge/parent.js"
|
|
@@ -63,26 +63,35 @@ const NOT_IN_IFRAME_MESSAGE =
|
|
|
63
63
|
let initPromise: Promise<CustomBlockInitPayload> | undefined
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
67
|
-
* `connect`, then awaits the host's `init` message. Resolves after the sandbox
|
|
68
|
-
* applies the payload and returns `initResult.success`.
|
|
66
|
+
* Initializes the custom block by running the SDK <-> host handshake.
|
|
69
67
|
*
|
|
70
|
-
*
|
|
68
|
+
* Resolves with the block's initial context (theme, block location, current
|
|
69
|
+
* user, data sources) once the host has initialized the block. Rejects with a
|
|
70
|
+
* `CustomBlockInitializationError` if initialization fails.
|
|
71
71
|
*
|
|
72
72
|
* Idempotent: subsequent calls return the same promise as the first and ignore any new options.
|
|
73
|
-
* Mount your React tree (or call any SDK hook
|
|
74
|
-
* returned promise resolves.
|
|
73
|
+
* Mount your React tree (or call any SDK hook) only after the returned promise resolves.
|
|
75
74
|
*/
|
|
76
75
|
export function initCustomBlock(
|
|
77
76
|
opts: InitCustomBlockOptions = {},
|
|
78
77
|
): Promise<CustomBlockInitPayload> {
|
|
79
78
|
if (initPromise === undefined) {
|
|
80
|
-
initPromise =
|
|
79
|
+
initPromise = performHandshake(opts)
|
|
81
80
|
}
|
|
82
81
|
return initPromise
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Performs the host <-> sandbox SDK handshake:
|
|
86
|
+
* 1. Attempts to load an optional self-hosted `custom_blocks.json`
|
|
87
|
+
* 2. Sends `connect` with the manifest
|
|
88
|
+
* 3. Awaits the host's `init` message
|
|
89
|
+
* 4. Applies the `init` payload and acknowledges with `initResult` (fire-and-forget)
|
|
90
|
+
*
|
|
91
|
+
* Resolves with the `init` payload after the process above. Rejects with a
|
|
92
|
+
* `CustomBlockInitializationError` if any step fails.
|
|
93
|
+
*/
|
|
94
|
+
async function performHandshake(
|
|
86
95
|
opts: InitCustomBlockOptions,
|
|
87
96
|
): Promise<CustomBlockInitPayload> {
|
|
88
97
|
try {
|
|
@@ -94,7 +103,7 @@ async function initializeCustomBlock(
|
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
// Load the manifest and send it to the host.
|
|
97
|
-
const manifestResult = await
|
|
106
|
+
const manifestResult = await attemptToLoadSelfHostedManifest()
|
|
98
107
|
customBlockHost.sendConnect(manifestResult)
|
|
99
108
|
|
|
100
109
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS
|
package/vite-plugin/index.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export type NotionCustomBlockPlugin = {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Vite plugin that serves
|
|
44
|
-
* bundle
|
|
43
|
+
* Vite plugin that serves an optional self-hosted `custom_blocks.json` to the custom block
|
|
44
|
+
* bundle during local development and emits it as a separate asset on build.
|
|
45
45
|
*/
|
|
46
46
|
export function notionCustomBlock(): NotionCustomBlockPlugin
|
package/vite-plugin/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Vite plugin half of `@notionhq/custom-blocks`. Exposes
|
|
3
|
-
* `custom_blocks.json` manifest to the custom-block bundle so the
|
|
4
|
-
* fetch can resolve.
|
|
2
|
+
* Vite plugin half of `@notionhq/custom-blocks`. Exposes an optional
|
|
3
|
+
* self-hosted `custom_blocks.json` manifest to the custom-block bundle so the
|
|
4
|
+
* SDK's runtime fetch can resolve.
|
|
5
5
|
*
|
|
6
6
|
* Authored as plain JS (with `index.d.ts` co-located for types) — the rest of
|
|
7
7
|
* the SDK is `.ts` consumed directly via the workspace symlink, but Vite's
|
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
* Vite's config loader uses Node's ESM resolver (not CommonJS `require`,
|
|
22
22
|
* which fails for ESM-only deps).
|
|
23
23
|
*
|
|
24
|
-
* - **Dev (`vite`)** — middleware serves `/custom_blocks.json` from the project root
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* - **Dev (`vite`)** — middleware serves `/custom_blocks.json` from the project root
|
|
25
|
+
* when the optional file exists.
|
|
26
|
+
* - **Build (`vite build`)** — emits an existing `custom_blocks.json` as an asset
|
|
27
|
+
* alongside the built `index.html` so `vite preview` and any local file server
|
|
28
|
+
* can serve it.
|
|
28
29
|
*
|
|
29
30
|
* Validation is intentionally not performed here — the SDK validates on
|
|
30
31
|
* receipt, and the CLI validates separately at deploy time.
|