@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 +5 -1
- package/dist/bridge/loadManifest.d.ts +3 -4
- package/dist/bridge/loadManifest.d.ts.map +1 -1
- package/dist/bridge/loadManifest.js +11 -5
- package/dist/init.js +1 -1
- package/dist/version.js +1 -1
- package/docs/deployment.md +7 -27
- package/docs/vite-plugin.md +3 -8
- package/package.json +1 -1
- package/src/bridge/loadManifest.ts +14 -5
- package/src/init.ts +1 -1
- package/vite-plugin/index.d.ts +1 -30
- package/vite-plugin/index.js +4 -74
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
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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 = "
|
|
3
|
+
const MANIFEST_URL = "/manifest";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
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
package/docs/deployment.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
- **
|
|
8
|
-
- **
|
|
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
|
-
|
|
10
|
+
## Local development
|
|
11
11
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
package/docs/vite-plugin.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vite plugin
|
|
2
2
|
|
|
3
|
-
The `notionCustomBlock()` plugin from `@notionhq/custom-blocks/vite`
|
|
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
|
|
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
|
-
|
|
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
|
@@ -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 = "
|
|
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
|
-
*
|
|
29
|
-
*
|
|
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(
|
|
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.
|
|
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)
|
package/vite-plugin/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/vite-plugin/index.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Vite plugin half of `@notionhq/custom-blocks`.
|
|
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
|
-
*
|
|
25
|
-
*
|
|
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:
|
|
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
|
}
|