@notionhq/custom-blocks 0.0.78 → 0.1.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/HOST.md +9 -2
- package/README.md +4 -2
- package/dist/bridge/SandboxBridge.d.ts +1 -1
- package/dist/bridge/SandboxBridge.js +1 -1
- package/dist/bridge/messages/connect.d.ts +1 -1
- package/dist/bridge/messages/connect.d.ts.map +1 -1
- package/dist/bridge/messages/connect.js +2 -8
- package/dist/bridge/messages/sandboxToHost.d.ts +1 -1
- package/dist/host/createCustomBlockHost.d.ts +1 -1
- package/dist/host/createCustomBlockHost.d.ts.map +1 -1
- package/dist/host/createCustomBlockHost.js +17 -4
- package/dist/host/lifecycle/{ready.d.ts → protocolVersion.d.ts} +1 -1
- package/dist/host/lifecycle/protocolVersion.d.ts.map +1 -0
- package/dist/version.js +1 -1
- package/docs/data-sources.md +7 -5
- package/docs/vite-plugin.md +46 -0
- package/package.json +1 -2
- package/src/bridge/SandboxBridge.ts +1 -1
- package/src/bridge/messages/connect.ts +2 -9
- package/src/host/createCustomBlockHost.ts +25 -5
- package/bin/cli/attach.js +0 -180
- package/bin/cli/cli.js +0 -248
- package/bin/cli/create.js +0 -106
- package/bin/cli/datasources.js +0 -255
- package/bin/cli/deploy.js +0 -109
- package/bin/cli/ids.js +0 -13
- package/bin/cli/ntn.js +0 -95
- package/bin/cli/pullData.js +0 -76
- package/bin/cli/pullManifest.js +0 -158
- package/bin/cli/target.js +0 -95
- package/bin/src/bridge/dataSources/propertySchema.js +0 -148
- package/bin/src/bridge/manifest.js +0 -40
- package/dist/host/lifecycle/ready.d.ts.map +0 -1
- package/docs/manifest.md +0 -42
- /package/dist/host/lifecycle/{ready.js → protocolVersion.js} +0 -0
- /package/src/host/lifecycle/{ready.ts → protocolVersion.ts} +0 -0
package/HOST.md
CHANGED
|
@@ -25,6 +25,7 @@ import { createCustomBlockHost } from "@notionhq/custom-blocks/host";
|
|
|
25
25
|
|
|
26
26
|
const host = createCustomBlockHost({
|
|
27
27
|
iframe,
|
|
28
|
+
minBridgeProtocolVersion: 3,
|
|
28
29
|
initialState: {
|
|
29
30
|
theme,
|
|
30
31
|
blockId,
|
|
@@ -35,14 +36,20 @@ const host = createCustomBlockHost({
|
|
|
35
36
|
dataSources: { bindings },
|
|
36
37
|
},
|
|
37
38
|
handlers: {
|
|
38
|
-
queryDataSource: async (message) => ({
|
|
39
|
+
queryDataSource: async (message) => ({
|
|
40
|
+
status: "success",
|
|
41
|
+
items: [],
|
|
42
|
+
hasMore: false,
|
|
43
|
+
}),
|
|
39
44
|
},
|
|
40
45
|
});
|
|
41
46
|
```
|
|
42
47
|
|
|
43
48
|
Use it when the generic protocol loop is helpful. Hosts with more specialized runtime needs can keep using the schemas and message types directly.
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
Responses return a `status`-discriminated result: `"success"` carries `items` and required `hasMore`; `"error"` carries the structured `error`. Set `hasMore` to `false` when no additional results are available.
|
|
51
|
+
|
|
52
|
+
Related exports: `CustomBlockHostOptions`, `CustomBlockHostInitialState`, `CustomBlockHostInitialDataSources`, `CustomBlockHostDataSourcesPayload`, `ConnectSuccessMessage`, `CustomBlockHostHandlers`, `CustomBlockHostHandle`, `CustomBlockHostLogDirection`, `CustomBlockHostQueryDataSourceResult`, `CustomBlockHostCreatePageResult`, `CustomBlockHostGetPageResult`, `CustomBlockHostUpdatePageResult`, `CustomBlockHostGetUserResult`, and `CustomBlockHostListUsersResult`.
|
|
46
53
|
|
|
47
54
|
## Bridge protocol
|
|
48
55
|
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ export function App() {
|
|
|
35
35
|
}
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
`<NotionCustomBlock>` runs the SDK ↔ host handshake (
|
|
38
|
+
`<NotionCustomBlock>` runs the SDK ↔ host handshake (`connect` → `init` → `initResult`) and only mounts `children` once it resolves. Inside the wrapper, every hook returns non-nullable values — there's no separate gating component to write. It also runs `useCustomBlockAutoResize` for you by default; pass `autoResize={false}` to opt out.
|
|
39
39
|
|
|
40
40
|
## Reference
|
|
41
41
|
|
|
@@ -47,7 +47,9 @@ API surface, one page per category. Import framework-neutral APIs from `@notionh
|
|
|
47
47
|
- [`docs/pages.md`](./docs/pages.md) — `pages.create / get / update / delete`, parent variants (including the recommended `data_source_key`), property input shapes.
|
|
48
48
|
- [`docs/users.md`](./docs/users.md) — `users.list / get`, the `NotionUser` shape, paging.
|
|
49
49
|
- [`docs/errors.md`](./docs/errors.md) — request results, error format, error codes, retries, and initialization failures.
|
|
50
|
-
- [`docs/
|
|
50
|
+
- [`docs/vite-plugin.md`](./docs/vite-plugin.md) — the `notionCustomBlock()` Vite plugin and `custom_blocks.json`, the block's local-preview manifest.
|
|
51
|
+
|
|
52
|
+
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).
|
|
51
53
|
|
|
52
54
|
## Forbidden APIs
|
|
53
55
|
|
|
@@ -9,7 +9,7 @@ import { type InitMessage } from "./messages/init.js";
|
|
|
9
9
|
* single host needs to support multiple custom blocks built with different versions of the bridge
|
|
10
10
|
* protocol. Increment this number any time a breaking change is made to the bridge protocol.
|
|
11
11
|
*/
|
|
12
|
-
export declare const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION =
|
|
12
|
+
export declare const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 3;
|
|
13
13
|
/**
|
|
14
14
|
* A single entry in the bridge message log. Kept intentionally plain so the log
|
|
15
15
|
* is copy-pasteable to a local coding agent without needing extra context.
|
|
@@ -13,7 +13,7 @@ import { PendingRequests } from "./pendingRequests.js";
|
|
|
13
13
|
* single host needs to support multiple custom blocks built with different versions of the bridge
|
|
14
14
|
* protocol. Increment this number any time a breaking change is made to the bridge protocol.
|
|
15
15
|
*/
|
|
16
|
-
export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION =
|
|
16
|
+
export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 3;
|
|
17
17
|
export class SandboxBridge {
|
|
18
18
|
constructor() {
|
|
19
19
|
this.hostState = {
|
|
@@ -41,7 +41,7 @@ export declare const connectMessageSchema: v.VariantSchema<"status", [v.ObjectSc
|
|
|
41
41
|
readonly error: v.ObjectSchema<{
|
|
42
42
|
readonly code: v.StringSchema<undefined>;
|
|
43
43
|
readonly message: v.StringSchema<undefined>;
|
|
44
|
-
readonly isRetryable: v.
|
|
44
|
+
readonly isRetryable: v.BooleanSchema<undefined>;
|
|
45
45
|
}, undefined>;
|
|
46
46
|
}, undefined>], undefined>;
|
|
47
47
|
export type ConnectMessage = v.InferOutput<typeof connectMessageSchema>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/messages/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/messages/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAoB5B;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;;;IAG/B;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BASH,CAAA;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,oBAAoB,CAAC,CAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
|
+
import { customBlockErrorInfoSchema } from "../errors.js";
|
|
2
3
|
import { manifestSchema } from "../manifest.js";
|
|
3
4
|
const connectMessageCommonEntries = {
|
|
4
5
|
type: v.literal("connect"),
|
|
@@ -15,13 +16,6 @@ const connectMessageCommonEntries = {
|
|
|
15
16
|
*/
|
|
16
17
|
sdkVersion: v.string(),
|
|
17
18
|
};
|
|
18
|
-
// TODO(custom-blocks): Replace with `customBlockErrorInfoSchema` which makes `isRetryable` required
|
|
19
|
-
// once min supported bridge protocol version is bumped to 3.
|
|
20
|
-
const connectErrorSchema = v.object({
|
|
21
|
-
code: v.string(),
|
|
22
|
-
message: v.string(),
|
|
23
|
-
isRetryable: v.optional(v.boolean(), false),
|
|
24
|
-
});
|
|
25
19
|
/**
|
|
26
20
|
* First message the sandbox sends after mount. It identifies the sandbox and starts the bridge
|
|
27
21
|
* initialization exchange. The host echoes `initializationId` in its `init` response.
|
|
@@ -38,7 +32,7 @@ export const connectMessageSchema = v.variant("status", [
|
|
|
38
32
|
}),
|
|
39
33
|
v.object({
|
|
40
34
|
status: v.literal("error"),
|
|
41
|
-
error:
|
|
35
|
+
error: customBlockErrorInfoSchema,
|
|
42
36
|
...connectMessageCommonEntries,
|
|
43
37
|
}),
|
|
44
38
|
]);
|
|
@@ -37,7 +37,7 @@ export declare const sandboxToHostMessageSchema: v.UnionSchema<[v.VariantSchema<
|
|
|
37
37
|
readonly error: v.ObjectSchema<{
|
|
38
38
|
readonly code: v.StringSchema<undefined>;
|
|
39
39
|
readonly message: v.StringSchema<undefined>;
|
|
40
|
-
readonly isRetryable: v.
|
|
40
|
+
readonly isRetryable: v.BooleanSchema<undefined>;
|
|
41
41
|
}, undefined>;
|
|
42
42
|
}, undefined>], undefined>, v.VariantSchema<"status", [v.ObjectSchema<{
|
|
43
43
|
readonly type: v.LiteralSchema<"initResult", undefined>;
|
|
@@ -11,7 +11,7 @@ import type { CustomBlockHostQueryDataSourceResult } from "./queries/types.js";
|
|
|
11
11
|
export type CustomBlockHostOptions = {
|
|
12
12
|
iframe: HTMLIFrameElement;
|
|
13
13
|
targetOrigin?: string;
|
|
14
|
-
minBridgeProtocolVersion
|
|
14
|
+
minBridgeProtocolVersion: number;
|
|
15
15
|
noConnectTimeoutMs?: number;
|
|
16
16
|
noInitResultTimeoutMs?: number;
|
|
17
17
|
initialState: CustomBlockHostInitialState;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createCustomBlockHost.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/host/createCustomBlockHost.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAc1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAA;AAM/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAA;AAazE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"createCustomBlockHost.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/host/createCustomBlockHost.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAc1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAA;AAM/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAA;AAazE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAIzD,OAAO,KAAK,EACX,qBAAqB,EACrB,iCAAiC,EACjC,2BAA2B,EAE3B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EACX,uBAAuB,EACvB,2BAA2B,EAC3B,MAAM,qBAAqB,CAAA;AAE5B,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,oBAAoB,CAAA;AAE9E,MAAM,MAAM,sBAAsB,GAAG;IACpC,MAAM,EAAE,iBAAiB,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wBAAwB,EAAE,MAAM,CAAA;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,YAAY,EAAE,2BAA2B,CAAA;IACzC,QAAQ,EAAE,uBAAuB,CAAA;IACjC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAA;IACpD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACnD,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,2BAA2B,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CAC1E,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,IAAI,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACtC,SAAS,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAA;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC,cAAc,EAAE,CAAC,WAAW,EAAE,iCAAiC,KAAK,IAAI,CAAA;IACxE,cAAc,EAAE,CAAC,WAAW,EAAE,UAAU,KAAK,IAAI,CAAA;IACjD,YAAY,EAAE,CAAC,IAAI,EAAE;QACpB,YAAY,EAAE,kBAAkB,CAAA;QAChC,QAAQ,EAAE,oCAAoC,CAAA;KAC9C,KAAK,IAAI,CAAA;CACV,CAAA;AAQD,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,sBAAsB,GAC7B,qBAAqB,CA+fvB"}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
2
|
import { sandboxToHostMessageSchema } from "../bridge/messages/sandboxToHost.js";
|
|
3
|
+
import { CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION } from "../bridge/SandboxBridge.js";
|
|
3
4
|
import { unreachable } from "../utils.js";
|
|
4
5
|
import { initErrorForFailureReason } from "./lifecycle/initErrors.js";
|
|
5
|
-
import { isValidBridgeProtocolVersion } from "./lifecycle/
|
|
6
|
+
import { isValidBridgeProtocolVersion } from "./lifecycle/protocolVersion.js";
|
|
6
7
|
import { getInvalidSandboxMessageResponse } from "./messages/invalidSandboxMessage.js";
|
|
7
8
|
import { QuerySubscriptions } from "./queries/querySubscriptions.js";
|
|
8
9
|
const DEFAULT_NO_CONNECT_TIMEOUT_MS = 5000;
|
|
9
10
|
const DEFAULT_NO_INIT_RESULT_TIMEOUT_MS = 5000;
|
|
10
|
-
const
|
|
11
|
+
const initializationIdentitySchema = v.object({
|
|
12
|
+
initializationId: v.string(),
|
|
13
|
+
});
|
|
11
14
|
export function createCustomBlockHost(options) {
|
|
12
|
-
const { iframe, initialState, handlers, onConnect, onInitResult, onLog, targetOrigin = "*", minBridgeProtocolVersion
|
|
15
|
+
const { iframe, initialState, handlers, onConnect, onInitResult, onLog, targetOrigin = "*", minBridgeProtocolVersion: configuredMinBridgeProtocolVersion, noConnectTimeoutMs = DEFAULT_NO_CONNECT_TIMEOUT_MS, noInitResultTimeoutMs = DEFAULT_NO_INIT_RESULT_TIMEOUT_MS, } = options;
|
|
16
|
+
const minBridgeProtocolVersion = Math.max(configuredMinBridgeProtocolVersion ?? CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION, CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION);
|
|
13
17
|
let theme = initialState.theme;
|
|
14
18
|
let parent = initialState.parent;
|
|
15
19
|
let page = initialState.page;
|
|
@@ -286,7 +290,16 @@ export function createCustomBlockHost(options) {
|
|
|
286
290
|
issues,
|
|
287
291
|
});
|
|
288
292
|
const response = getInvalidSandboxMessageResponse(data);
|
|
289
|
-
|
|
293
|
+
const parsedIdentity = v.safeParse(initializationIdentitySchema, data);
|
|
294
|
+
const initializationId = parsedIdentity.success
|
|
295
|
+
? parsedIdentity.output.initializationId
|
|
296
|
+
: undefined;
|
|
297
|
+
if (initStatus === "waitingForConnect" &&
|
|
298
|
+
response.incomingType === "connect" &&
|
|
299
|
+
initializationId !== undefined) {
|
|
300
|
+
sendInitError(initializationId, initErrorForFailureReason("invalid_connect"));
|
|
301
|
+
}
|
|
302
|
+
else if (response.nack !== undefined) {
|
|
290
303
|
post(response.nack);
|
|
291
304
|
}
|
|
292
305
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare function isValidBridgeProtocolVersion(version: number): boolean;
|
|
2
|
-
//# sourceMappingURL=
|
|
2
|
+
//# sourceMappingURL=protocolVersion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocolVersion.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/host/lifecycle/protocolVersion.ts"],"names":[],"mappings":"AAAA,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE"}
|
package/dist/version.js
CHANGED
package/docs/data-sources.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Data sources
|
|
2
2
|
|
|
3
|
-
A custom block declares its **data sources** — semantic keys like `people` or `tasks` — in `
|
|
3
|
+
A custom block declares its **data sources** — semantic keys like `people` or `tasks` — in the worker's `worker.customBlock(...)` configuration.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The mapping from those semantic keys to concrete Notion data sources is stored on each block instance and configured in Notion.
|
|
6
|
+
|
|
7
|
+
Use `ManifestDataSource`, `ManifestProperty`, and `ManifestIcon` when code needs the corresponding definition metadata types.
|
|
6
8
|
|
|
7
9
|
At runtime, your code references the semantic key and the SDK handles the lookup for you. Use `useDataSource(key)` for the rows themselves (it also exposes the resolved schema), and `useManifest()` when you need the declared data-source keys and their declarations (e.g. picking a default key, or rendering a key switcher). For non-React renderers, `customBlock.getManifest()` exposes the same manifest, but row querying is currently only exposed through the React `useDataSource` hook.
|
|
8
10
|
|
|
@@ -115,8 +117,8 @@ export function ScoreList() {
|
|
|
115
117
|
if (error) return <div role="alert">Couldn't load: {error.message}</div>;
|
|
116
118
|
if (isLoading && items.length === 0) return <div>Loading…</div>;
|
|
117
119
|
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
+
const displayItems = items.filter(isComplete);
|
|
121
|
+
if (displayItems.length === 0) {
|
|
120
122
|
return (
|
|
121
123
|
<div>
|
|
122
124
|
Map a data source with key <code>{KEY}</code> exposing <code>name</code>{" "}
|
|
@@ -128,7 +130,7 @@ export function ScoreList() {
|
|
|
128
130
|
return (
|
|
129
131
|
<div>
|
|
130
132
|
<ul>
|
|
131
|
-
{
|
|
133
|
+
{displayItems.map((item) => (
|
|
132
134
|
<li key={item.id}>
|
|
133
135
|
{String(item.propertiesByKey.name)} —{" "}
|
|
134
136
|
{Number(item.propertiesByKey.score)}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Vite plugin
|
|
2
|
+
|
|
3
|
+
The `notionCustomBlock()` plugin from `@notionhq/custom-blocks/vite` wires a block's local dev server and build output to the SDK handshake.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { defineConfig } from "vite";
|
|
7
|
+
import react from "@vitejs/plugin-react";
|
|
8
|
+
import { notionCustomBlock } from "@notionhq/custom-blocks/vite";
|
|
9
|
+
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
plugins: [react(), notionCustomBlock()],
|
|
12
|
+
});
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
In dev, the plugin serves `custom_blocks.json` from the project root so HMR and the SDK handshake see the same file. On `vite build`, it emits `custom_blocks.json` into `dist/` as a separate asset alongside the bundled HTML and JS. It also forces Vite's `base` to `"./"` (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.
|
|
16
|
+
|
|
17
|
+
## `custom_blocks.json` and local previews
|
|
18
|
+
|
|
19
|
+
`custom_blocks.json` is the block's local manifest — the data-source keys and property shapes the block expects:
|
|
20
|
+
|
|
21
|
+
```json
|
|
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`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@notionhq/custom-blocks",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"bin": {
|
|
35
|
-
"ncblock": "./bin/cli/cli.js",
|
|
36
35
|
"notion-custom-blocks": "./bin/notion-custom-blocks/cli.js"
|
|
37
36
|
},
|
|
38
37
|
"scripts": {
|
|
@@ -63,7 +63,7 @@ import type { NotionUser } from "./users/user.js"
|
|
|
63
63
|
* single host needs to support multiple custom blocks built with different versions of the bridge
|
|
64
64
|
* protocol. Increment this number any time a breaking change is made to the bridge protocol.
|
|
65
65
|
*/
|
|
66
|
-
export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION =
|
|
66
|
+
export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 3
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* A single entry in the bridge message log. Kept intentionally plain so the log
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as v from "valibot"
|
|
2
|
+
import { customBlockErrorInfoSchema } from "../errors.js"
|
|
2
3
|
import { manifestSchema } from "../manifest.js"
|
|
3
4
|
|
|
4
5
|
const connectMessageCommonEntries = {
|
|
@@ -17,14 +18,6 @@ const connectMessageCommonEntries = {
|
|
|
17
18
|
sdkVersion: v.string(),
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
// TODO(custom-blocks): Replace with `customBlockErrorInfoSchema` which makes `isRetryable` required
|
|
21
|
-
// once min supported bridge protocol version is bumped to 3.
|
|
22
|
-
const connectErrorSchema = v.object({
|
|
23
|
-
code: v.string(),
|
|
24
|
-
message: v.string(),
|
|
25
|
-
isRetryable: v.optional(v.boolean(), false),
|
|
26
|
-
})
|
|
27
|
-
|
|
28
21
|
/**
|
|
29
22
|
* First message the sandbox sends after mount. It identifies the sandbox and starts the bridge
|
|
30
23
|
* initialization exchange. The host echoes `initializationId` in its `init` response.
|
|
@@ -41,7 +34,7 @@ export const connectMessageSchema = v.variant("status", [
|
|
|
41
34
|
}),
|
|
42
35
|
v.object({
|
|
43
36
|
status: v.literal("error"),
|
|
44
|
-
error:
|
|
37
|
+
error: customBlockErrorInfoSchema,
|
|
45
38
|
...connectMessageCommonEntries,
|
|
46
39
|
}),
|
|
47
40
|
])
|
|
@@ -34,11 +34,12 @@ import type { UpdatePageMessage } from "../bridge/messages/updatePage.js"
|
|
|
34
34
|
import type { UpdatePageResultMessage } from "../bridge/messages/updatePageResult.js"
|
|
35
35
|
import type { CustomBlockPage } from "../bridge/pages/page.js"
|
|
36
36
|
import type { NotionParent } from "../bridge/parent.js"
|
|
37
|
+
import { CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION } from "../bridge/SandboxBridge.js"
|
|
37
38
|
import type { NotionTheme } from "../bridge/theme.js"
|
|
38
39
|
import type { NotionUser } from "../bridge/users/user.js"
|
|
39
40
|
import { unreachable } from "../utils.js"
|
|
40
41
|
import { initErrorForFailureReason } from "./lifecycle/initErrors.js"
|
|
41
|
-
import { isValidBridgeProtocolVersion } from "./lifecycle/
|
|
42
|
+
import { isValidBridgeProtocolVersion } from "./lifecycle/protocolVersion.js"
|
|
42
43
|
import type {
|
|
43
44
|
ConnectSuccessMessage,
|
|
44
45
|
CustomBlockHostDataSourcesPayload,
|
|
@@ -56,7 +57,7 @@ import type { CustomBlockHostQueryDataSourceResult } from "./queries/types.js"
|
|
|
56
57
|
export type CustomBlockHostOptions = {
|
|
57
58
|
iframe: HTMLIFrameElement
|
|
58
59
|
targetOrigin?: string
|
|
59
|
-
minBridgeProtocolVersion
|
|
60
|
+
minBridgeProtocolVersion: number
|
|
60
61
|
noConnectTimeoutMs?: number
|
|
61
62
|
noInitResultTimeoutMs?: number
|
|
62
63
|
initialState: CustomBlockHostInitialState
|
|
@@ -82,7 +83,9 @@ export type CustomBlockHostHandle = {
|
|
|
82
83
|
|
|
83
84
|
const DEFAULT_NO_CONNECT_TIMEOUT_MS = 5_000
|
|
84
85
|
const DEFAULT_NO_INIT_RESULT_TIMEOUT_MS = 5_000
|
|
85
|
-
const
|
|
86
|
+
const initializationIdentitySchema = v.object({
|
|
87
|
+
initializationId: v.string(),
|
|
88
|
+
})
|
|
86
89
|
|
|
87
90
|
export function createCustomBlockHost(
|
|
88
91
|
options: CustomBlockHostOptions,
|
|
@@ -95,10 +98,14 @@ export function createCustomBlockHost(
|
|
|
95
98
|
onInitResult,
|
|
96
99
|
onLog,
|
|
97
100
|
targetOrigin = "*",
|
|
98
|
-
minBridgeProtocolVersion
|
|
101
|
+
minBridgeProtocolVersion: configuredMinBridgeProtocolVersion,
|
|
99
102
|
noConnectTimeoutMs = DEFAULT_NO_CONNECT_TIMEOUT_MS,
|
|
100
103
|
noInitResultTimeoutMs = DEFAULT_NO_INIT_RESULT_TIMEOUT_MS,
|
|
101
104
|
} = options
|
|
105
|
+
const minBridgeProtocolVersion = Math.max(
|
|
106
|
+
configuredMinBridgeProtocolVersion ?? CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
|
|
107
|
+
CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
|
|
108
|
+
)
|
|
102
109
|
|
|
103
110
|
let theme = initialState.theme
|
|
104
111
|
let parent = initialState.parent
|
|
@@ -418,7 +425,20 @@ export function createCustomBlockHost(
|
|
|
418
425
|
})
|
|
419
426
|
const response = getInvalidSandboxMessageResponse(data)
|
|
420
427
|
|
|
421
|
-
|
|
428
|
+
const parsedIdentity = v.safeParse(initializationIdentitySchema, data)
|
|
429
|
+
const initializationId = parsedIdentity.success
|
|
430
|
+
? parsedIdentity.output.initializationId
|
|
431
|
+
: undefined
|
|
432
|
+
if (
|
|
433
|
+
initStatus === "waitingForConnect" &&
|
|
434
|
+
response.incomingType === "connect" &&
|
|
435
|
+
initializationId !== undefined
|
|
436
|
+
) {
|
|
437
|
+
sendInitError(
|
|
438
|
+
initializationId,
|
|
439
|
+
initErrorForFailureReason("invalid_connect"),
|
|
440
|
+
)
|
|
441
|
+
} else if (response.nack !== undefined) {
|
|
422
442
|
post(response.nack)
|
|
423
443
|
}
|
|
424
444
|
}
|
package/bin/cli/attach.js
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { extractId, formatUuid } from "./ids.js";
|
|
2
|
-
import { ntnApi, ntnApiPatch, ntnWorkersCapabilities } from "./ntn.js";
|
|
3
|
-
// ── Capability resolution ───────────────────────────────────────
|
|
4
|
-
function normalizeCapabilities(raw) {
|
|
5
|
-
if (Array.isArray(raw)) {
|
|
6
|
-
return raw;
|
|
7
|
-
}
|
|
8
|
-
if (raw && typeof raw === "object") {
|
|
9
|
-
const obj = raw;
|
|
10
|
-
for (const field of ["capabilities", "results", "data"]) {
|
|
11
|
-
if (Array.isArray(obj[field])) {
|
|
12
|
-
return obj[field];
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
return [];
|
|
17
|
-
}
|
|
18
|
-
function capabilityKey(cap) {
|
|
19
|
-
for (const field of ["key", "name", "id"]) {
|
|
20
|
-
const value = cap[field];
|
|
21
|
-
if (typeof value === "string" && value.length > 0) {
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
function capabilityType(cap) {
|
|
28
|
-
// `ntn` reports the capability kind under `_tag`; accept `type` too in case
|
|
29
|
-
// the wire shape shifts.
|
|
30
|
-
for (const field of ["_tag", "type"]) {
|
|
31
|
-
const value = cap[field];
|
|
32
|
-
if (typeof value === "string") {
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
function isCustomBlockCapability(cap) {
|
|
39
|
-
const type = capabilityType(cap);
|
|
40
|
-
// When no type field is present we can't filter — keep it and let the
|
|
41
|
-
// count-based selection below decide.
|
|
42
|
-
if (type === undefined) {
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
return type === "customBlock" || type === "custom_block";
|
|
46
|
-
}
|
|
47
|
-
function firstString(obj, fields) {
|
|
48
|
-
for (const field of fields) {
|
|
49
|
-
const value = obj[field];
|
|
50
|
-
if (typeof value === "string" && value.length > 0) {
|
|
51
|
-
return value;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
function capabilityDefinitionId(cap) {
|
|
57
|
-
const top = firstString(cap, ["definition_id", "definitionId"]);
|
|
58
|
-
if (top) {
|
|
59
|
-
return top;
|
|
60
|
-
}
|
|
61
|
-
// The real `ntn` shape nests it under `state.definitionId`.
|
|
62
|
-
const state = cap.state;
|
|
63
|
-
if (state && typeof state === "object") {
|
|
64
|
-
const nested = firstString(state, [
|
|
65
|
-
"definitionId",
|
|
66
|
-
"definition_id",
|
|
67
|
-
]);
|
|
68
|
-
if (nested) {
|
|
69
|
-
return nested;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
// Some shapes nest the definition under a `definition` object.
|
|
73
|
-
const definition = cap.definition;
|
|
74
|
-
if (definition && typeof definition === "object") {
|
|
75
|
-
const id = definition.id;
|
|
76
|
-
if (typeof id === "string" && id.length > 0) {
|
|
77
|
-
return id;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Resolve a worker's custom-block capability to a `definition_id`.
|
|
84
|
-
*
|
|
85
|
-
* - Filters `ntn workers capabilities list` down to customBlock capabilities.
|
|
86
|
-
* - With `capability` set, picks the entry with that key.
|
|
87
|
-
* - With exactly one candidate, uses it.
|
|
88
|
-
* - With more than one and no `capability`, errors with the available keys.
|
|
89
|
-
*/
|
|
90
|
-
export function resolveDefinitionId(capability) {
|
|
91
|
-
const raw = ntnWorkersCapabilities();
|
|
92
|
-
const all = normalizeCapabilities(raw);
|
|
93
|
-
if (all.length === 0) {
|
|
94
|
-
console.error("Error: no worker capabilities found. Deploy the worker first with `ntn workers deploy`.\n");
|
|
95
|
-
console.error(" Raw `ntn workers capabilities list --json` output:");
|
|
96
|
-
console.error(JSON.stringify(raw, null, "\t"));
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
99
|
-
const customBlocks = all.filter(isCustomBlockCapability);
|
|
100
|
-
if (customBlocks.length === 0) {
|
|
101
|
-
console.error("Error: this worker exposes no customBlock capability to attach.\n");
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
let selected;
|
|
105
|
-
if (capability) {
|
|
106
|
-
const match = customBlocks.find(c => capabilityKey(c) === capability);
|
|
107
|
-
if (!match) {
|
|
108
|
-
const keys = customBlocks.map(c => capabilityKey(c) ?? "(unnamed)");
|
|
109
|
-
console.error(`Error: no customBlock capability named '${capability}'.\n`);
|
|
110
|
-
console.error(` Available: ${keys.join(", ")}`);
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|
|
113
|
-
selected = match;
|
|
114
|
-
}
|
|
115
|
-
else if (customBlocks.length > 1) {
|
|
116
|
-
const keys = customBlocks.map(c => capabilityKey(c) ?? "(unnamed)");
|
|
117
|
-
console.error(`Error: this worker exposes ${customBlocks.length} customBlock capabilities. Pick one with --capability <key>.\n`);
|
|
118
|
-
console.error(` Available: ${keys.join(", ")}`);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
selected = customBlocks[0];
|
|
123
|
-
}
|
|
124
|
-
const definitionId = capabilityDefinitionId(selected);
|
|
125
|
-
if (!definitionId) {
|
|
126
|
-
console.error("Error: could not read a definition_id from the selected capability.\n");
|
|
127
|
-
console.error(" Raw capability entry:");
|
|
128
|
-
console.error(JSON.stringify(selected, null, "\t"));
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
return { definitionId, capabilityKey: capabilityKey(selected) };
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Body for pointing a custom block at a worker's definition. The update-block
|
|
135
|
-
* endpoint requires the payload be keyed by block type — a top-level
|
|
136
|
-
* `definition_id` is rejected (`should be not present`). So it nests under
|
|
137
|
-
* `custom_block`, matching how `patchBlock` (datasources.ts) sets data_sources.
|
|
138
|
-
*/
|
|
139
|
-
function definitionPatchBody(definitionId) {
|
|
140
|
-
return { custom_block: { definition_id: definitionId } };
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Point an existing custom block at a worker's customBlock definition.
|
|
144
|
-
*
|
|
145
|
-
* Resolves the block, verifies it's a `custom_block`, resolves the worker's
|
|
146
|
-
* definition_id (erroring on ambiguity), then PATCHes the block. Returns the
|
|
147
|
-
* dashed block UUID so callers (e.g. `create`) can chain.
|
|
148
|
-
*/
|
|
149
|
-
export function attach(options) {
|
|
150
|
-
const uuid = formatUuid(extractId(options.idOrUrl));
|
|
151
|
-
if (!options.jsonOutput) {
|
|
152
|
-
console.log(`\nFetching block ${uuid}…`);
|
|
153
|
-
}
|
|
154
|
-
const block = ntnApi(`/v1/blocks/${uuid}`);
|
|
155
|
-
if (block.type !== "custom_block") {
|
|
156
|
-
console.error(`Error: block ${uuid} is type "${block.type}", not "custom_block".\n`);
|
|
157
|
-
console.error(" Pass a custom block's ID/URL, or create one with `ncblock create`.");
|
|
158
|
-
process.exit(1);
|
|
159
|
-
}
|
|
160
|
-
const { definitionId, capabilityKey: key } = resolveDefinitionId(options.capability);
|
|
161
|
-
if (!options.jsonOutput) {
|
|
162
|
-
console.log(`Attaching definition ${definitionId}${key ? ` (capability "${key}")` : ""}…`);
|
|
163
|
-
}
|
|
164
|
-
ntnApiPatch(`/v1/blocks/${uuid}`, definitionPatchBody(definitionId));
|
|
165
|
-
const emitResult = options.emitResult ?? true;
|
|
166
|
-
if (options.jsonOutput) {
|
|
167
|
-
if (emitResult) {
|
|
168
|
-
console.log(JSON.stringify({
|
|
169
|
-
action: "attach",
|
|
170
|
-
block_id: uuid,
|
|
171
|
-
definition_id: definitionId,
|
|
172
|
-
capability: key,
|
|
173
|
-
}, null, "\t"));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
console.log(`\n✓ Attached block ${uuid} to definition ${definitionId}\n`);
|
|
178
|
-
}
|
|
179
|
-
return uuid;
|
|
180
|
-
}
|