@notionhq/custom-blocks 0.0.77 → 0.0.79

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.
Files changed (100) hide show
  1. package/HOST.md +68 -34
  2. package/README.md +1 -1
  3. package/bin/src/bridge/manifest.js +3 -3
  4. package/dist/bridge/SandboxBridge.d.ts +8 -13
  5. package/dist/bridge/SandboxBridge.d.ts.map +1 -1
  6. package/dist/bridge/SandboxBridge.js +110 -39
  7. package/dist/bridge/dataSources/resolve.d.ts +3 -4
  8. package/dist/bridge/dataSources/resolve.d.ts.map +1 -1
  9. package/dist/bridge/dataSources/resolve.js +2 -12
  10. package/dist/bridge/hostState.d.ts +2 -0
  11. package/dist/bridge/hostState.d.ts.map +1 -1
  12. package/dist/bridge/loadManifest.d.ts +4 -4
  13. package/dist/bridge/loadManifest.d.ts.map +1 -1
  14. package/dist/bridge/loadManifest.js +2 -2
  15. package/dist/bridge/manifest.d.ts +3 -3
  16. package/dist/bridge/manifest.js +3 -3
  17. package/dist/bridge/messages/{ready.d.ts → connect.d.ts} +14 -12
  18. package/dist/bridge/messages/connect.d.ts.map +1 -0
  19. package/dist/bridge/messages/connect.js +38 -0
  20. package/dist/bridge/messages/hostToSandbox.d.ts +21 -0
  21. package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -1
  22. package/dist/bridge/messages/init.d.ts +35 -6
  23. package/dist/bridge/messages/init.d.ts.map +1 -1
  24. package/dist/bridge/messages/init.js +8 -4
  25. package/dist/bridge/messages/initResult.d.ts +29 -0
  26. package/dist/bridge/messages/initResult.d.ts.map +1 -0
  27. package/dist/bridge/messages/initResult.js +25 -0
  28. package/dist/bridge/messages/sandboxToHost.d.ts +21 -6
  29. package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -1
  30. package/dist/bridge/messages/sandboxToHost.js +4 -2
  31. package/dist/bridge/sandboxClient.d.ts +2 -4
  32. package/dist/bridge/sandboxClient.d.ts.map +1 -1
  33. package/dist/bridge/sandboxClient.js +2 -5
  34. package/dist/customBlock.d.ts +1 -1
  35. package/dist/customBlock.d.ts.map +1 -1
  36. package/dist/customBlock.js +1 -1
  37. package/dist/host/createCustomBlockHost.d.ts +7 -4
  38. package/dist/host/createCustomBlockHost.d.ts.map +1 -1
  39. package/dist/host/createCustomBlockHost.js +215 -38
  40. package/dist/host/lifecycle/initErrors.d.ts +1 -1
  41. package/dist/host/lifecycle/initErrors.d.ts.map +1 -1
  42. package/dist/host/lifecycle/initErrors.js +25 -7
  43. package/dist/host/lifecycle/{ready.d.ts → protocolVersion.d.ts} +1 -1
  44. package/dist/host/lifecycle/protocolVersion.d.ts.map +1 -0
  45. package/dist/host/lifecycle/types.d.ts +6 -4
  46. package/dist/host/lifecycle/types.d.ts.map +1 -1
  47. package/dist/host/messages/invalidSandboxMessage.js +0 -3
  48. package/dist/host.d.ts +5 -3
  49. package/dist/host.d.ts.map +1 -1
  50. package/dist/host.js +2 -1
  51. package/dist/index.d.ts +2 -2
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +1 -1
  54. package/dist/init.d.ts +7 -7
  55. package/dist/init.d.ts.map +1 -1
  56. package/dist/init.js +70 -50
  57. package/dist/react/index.d.ts +1 -1
  58. package/dist/react/index.d.ts.map +1 -1
  59. package/dist/react/standalonePreview.d.ts.map +1 -1
  60. package/dist/react/standalonePreview.js +2 -0
  61. package/dist/react/useCustomBlockInit.d.ts +5 -6
  62. package/dist/react/useCustomBlockInit.d.ts.map +1 -1
  63. package/dist/react/useCustomBlockInit.js +3 -19
  64. package/dist/react/useRuntimeState.d.ts +4 -3
  65. package/dist/react/useRuntimeState.d.ts.map +1 -1
  66. package/dist/react/useRuntimeState.js +3 -2
  67. package/dist/version.js +1 -1
  68. package/docs/data-sources.md +10 -8
  69. package/docs/errors.md +5 -1
  70. package/docs/lifecycle.md +19 -31
  71. package/docs/manifest.md +1 -1
  72. package/package.json +1 -1
  73. package/src/bridge/SandboxBridge.ts +138 -44
  74. package/src/bridge/dataSources/resolve.ts +3 -14
  75. package/src/bridge/hostState.ts +2 -0
  76. package/src/bridge/loadManifest.ts +6 -6
  77. package/src/bridge/manifest.ts +3 -3
  78. package/src/bridge/messages/connect.ts +42 -0
  79. package/src/bridge/messages/init.ts +24 -10
  80. package/src/bridge/messages/initResult.ts +37 -0
  81. package/src/bridge/messages/sandboxToHost.ts +4 -2
  82. package/src/bridge/sandboxClient.ts +3 -8
  83. package/src/customBlock.ts +2 -2
  84. package/src/host/createCustomBlockHost.ts +265 -45
  85. package/src/host/lifecycle/initErrors.ts +25 -7
  86. package/src/host/lifecycle/types.ts +13 -4
  87. package/src/host/messages/invalidSandboxMessage.ts +0 -3
  88. package/src/host.ts +13 -2
  89. package/src/index.ts +3 -1
  90. package/src/init.ts +73 -60
  91. package/src/react/index.ts +0 -1
  92. package/src/react/standalonePreview.ts +2 -0
  93. package/src/react/useCustomBlockInit.ts +6 -27
  94. package/src/react/useRuntimeState.ts +4 -3
  95. package/dist/bridge/messages/ready.d.ts.map +0 -1
  96. package/dist/bridge/messages/ready.js +0 -43
  97. package/dist/host/lifecycle/ready.d.ts.map +0 -1
  98. package/src/bridge/messages/ready.ts +0 -48
  99. /package/dist/host/lifecycle/{ready.js → protocolVersion.js} +0 -0
  100. /package/src/host/lifecycle/{ready.ts → protocolVersion.ts} +0 -0
package/src/init.ts CHANGED
@@ -1,19 +1,16 @@
1
1
  import type { NotionDataSource } from "./bridge/dataSources/dataSource.js"
2
2
  import type { NotionBlockId } from "./bridge/ids.js"
3
3
  import { loadManifest } from "./bridge/loadManifest.js"
4
- import {
5
- CustomBlockInitError,
6
- type CustomBlockInitErrorCode,
7
- type InitMessage,
8
- } from "./bridge/messages/init.js"
4
+ import { CustomBlockInitializationError } from "./bridge/messages/init.js"
9
5
  import type { CustomBlockPage } from "./bridge/pages/page.js"
10
6
  import type { NotionParent } from "./bridge/parent.js"
11
7
  import { customBlockHost } from "./bridge/sandboxClient.js"
12
8
  import type { NotionTheme } from "./bridge/theme.js"
13
9
  import type { NotionUser } from "./bridge/users/user.js"
10
+ import { unreachable } from "./utils.js"
14
11
 
15
12
  /**
16
- * The payload sent by the host in the `init` message in response to the sandbox's `ready` message.
13
+ * The payload sent by the host in the `init` message in response to the sandbox's `connect` message.
17
14
  */
18
15
  export type CustomBlockInitPayload = {
19
16
  theme: NotionTheme
@@ -30,16 +27,17 @@ export type CustomBlockInitPayload = {
30
27
  * `<NotionCustomBlock>` catches this specifically and falls back to a standalone preview with a
31
28
  * warning banner. Direct callers can `instanceof` it to apply their own policy.
32
29
  */
33
- export class NotInIframeError extends Error {
30
+ export class NotInIframeError extends CustomBlockInitializationError {
34
31
  constructor(message: string = NOT_IN_IFRAME_MESSAGE) {
35
- super(message)
32
+ super({
33
+ code: "not_in_iframe",
34
+ message,
35
+ isRetryable: false,
36
+ })
36
37
  this.name = "NotInIframeError"
37
- this.code = "not_in_iframe"
38
- this.isRetryable = false
39
38
  }
40
39
 
41
- code: CustomBlockInitErrorCode
42
- isRetryable: boolean
40
+ declare code: "not_in_iframe"
43
41
  }
44
42
 
45
43
  /**
@@ -64,9 +62,10 @@ let initPromise: Promise<CustomBlockInitPayload> | undefined
64
62
 
65
63
  /**
66
64
  * Performs the SDK <-> host handshake: loads `custom_blocks.json`, posts
67
- * `ready`, then awaits the host's `init` message. Resolves with that payload.
65
+ * `connect`, then awaits the host's `init` message. Resolves after the sandbox
66
+ * applies the payload and returns `initResult.success`.
68
67
  *
69
- * Rejects with a `CustomBlockInitError` if the host doesn't respond inside `timeoutMs`.
68
+ * Rejects with a `CustomBlockInitializationError` if initialization fails.
70
69
  *
71
70
  * Idempotent: subsequent calls return the same promise as the first and ignore any new options.
72
71
  * Mount your React tree (or call any SDK hook / `customBlock.subscribe`) only after the
@@ -76,61 +75,75 @@ export function initCustomBlock(
76
75
  opts: InitCustomBlockOptions = {},
77
76
  ): Promise<CustomBlockInitPayload> {
78
77
  if (initPromise === undefined) {
79
- initPromise = (async () => {
80
- // Fail fast with a typed error when rendered as a standalone tab and not in a parent frame.
81
- // Otherwise, it would eventually hit the timeout, since `postMessage` to `window.parent`
82
- // would just hit the same window and never arrive.
83
- if (typeof window !== "undefined" && window.parent === window) {
84
- throw new NotInIframeError()
85
- }
78
+ initPromise = initializeCustomBlock(opts)
79
+ }
80
+ return initPromise
81
+ }
86
82
 
87
- // Load the manifest and send it to the host.
88
- const manifestResult = await loadManifest()
89
- customBlockHost.sendReady(manifestResult)
83
+ async function initializeCustomBlock(
84
+ opts: InitCustomBlockOptions,
85
+ ): Promise<CustomBlockInitPayload> {
86
+ try {
87
+ // Fail fast with a typed error when rendered as a standalone tab and not in a parent frame.
88
+ // Otherwise, it would eventually hit the timeout, since `postMessage` to `window.parent`
89
+ // would just hit the same window and never arrive.
90
+ if (typeof window !== "undefined" && window.parent === window) {
91
+ throw new NotInIframeError()
92
+ }
90
93
 
91
- const timeoutMs = opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS
92
- let message: InitMessage
93
- try {
94
- message = await customBlockHost.awaitInit(
95
- AbortSignal.timeout(timeoutMs),
96
- )
97
- } catch (error) {
98
- if (isTimeoutError(error)) {
99
- throw new CustomBlockInitError({
100
- code: "init_timeout",
101
- message: "Host did not respond to init before the timeout.",
102
- isRetryable: true,
103
- })
104
- }
105
- throw error
106
- }
107
- if (message.status === "error") {
108
- console.error(
109
- `[custom-blocks-sdk] host reported init error (${message.error.code}): ${message.error.message}`,
110
- )
111
- throw new CustomBlockInitError(message.error)
112
- }
94
+ // Load the manifest and send it to the host.
95
+ const manifestResult = await loadManifest()
96
+ customBlockHost.sendConnect(manifestResult)
113
97
 
114
- const hostState = customBlockHost.getState()
115
- if (hostState.status !== "initialized") {
116
- throw new CustomBlockInitError({
98
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS
99
+ await awaitHostInitWithTimeout(timeoutMs)
100
+
101
+ const hostState = customBlockHost.getState()
102
+ switch (hostState.status) {
103
+ case "initialized":
104
+ return {
105
+ theme: hostState.theme,
106
+ blockId: hostState.blockId,
107
+ parent: hostState.parent,
108
+ page: hostState.page,
109
+ currentUser: hostState.currentUser,
110
+ dataSources: hostState.dataSources,
111
+ }
112
+ case "uninitialized":
113
+ throw new CustomBlockInitializationError({
117
114
  code: "context_unavailable",
118
115
  message: "Host block payload is unavailable.",
119
116
  isRetryable: true,
120
117
  })
121
- }
118
+ default:
119
+ return unreachable(hostState)
120
+ }
121
+ } catch (error) {
122
+ if (error instanceof CustomBlockInitializationError) {
123
+ throw error
124
+ }
125
+ const message = error instanceof Error ? error.message : String(error)
126
+ throw new CustomBlockInitializationError({
127
+ code: "unknown_error",
128
+ message,
129
+ isRetryable: false,
130
+ })
131
+ }
132
+ }
122
133
 
123
- return {
124
- theme: message.theme,
125
- blockId: hostState.blockId,
126
- parent: hostState.parent,
127
- page: hostState.page,
128
- currentUser: message.currentUser,
129
- dataSources: hostState.dataSources,
130
- }
131
- })()
134
+ async function awaitHostInitWithTimeout(timeoutMs: number): Promise<void> {
135
+ try {
136
+ await customBlockHost.awaitInit(AbortSignal.timeout(timeoutMs))
137
+ } catch (error) {
138
+ if (isTimeoutError(error)) {
139
+ throw new CustomBlockInitializationError({
140
+ code: "init_timeout",
141
+ message: "Host did not respond to init before the timeout.",
142
+ isRetryable: true,
143
+ })
144
+ }
145
+ throw error
132
146
  }
133
- return initPromise
134
147
  }
135
148
 
136
149
  function isTimeoutError(error: unknown): boolean {
@@ -13,7 +13,6 @@ export {
13
13
  } from "./NotionCustomBlock.js"
14
14
  export { useCustomBlockAutoResize } from "./useCustomBlockAutoResize.js"
15
15
  export {
16
- type CustomBlockInitFailure,
17
16
  type UseCustomBlockInitResult,
18
17
  useCustomBlockInit,
19
18
  } from "./useCustomBlockInit.js"
@@ -8,11 +8,13 @@ const previewPageId = v.parse(notionPageIdSchema, "")
8
8
  export function seedStandalonePreviewState() {
9
9
  customBlockHost.setMockState({
10
10
  type: "init",
11
+ initializationId: "standalone-preview",
11
12
  status: "success",
12
13
  theme: "light",
13
14
  blockId: previewBlockId,
14
15
  parent: { type: "page_id", page_id: previewPageId },
15
16
  page: { id: previewPageId, parent: { type: "workspace", workspace: true } },
17
+ manifest: { version: 1, dataSources: {} },
16
18
  dataSources: { bindings: {} },
17
19
  currentUser: {
18
20
  object: "user",
@@ -1,27 +1,24 @@
1
1
  import { useEffect, useState } from "react"
2
- import { CustomBlockInitError } from "../bridge/messages/init.js"
2
+ import type { CustomBlockInitializationError } from "../bridge/messages/init.js"
3
3
  import {
4
4
  type CustomBlockInitPayload,
5
5
  type InitCustomBlockOptions,
6
6
  initCustomBlock,
7
- NotInIframeError,
8
7
  } from "../init.js"
9
8
 
10
- export type CustomBlockInitFailure = CustomBlockInitError | NotInIframeError
11
-
12
9
  /**
13
10
  * Discriminated state returned by {@link useCustomBlockInit}.
14
11
  *
15
12
  * Branch on `isLoaded`/`error`:
16
13
  * - `{ isLoaded: false, error: undefined }` — handshake in progress.
17
- * - `{ isLoaded: false, error: CustomBlockInitFailure }` — handshake failed (most commonly a
18
- * `CustomBlockInitError` with code `init_timeout` because the host never sent `init`).
14
+ * - `{ isLoaded: false, error: CustomBlockInitializationError }` — handshake failed (most commonly a
15
+ * `CustomBlockInitializationError` with code `init_timeout` because the host never sent `init`).
19
16
  * - `{ isLoaded: true, initial }` — handshake complete; safe to render
20
17
  * children that call `useTheme`, `useBlockId`, etc.
21
18
  */
22
19
  export type UseCustomBlockInitResult =
23
20
  | { isLoaded: false; error: undefined }
24
- | { isLoaded: false; error: CustomBlockInitFailure }
21
+ | { isLoaded: false; error: CustomBlockInitializationError }
25
22
  | { isLoaded: true; error: undefined; initial: CustomBlockInitPayload }
26
23
 
27
24
  /**
@@ -56,12 +53,9 @@ export function useCustomBlockInit(
56
53
  setState({ isLoaded: true, error: undefined, initial })
57
54
  }
58
55
  },
59
- err => {
56
+ error => {
60
57
  if (!cancelled) {
61
- setState({
62
- isLoaded: false,
63
- error: normalizeInitError(err),
64
- })
58
+ setState({ isLoaded: false, error })
65
59
  }
66
60
  },
67
61
  )
@@ -74,18 +68,3 @@ export function useCustomBlockInit(
74
68
  }, [])
75
69
  return state
76
70
  }
77
-
78
- function normalizeInitError(error: unknown): CustomBlockInitFailure {
79
- if (
80
- error instanceof CustomBlockInitError ||
81
- error instanceof NotInIframeError
82
- ) {
83
- return error
84
- }
85
- const message = error instanceof Error ? error.message : String(error)
86
- return new CustomBlockInitError({
87
- code: "unknown_error",
88
- message,
89
- isRetryable: false,
90
- })
91
- }
@@ -72,14 +72,15 @@ export function useCurrentUser(): NotionUser {
72
72
  }
73
73
 
74
74
  /**
75
- * Returns the author-declared manifest loaded from `custom_blocks.json` — the semantic
75
+ * Returns the authoritative manifest received from the host — the semantic
76
76
  * data-source keys plus their declared `name`, `description`, and property declarations.
77
- * `null` when the block ships no manifest.
77
+ *
78
+ * Throws if called before `initCustomBlock` has resolved.
78
79
  *
79
80
  * This is the configuration the block declared, not host-resolved bindings: use
80
81
  * `useDataSource(key)` for rows and resolved schema. Handy for enumerating the declared
81
82
  * data-source keys (e.g. picking a default key, or rendering a key switcher).
82
83
  */
83
- export function useManifest(): CustomBlockManifest | null {
84
+ export function useManifest(): CustomBlockManifest {
84
85
  return useSyncExternalStore(customBlock.subscribe, customBlock.getManifest)
85
86
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"ready.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/messages/ready.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AA0B5B;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;IAG7B;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BASH,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,kBAAkB,CAAC,CAAA"}
@@ -1,43 +0,0 @@
1
- import * as v from "valibot";
2
- import { manifestSchema } from "../manifest.js";
3
- const readyMessageCommonEntries = {
4
- type: v.literal("ready"),
5
- /**
6
- * Used to ensure that the host and client are using the same version of the bridge protocol. A
7
- * single host needs to support multiple custom blocks built with different versions of the bridge
8
- * protocol. Increment this number any time a breaking change is made to the bridge protocol.
9
- */
10
- bridgeProtocolVersion: v.number(),
11
- /**
12
- * Semver version of the SDK package that sent this ready message. Should only be used for
13
- * analytics purposes. Business logic should compare against `bridgeProtocolVersion` instead.
14
- */
15
- sdkVersion: v.string(),
16
- };
17
- // TODO(custom-blocks): Replace with `customBlockErrorInfoSchema` which makes `isRetryable` required
18
- // once min supported bridge protocol version is bumped to 3.
19
- const readyErrorSchema = v.object({
20
- code: v.string(),
21
- message: v.string(),
22
- isRetryable: v.optional(v.boolean(), false),
23
- });
24
- /**
25
- * First message the sandbox sends after mount, kicking off the bridge handshake. The host replies
26
- * with `init`.
27
- */
28
- export const readyMessageSchema = v.union([
29
- v.object({
30
- status: v.literal("success"),
31
- /**
32
- * The data sources and settings the custom block expects, or null when the
33
- * block has no manifest and no declared data requirements.
34
- */
35
- manifest: v.union([manifestSchema, v.null_()]),
36
- ...readyMessageCommonEntries,
37
- }),
38
- v.object({
39
- status: v.literal("error"),
40
- error: readyErrorSchema,
41
- ...readyMessageCommonEntries,
42
- }),
43
- ]);
@@ -1 +0,0 @@
1
- {"version":3,"file":"ready.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/host/lifecycle/ready.ts"],"names":[],"mappings":"AAAA,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE"}
@@ -1,48 +0,0 @@
1
- import * as v from "valibot"
2
- import { manifestSchema } from "../manifest.js"
3
-
4
- const readyMessageCommonEntries = {
5
- type: v.literal("ready"),
6
- /**
7
- * Used to ensure that the host and client are using the same version of the bridge protocol. A
8
- * single host needs to support multiple custom blocks built with different versions of the bridge
9
- * protocol. Increment this number any time a breaking change is made to the bridge protocol.
10
- */
11
- bridgeProtocolVersion: v.number(),
12
- /**
13
- * Semver version of the SDK package that sent this ready message. Should only be used for
14
- * analytics purposes. Business logic should compare against `bridgeProtocolVersion` instead.
15
- */
16
- sdkVersion: v.string(),
17
- }
18
-
19
- // TODO(custom-blocks): Replace with `customBlockErrorInfoSchema` which makes `isRetryable` required
20
- // once min supported bridge protocol version is bumped to 3.
21
- const readyErrorSchema = v.object({
22
- code: v.string(),
23
- message: v.string(),
24
- isRetryable: v.optional(v.boolean(), false),
25
- })
26
-
27
- /**
28
- * First message the sandbox sends after mount, kicking off the bridge handshake. The host replies
29
- * with `init`.
30
- */
31
- export const readyMessageSchema = v.union([
32
- v.object({
33
- status: v.literal("success"),
34
- /**
35
- * The data sources and settings the custom block expects, or null when the
36
- * block has no manifest and no declared data requirements.
37
- */
38
- manifest: v.union([manifestSchema, v.null_()]),
39
- ...readyMessageCommonEntries,
40
- }),
41
- v.object({
42
- status: v.literal("error"),
43
- error: readyErrorSchema,
44
- ...readyMessageCommonEntries,
45
- }),
46
- ])
47
-
48
- export type ReadyMessage = v.InferOutput<typeof readyMessageSchema>