@notionhq/custom-blocks-dev-shell 0.1.1 → 0.1.2

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/dist/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Workers local shell</title>
7
- <script type="module" crossorigin src="/assets/index-xG6RNJPx.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-B1zP6MFl.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-DvRqSoqY.css">
9
9
  </head>
10
10
  <body>
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Load a worker's dev-shell data sources from `<dataDir>/*.json` — one source
3
+ * per file, hand-authored (the scaffolded worker AGENTS.md documents the
4
+ * format). Every file is validated here, before it reaches the shell, so a
5
+ * malformed source fails spin-up naming the file and the problem instead of
6
+ * breaking the UI.
7
+ */
8
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
9
+ import { basename, resolve } from "node:path";
10
+ import * as v from "valibot";
11
+ const DEV_SHELL_DATA_SOURCE_TYPES = [
12
+ "built-in",
13
+ "worker",
14
+ "manual",
15
+ "syncedFromProd",
16
+ ];
17
+ // Local copy of the SDK's NOTION_PROPERTY_TYPES: the compiled CLI runs under
18
+ // plain node, which can't import the SDK's published TS source at runtime.
19
+ // The `satisfies` below plus the type-only import keep the two lists locked
20
+ // together — a drift on either side fails typecheck.
21
+ const NOTION_PROPERTY_TYPES = [
22
+ "title",
23
+ "rich_text",
24
+ "number",
25
+ "checkbox",
26
+ "url",
27
+ "email",
28
+ "phone_number",
29
+ "select",
30
+ "multi_select",
31
+ "status",
32
+ "date",
33
+ "people",
34
+ "files",
35
+ "unique_id",
36
+ "relation",
37
+ "place",
38
+ "formula",
39
+ "rollup",
40
+ "button",
41
+ "verification",
42
+ "last_visited_time",
43
+ "location",
44
+ "created_time",
45
+ "last_edited_time",
46
+ "created_by",
47
+ "last_edited_by",
48
+ ];
49
+ const _allPropertyTypesListed = true;
50
+ void _allPropertyTypesListed;
51
+ export function readDataSources(dataDir) {
52
+ if (!existsSync(dataDir)) {
53
+ return [];
54
+ }
55
+ return readdirSync(dataDir)
56
+ .filter(name => name.endsWith(".json"))
57
+ .map(name => readSource(resolve(dataDir, name)))
58
+ .sort((left, right) => left.name.localeCompare(right.name));
59
+ }
60
+ const nonEmptyString = v.pipe(v.string(), v.nonEmpty());
61
+ const sourceFileSchema = v.object({
62
+ type: v.optional(v.literal("worker"), "worker"),
63
+ key: v.optional(nonEmptyString),
64
+ name: v.optional(nonEmptyString),
65
+ icon: v.optional(v.string()),
66
+ schema: v.optional(v.record(v.string(), v.object({
67
+ name: v.optional(v.string()),
68
+ type: v.picklist(NOTION_PROPERTY_TYPES),
69
+ })), {}),
70
+ rows: v.optional(v.array(v.looseObject({ id: nonEmptyString })), []),
71
+ });
72
+ /**
73
+ * Parse and validate one source file. Structural mistakes throw with the file
74
+ * and problem named; omitted `key`/`name` fall back to the filename, and
75
+ * omitted `schema`/`rows` to empty — so the minimal valid file is `{}`.
76
+ */
77
+ function readSource(file) {
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(readFileSync(file, "utf-8"));
81
+ }
82
+ catch (error) {
83
+ throw new Error(`${file}: not valid JSON — ${error.message}`);
84
+ }
85
+ if (Array.isArray(parsed)) {
86
+ throw new Error(`${file}: must be a JSON object`);
87
+ }
88
+ const result = v.safeParse(sourceFileSchema, parsed);
89
+ if (!result.success) {
90
+ const issue = result.issues[0];
91
+ const path = v.getDotPath(issue);
92
+ throw new Error(`${file}: ${path ? `${path}: ` : ""}${issue.message}`);
93
+ }
94
+ const { type, icon, schema, rows } = result.output;
95
+ const key = result.output.key ?? basename(file, ".json");
96
+ return {
97
+ type,
98
+ key,
99
+ name: result.output.name ?? key,
100
+ ...(icon !== undefined ? { icon } : {}),
101
+ schema: Object.fromEntries(Object.entries(schema).map(([propertyKey, property]) => [
102
+ propertyKey,
103
+ { name: property.name ?? propertyKey, type: property.type },
104
+ ])),
105
+ rows,
106
+ };
107
+ }
package/dist-cli/main.js CHANGED
@@ -11,6 +11,7 @@ import { createRequire } from "node:module";
11
11
  import { basename, dirname, join, resolve } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { BLOCK_BASE_PORT, buildBlockRegistry, SHELL_2_PORT, writeBlockViteConfig, } from "./block-server.js";
14
+ import { readDataSources } from "./data-sources.js";
14
15
  import { serveUi } from "./serve-ui.js";
15
16
  import { blockCapabilities, findWorkerDir, generateWorkerManifest, } from "./worker-manifest.js";
16
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -114,6 +115,16 @@ async function main() {
114
115
  // Not an error — start the shell anyway; it shows "None" under Blocks.
115
116
  console.log(`${label(basename(workerDir))} Worker declares no custom blocks.`);
116
117
  }
118
+ // Hand-authored data sources under the worker's src/data — see the
119
+ // scaffolded AGENTS.md for the file format. Validated before injection; a
120
+ // malformed file fails spin-up with the problem named.
121
+ const dataSources = readDataSources(resolve(workerDir, "src/data"));
122
+ console.log(dataSources.length > 0
123
+ ? `${label(basename(workerDir))} Data sources from src/data: ${dataSources
124
+ .map(source => source.name)
125
+ .join(", ")}`
126
+ : `${label(basename(workerDir))} No data sources — create src/data/<key>.json files ` +
127
+ `(format: node_modules/@notionhq/custom-blocks-dev-shell/docs/data-sources.md).`);
117
128
  if (blocks.length > 0 &&
118
129
  cliArgs.shellPort >= cliArgs.blockBasePort &&
119
130
  cliArgs.shellPort < cliArgs.blockBasePort + blocks.length) {
@@ -161,6 +172,7 @@ async function main() {
161
172
  await serveUi(distDir, cliArgs.shellPort, {
162
173
  mode: "worker",
163
174
  blocks: registry,
175
+ dataSources,
164
176
  });
165
177
  }
166
178
  catch (error) {
@@ -0,0 +1,72 @@
1
+ # Dev-shell data sources
2
+
3
+ Data sources come in kinds, marked by their `type`, and all live in the
4
+ worker's `src/data/*.json` — one source per file, read at spin-up. The shell
5
+ supports the following types today:
6
+
7
+ - `worker` — hand-authored JSON files. This is how you give your blocks data,
8
+ and what this document describes.
9
+ - `manual` — sources a user creates themselves in the shell.
10
+ - `built-in` — sources we provide.
11
+
12
+
13
+ ## File format
14
+
15
+ ```json
16
+ {
17
+ "type": "worker",
18
+ "key": "tasks",
19
+ "name": "Tasks",
20
+ "icon": "✅",
21
+ "schema": {
22
+ "title": { "name": "Title", "type": "title" },
23
+ "status": { "name": "Status", "type": "select" },
24
+ "dueDate": { "name": "Due date", "type": "date" },
25
+ "done": { "name": "Done", "type": "checkbox" }
26
+ },
27
+ "rows": [
28
+ {
29
+ "id": "tasks-1",
30
+ "title": "Ship the block",
31
+ "status": "In progress",
32
+ "dueDate": { "type": "date", "start_date": "2026-08-01" },
33
+ "done": false
34
+ }
35
+ ]
36
+ }
37
+ ```
38
+
39
+ - `type` — the source's kind; `"worker"`, the only kind these files can
40
+ declare today, is the default.
41
+ - `key` — what blocks bind against; make it match a `dataSources` key from
42
+ `worker.customBlock(...)` so properties auto-match. Defaults to the
43
+ filename.
44
+ - `name` — the label shown in the shell sidebar. Defaults to `key`.
45
+ - `icon` — optional emoji shown alongside the name.
46
+ - `schema` — property key → `{ name, type }`. Use the same property keys and
47
+ types the block declares. Types: `title`, `rich_text`, `number`, `select`,
48
+ `multi_select`, `status`, `date`, `checkbox`, `url`, `email`,
49
+ `phone_number`, `people`, `files`, `relation`.
50
+ - `rows` — each row needs a unique string `id`; property values live under
51
+ their schema keys.
52
+
53
+ Value shapes by property type:
54
+
55
+ | Type | Value |
56
+ | --- | --- |
57
+ | `title`, `rich_text`, `select`, `status`, `url`, `email`, `phone_number` | string |
58
+ | `multi_select` | array of strings |
59
+ | `number` | number |
60
+ | `checkbox` | boolean |
61
+ | `date` | `{ "type": "date", "start_date": "YYYY-MM-DD" }`; ranges and datetimes use `"daterange"` / `"datetime"` / `"datetimerange"` with `end_date`, `start_time`, `end_time` |
62
+ | `people` | array of `{ "id": "...", "table": "notion_user" }` pointers |
63
+ | `relation` | array of `{ "id": "...", "table": "block" }` pointers |
64
+ | `files` | string (the host serializes file properties as text today) |
65
+
66
+ ## Validation
67
+
68
+ Files are validated at spin-up, before they reach the shell UI. A malformed
69
+ file fails the run with the file and problem named, e.g.
70
+ `src/data/tasks.json: rows.0.id: Invalid key: Expected "id" but received undefined`.
71
+ Omitted `key`/`name` fall back to the filename; omitted `schema`/`rows` to
72
+ empty.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks-dev-shell",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Local preview shell for Notion custom block workers.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,11 +19,13 @@
19
19
  "bin",
20
20
  "dist",
21
21
  "dist-cli",
22
+ "docs",
22
23
  "README.md"
23
24
  ],
24
25
  "dependencies": {
25
26
  "react": "^19.2.5",
26
27
  "react-dom": "^19.2.5",
28
+ "valibot": "^1.3.1",
27
29
  "@notionhq/custom-blocks": "0.1.0"
28
30
  },
29
31
  "devDependencies": {