@notionhq/custom-blocks 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,11 @@ the workspace. React is optional unless the block imports
15
15
  `@notionhq/custom-blocks/react`. React blocks need `react` and `react-dom`.
16
16
 
17
17
  For local preview, run `ntn customblocks dev` from the worker project, then
18
- open `http://localhost:9873`.
18
+ open `http://localhost:9873`. Install
19
+ [`@notionhq/custom-blocks-dev-shell`](https://www.npmjs.com/package/@notionhq/custom-blocks-dev-shell)
20
+ as a devDependency to pin the shell and get its reference docs in
21
+ `node_modules`; see [`docs/deployment.md`](./docs/deployment.md) for how local
22
+ serving works.
19
23
 
20
24
  ## Quick start
21
25
 
@@ -10,12 +10,11 @@ export type ManifestLoadResult = {
10
10
  error: CustomBlockConnectError;
11
11
  };
12
12
  /**
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.
13
+ * On the literal `localhost` hostname, attempts to load the custom block's optional self-hosted
14
+ * manifest exposed by the worker dev shell at `/manifest`.
16
15
  *
17
16
  * It is up to the host to decide whether to use this manifest or provide its own persisted manifest.
18
17
  */
19
- export declare function attemptToLoadSelfHostedManifest(): Promise<ManifestLoadResult>;
18
+ export declare function attemptToLoadSelfHostedManifest(hostname?: string | undefined): Promise<ManifestLoadResult>;
20
19
  export {};
21
20
  //# 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":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4CAA4C,CAAA;AACtF,OAAO,EACN,KAAK,mBAAmB,EAExB,MAAM,8CAA8C,CAAA;AAKrD,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"}
1
+ {"version":3,"file":"loadManifest.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/loadManifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4CAA4C,CAAA;AACtF,OAAO,EACN,KAAK,mBAAmB,EAExB,MAAM,8CAA8C,CAAA;AAKrD,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;;;;;GAKG;AACH,wBAAsB,+BAA+B,CACpD,QAAQ,GAAE,MAAM,GAAG,SAEQ,GACzB,OAAO,CAAC,kBAAkB,CAAC,CAgE7B"}
@@ -1,14 +1,20 @@
1
1
  import { manifestSchema, } from "../protocol/manifest.js";
2
2
  import * as v from "valibot";
3
- const MANIFEST_URL = "custom_blocks.json";
3
+ const MANIFEST_URL = "/manifest";
4
4
  /**
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.
5
+ * On the literal `localhost` hostname, attempts to load the custom block's optional self-hosted
6
+ * manifest exposed by the worker dev shell at `/manifest`.
8
7
  *
9
8
  * It is up to the host to decide whether to use this manifest or provide its own persisted manifest.
10
9
  */
11
- export async function attemptToLoadSelfHostedManifest() {
10
+ export async function attemptToLoadSelfHostedManifest(hostname = typeof window === "undefined"
11
+ ? undefined
12
+ : window.location.hostname) {
13
+ // Runtime manifests are a localhost-only development fallback. Deployed
14
+ // blocks receive their authoritative manifest from the host during init.
15
+ if (hostname !== "localhost") {
16
+ return { manifest: null };
17
+ }
12
18
  let response;
13
19
  try {
14
20
  response = await fetch(MANIFEST_URL, { credentials: "omit" });
package/dist/init.js CHANGED
@@ -39,7 +39,7 @@ export function initCustomBlock(opts = {}) {
39
39
  }
40
40
  /**
41
41
  * Performs the host <-> sandbox SDK handshake:
42
- * 1. Attempts to load an optional self-hosted `custom_blocks.json`
42
+ * 1. On localhost, attempts to load the worker manifest from `/manifest`
43
43
  * 2. Sends `connect` with the manifest
44
44
  * 3. Awaits the host's `init` message
45
45
  * 4. Applies the `init` payload and acknowledges with `initResult` (fire-and-forget)
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  *
5
5
  * WARNING: Generated during SDK publish. Do not edit in the published package.
6
6
  */
7
- export const CUSTOM_BLOCKS_SDK_VERSION = "0.1.10"
7
+ export const CUSTOM_BLOCKS_SDK_VERSION = "0.1.12"
@@ -1,37 +1,17 @@
1
1
  # Deployment and manifests
2
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.
3
+ A custom block is a static web bundle — an `index.html` plus optional assets — that Notion loads into a sandboxed `<iframe>`. Custom blocks ship 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.
4
4
 
5
- The difference between the two paths is where the manifest lives:
5
+ A custom block manifest describes the data sources a block requires, including their semantic keys, display metadata, and property schemas. The block's `worker.customBlock(...)` declaration is the manifest's single source of truth:
6
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.
7
+ - **Deployed** — deploying persists the manifest on the block's definition record in Notion. The host reads it from there and sends it to the SDK during initialization.
8
+ - **Local development** — the dev shell extracts the same worker declaration at runtime and serves the selected block's manifest dynamically at `/manifest`.
9
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`.
10
+ ## Local development
11
11
 
12
- ## Self-hosted blocks (localhost fallback)
12
+ Run `ntn customblocks dev` from a worker. It builds the worker, extracts its custom block declarations, starts one localhost Vite server per block, and renders them in the mock host at http://localhost:9873.
13
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.
14
+ At startup, the SDK fetches `/manifest` only when the bundle's hostname is exactly `localhost`, then forwards the response in `connect`. On every other hostname, the SDK skips this fetch and relies on the host-provided manifest from `init`.
35
15
 
36
16
  ## Types
37
17
 
@@ -1,6 +1,6 @@
1
1
  # Vite plugin
2
2
 
3
- The `notionCustomBlock()` plugin from `@notionhq/custom-blocks/vite` wires a block's local dev server and build output to the SDK handshake.
3
+ The `notionCustomBlock()` plugin from `@notionhq/custom-blocks/vite` configures a block's build output for Notion hosting.
4
4
 
5
5
  ```ts
6
6
  import { defineConfig } from "vite";
@@ -12,11 +12,6 @@ export default defineConfig({
12
12
  });
13
13
  ```
14
14
 
15
- It does two things:
15
+ It 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.
16
16
 
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`.
19
-
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.
21
-
22
- The plugin does not validate the manifest. The SDK validates it on receipt.
17
+ The plugin does not serve or emit manifests. Declare the manifest with `worker.customBlock(...)`; the worker dev shell exposes it dynamically at `/manifest` during local development. See [deployment.md](./deployment.md) for more information.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -5,7 +5,7 @@ import {
5
5
  } from "@notionhq/custom-blocks-protocol/manifest.js"
6
6
  import * as v from "valibot"
7
7
 
8
- const MANIFEST_URL = "custom_blocks.json"
8
+ const MANIFEST_URL = "/manifest"
9
9
 
10
10
  type CustomBlockConnectErrorCode =
11
11
  | "manifest_unavailable"
@@ -25,13 +25,22 @@ export type ManifestLoadResult =
25
25
  }
26
26
 
27
27
  /**
28
- * Attempts to load the custom block's optional self-hosted manifest, co-located with the bundle at
29
- * `/custom_blocks.json`. This is to support local development where the block is served from a
30
- * local dev server.
28
+ * On the literal `localhost` hostname, attempts to load the custom block's optional self-hosted
29
+ * manifest exposed by the worker dev shell at `/manifest`.
31
30
  *
32
31
  * It is up to the host to decide whether to use this manifest or provide its own persisted manifest.
33
32
  */
34
- export async function attemptToLoadSelfHostedManifest(): Promise<ManifestLoadResult> {
33
+ export async function attemptToLoadSelfHostedManifest(
34
+ hostname: string | undefined = typeof window === "undefined"
35
+ ? undefined
36
+ : window.location.hostname,
37
+ ): Promise<ManifestLoadResult> {
38
+ // Runtime manifests are a localhost-only development fallback. Deployed
39
+ // blocks receive their authoritative manifest from the host during init.
40
+ if (hostname !== "localhost") {
41
+ return { manifest: null }
42
+ }
43
+
35
44
  let response: Response
36
45
  try {
37
46
  response = await fetch(MANIFEST_URL, { credentials: "omit" })
package/src/init.ts CHANGED
@@ -83,7 +83,7 @@ export function initCustomBlock(
83
83
 
84
84
  /**
85
85
  * Performs the host <-> sandbox SDK handshake:
86
- * 1. Attempts to load an optional self-hosted `custom_blocks.json`
86
+ * 1. On localhost, attempts to load the worker manifest from `/manifest`
87
87
  * 2. Sends `connect` with the manifest
88
88
  * 3. Awaits the host's `init` message
89
89
  * 4. Applies the `init` payload and acknowledges with `initResult` (fire-and-forget)
@@ -6,41 +6,12 @@
6
6
 
7
7
  type UserConfig = { base?: string }
8
8
 
9
- type ResolvedConfig = { root: string; command: "serve" | "build" }
10
-
11
- type ServerLike = {
12
- middlewares: { use: (handler: MiddlewareHandler) => unknown }
13
- }
14
-
15
- type MiddlewareHandler = (
16
- req: { url?: string },
17
- res: {
18
- statusCode?: number
19
- setHeader: (name: string, value: string) => void
20
- end: (body?: string) => void
21
- },
22
- next: () => void,
23
- ) => void
24
-
25
- type AssetEmitterContext = {
26
- emitFile: (file: {
27
- type: "asset"
28
- fileName: string
29
- source: string
30
- }) => string
31
- }
32
-
33
9
  export type NotionCustomBlockPlugin = {
34
10
  name: string
35
11
  config: (userConfig: UserConfig) => { base: string }
36
- configResolved: (config: ResolvedConfig) => void
37
- configureServer: (server: ServerLike) => void
38
- configurePreviewServer: (server: ServerLike) => void
39
- buildStart: (this: AssetEmitterContext) => void
40
12
  }
41
13
 
42
14
  /**
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.
15
+ * Vite plugin that configures relative asset paths for custom block bundles.
45
16
  */
46
17
  export function notionCustomBlock(): NotionCustomBlockPlugin
@@ -1,7 +1,5 @@
1
1
  /**
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.
2
+ * Vite plugin half of `@notionhq/custom-blocks`.
5
3
  *
6
4
  * Authored as plain JS (with `index.d.ts` co-located for types) — the rest of
7
5
  * the SDK is `.ts` consumed directly via the workspace symlink, but Vite's
@@ -21,56 +19,14 @@
21
19
  * Vite's config loader uses Node's ESM resolver (not CommonJS `require`,
22
20
  * which fails for ESM-only deps).
23
21
  *
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.
29
- *
30
- * Validation is intentionally not performed here — the SDK validates on
31
- * receipt, and the CLI validates separately at deploy time.
22
+ * The worker dev shell owns the localhost `/manifest` route. This plugin only
23
+ * controls bundle asset paths.
32
24
  */
33
- import { existsSync, readFileSync } from "node:fs"
34
- import { resolve } from "node:path"
35
-
36
- const MANIFEST_FILENAME = "custom_blocks.json"
37
- const MANIFEST_URL_PATH = `/${MANIFEST_FILENAME}`
38
25
  const REQUIRED_BASE = "./"
39
26
 
40
27
  export function notionCustomBlock() {
41
- let projectRoot = process.cwd()
42
- let command = "serve"
43
-
44
- function manifestPath() {
45
- return resolve(projectRoot, MANIFEST_FILENAME)
46
- }
47
-
48
- function readManifest() {
49
- const file = manifestPath()
50
- if (!existsSync(file)) {
51
- return undefined
52
- }
53
- return readFileSync(file, "utf8")
54
- }
55
-
56
- function middleware(req, res, next) {
57
- const path = req.url?.split("?", 1)[0]
58
- if (path !== MANIFEST_URL_PATH) {
59
- next()
60
- return
61
- }
62
- const contents = readManifest()
63
- if (contents === undefined) {
64
- res.statusCode = 404
65
- res.end()
66
- return
67
- }
68
- res.setHeader("Content-Type", "application/json")
69
- res.end(contents)
70
- }
71
-
72
28
  return {
73
- name: "custom-blocks:notion-manifest",
29
+ name: "custom-blocks:bundle-base",
74
30
  config(userConfig) {
75
31
  // Custom blocks are served from a content-addressed path that
76
32
  // has trailing slashes, eg https://dev.notion.so/custom-block-bundle/357b35e6-e67f-81fd-b425-00e7b3e2afd1/
@@ -86,31 +42,5 @@ export function notionCustomBlock() {
86
42
  }
87
43
  return { base: REQUIRED_BASE }
88
44
  },
89
- configResolved(config) {
90
- projectRoot = config.root
91
- command = config.command
92
- },
93
- configureServer(server) {
94
- server.middlewares.use(middleware)
95
- },
96
- configurePreviewServer(server) {
97
- server.middlewares.use(middleware)
98
- },
99
- buildStart() {
100
- // `emitFile` is only valid during `vite build`. In serve mode the
101
- // middleware above handles `/custom_blocks.json`, so we skip emission.
102
- if (command !== "build") {
103
- return
104
- }
105
- const contents = readManifest()
106
- if (contents === undefined) {
107
- return
108
- }
109
- this.emitFile({
110
- type: "asset",
111
- fileName: MANIFEST_FILENAME,
112
- source: contents,
113
- })
114
- },
115
45
  }
116
46
  }