@notionhq/custom-blocks-dev-shell 0.1.65 → 0.1.67

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-BtBedBtp.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-jvcNWLYF.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-BOt07-3a.css">
9
9
  </head>
10
10
  <body>
@@ -17,11 +17,10 @@ const UNSUPPORTED_TYPE_FALLBACK = "rich_text";
17
17
  export function parseConvertArgs(argv) {
18
18
  const args = {
19
19
  input: undefined,
20
- key: undefined,
21
20
  name: undefined,
22
21
  };
23
22
  const rejectUnknown = (arg) => {
24
- throw new Error(`Unknown convert option "${arg}". Supported: --in <file>, --key <key>, --name <name>.`);
23
+ throw new Error(`Unknown convert option "${arg}". Supported: --in <file>, --name <name>.`);
25
24
  };
26
25
  parseCliArgs({
27
26
  argv,
@@ -29,9 +28,6 @@ export function parseConvertArgs(argv) {
29
28
  "--in": value => {
30
29
  args.input = value;
31
30
  },
32
- "--key": value => {
33
- args.key = value;
34
- },
35
31
  "--name": value => {
36
32
  args.name = value;
37
33
  },
@@ -68,16 +64,14 @@ export function convertSample(input, overrides = {}) {
68
64
  const sample = parsed.output;
69
65
  const warnings = [];
70
66
  const mappings = buildPropertyMappings(sample.properties, sample.results, warnings);
71
- const key = overrides.key ?? slugify(sample.name ?? "") ?? "data_source";
72
- const name = overrides.name ?? (sample.name || undefined) ?? key;
67
+ const name = overrides.name ?? (sample.name || undefined);
73
68
  const schema = {};
74
69
  for (const mapping of mappings.values()) {
75
70
  schema[mapping.key] = { name: mapping.name, type: mapping.type };
76
71
  }
77
72
  const source = {
78
73
  type: "syncedFromProd",
79
- key,
80
- name,
74
+ ...(name !== undefined ? { name } : {}),
81
75
  schema,
82
76
  rows: (sample.results ?? []).map(row => convertRow(row, mappings)),
83
77
  };
@@ -207,7 +201,6 @@ export async function runConvert(argv) {
207
201
  throw new Error(`Input is not valid JSON — ${formatUnknownError(error)}`);
208
202
  }
209
203
  const { source, warnings } = convertSample(parsed, {
210
- ...(args.key !== undefined ? { key: args.key } : {}),
211
204
  ...(args.name !== undefined ? { name: args.name } : {}),
212
205
  });
213
206
  for (const warning of warnings) {
@@ -60,7 +60,6 @@ export function readDataSources(dataDir) {
60
60
  const nonEmptyString = v.pipe(v.string(), v.nonEmpty());
61
61
  export const sourceFileSchema = v.object({
62
62
  type: v.optional(v.picklist(DEV_SHELL_DATA_SOURCE_TYPES), "worker"),
63
- key: v.optional(nonEmptyString),
64
63
  name: v.optional(nonEmptyString),
65
64
  icon: v.optional(v.string()),
66
65
  schema: v.optional(v.record(v.string(), v.object({
@@ -71,7 +70,7 @@ export const sourceFileSchema = v.object({
71
70
  });
72
71
  /**
73
72
  * 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
73
+ * and problem named; the filename defines identity and the default `name`, and
75
74
  * omitted `schema`/`rows` to empty — so the minimal valid file is `{}`.
76
75
  */
77
76
  function readSource(file) {
@@ -93,12 +92,11 @@ function readSource(file) {
93
92
  }
94
93
  const { type, icon, schema, rows } = result.output;
95
94
  const filename = basename(file, ".json");
96
- const key = result.output.key ?? filename;
97
95
  return {
98
96
  type,
99
- key,
97
+ key: filename,
100
98
  filename,
101
- name: result.output.name ?? key,
99
+ name: result.output.name ?? filename,
102
100
  ...(icon !== undefined ? { icon } : {}),
103
101
  schema: Object.fromEntries(Object.entries(schema).map(([propertyKey, property]) => [
104
102
  propertyKey,
@@ -14,6 +14,8 @@ import { formatUnknownErrorWithStack } from "./errors.js";
14
14
  import { materializeWorkerSchemaDataSources } from "./materialize.js";
15
15
  import { BLOCK_BASE_PORT, parsePort, SHELL_PORT, validateBlockPortRange, validateShellPort, } from "./ports.js";
16
16
  import { installProcessSignalHandlers, ProcessSupervisor, } from "./process-supervisor.js";
17
+ import { resolveLaunchBindings } from "./resolve-bindings.js";
18
+ import { parseBindingOverride, } from "./source-bindings.js";
17
19
  import { parseCliArgs } from "./utils.js";
18
20
  import { writeBlockViteConfig } from "./vite-block-server.js";
19
21
  import { blockCapabilities, findWorkerDir, generateWorkerManifest, } from "./worker-manifest.js";
@@ -59,11 +61,15 @@ export function parseWorkerLaunchArgs(argv) {
59
61
  worker: undefined,
60
62
  shellPort: SHELL_PORT,
61
63
  blockBasePort: BLOCK_BASE_PORT,
64
+ bindings: [],
62
65
  };
63
66
  let hasPositionalWorker = false;
64
67
  parseCliArgs({
65
68
  argv,
66
69
  handlers: {
70
+ "--bind": value => {
71
+ args.bindings.push(parseBindingOverride(value));
72
+ },
67
73
  "--worker": value => {
68
74
  if (value.length === 0) {
69
75
  throw new Error("--worker requires a path to a worker directory.");
@@ -83,7 +89,7 @@ export function parseWorkerLaunchArgs(argv) {
83
89
  },
84
90
  onUnknown: arg => {
85
91
  throw new Error(`Unknown dev shell option "${arg}". Supported: --worker <dir>, ` +
86
- `--port <port>, --block-base-port <port>.`);
92
+ `--port <port>, --block-base-port <port>, --bind <capabilityKey>.<dataSourceKey>=<sampleDataFileName>.`);
87
93
  },
88
94
  onPositional: arg => {
89
95
  // The published launcher passes its optional worker path positionally.
@@ -186,6 +192,13 @@ async function prepareWorkerLaunch(options) {
186
192
  const plan = await prepareWorkerLaunchPlan(workerDir, {
187
193
  blockBasePort: args.blockBasePort,
188
194
  });
195
+ const { registryBindings } = resolveLaunchBindings(plan.blocks.map(block => block.capability), plan.dataSources, args.bindings);
196
+ for (const entry of plan.registry) {
197
+ const bindings = registryBindings.get(entry.key);
198
+ if (bindings !== undefined) {
199
+ entry.sampleFilesByDataSource = bindings;
200
+ }
201
+ }
189
202
  logWorkerPlan(workerName, plan);
190
203
  validateShellPort(args.shellPort, args.blockBasePort, plan.blocks.length);
191
204
  const shell = await options.createShellLaunch({
@@ -23,8 +23,7 @@ const TYPE_ALIASES = {
23
23
  };
24
24
  /**
25
25
  * Generated files are namespaced so they never shadow authored files. The
26
- * filename and `key` carry the prefix keys must stay unique for bindings to
27
- * be unambiguous — while the display name stays as declared, so the sidebar
26
+ * filename carries the prefix, while the display name stays as declared, so the sidebar
28
27
  * shows the clean name.
29
28
  */
30
29
  const WORKER_PREFIX = "worker_";
@@ -69,14 +68,12 @@ export function materializeWorkerSchemaDataSources(manifest, dataDir) {
69
68
  }
70
69
  mkdirSync(dataDir, { recursive: true });
71
70
  for (const [key, descriptor] of descriptors) {
72
- // The key is the worker's contract and lands verbatim in the file's
73
- // `key`; the filename is ours, so path-hostile characters (`/`, `..`)
74
- // are encoded rather than allowed to leave `dataDir` or crash the write.
71
+ // Replace path-hostile characters so the filename stays inside `dataDir`.
75
72
  const file = resolve(dataDir, `${WORKER_PREFIX}${fileSafe(key)}.json`);
76
73
  if (existsSync(file)) {
77
74
  continue;
78
75
  }
79
- writeFileSync(file, `${JSON.stringify(buildSource(key, descriptor), null, 2)}\n`);
76
+ writeFileSync(file, `${JSON.stringify(buildSource(descriptor), null, 2)}\n`);
80
77
  }
81
78
  }
82
79
  function fileSafe(key) {
@@ -90,7 +87,7 @@ function samePropertySchemas(left, right) {
90
87
  left[key].type === right[key].type &&
91
88
  (left[key].name ?? key) === (right[key].name ?? key)));
92
89
  }
93
- function buildSource(key, descriptor) {
90
+ function buildSource(descriptor) {
94
91
  const { name, properties } = descriptor;
95
92
  const schema = {};
96
93
  for (const [propertyKey, property] of Object.entries(properties)) {
@@ -101,7 +98,6 @@ function buildSource(key, descriptor) {
101
98
  }
102
99
  return {
103
100
  type: "worker",
104
- key: `${WORKER_PREFIX}${key}`,
105
101
  name,
106
102
  schema,
107
103
  rows: [],
@@ -2,21 +2,18 @@
2
2
  * Copy the shell's pre-built sources (`data/*.json`, shipped in the
3
3
  * package) into a worker's data directory. Like materialization, existing
4
4
  * worker data always wins: a bundled source is skipped when its filename is
5
- * already taken, and also when its `key` is already claimed by any existing
6
- * source — `key` is a source's binding/lookup identity, so a duplicate would
7
- * make one of the two ambiguous.
5
+ * already taken. The filename defines the source identity.
8
6
  */
9
7
  import { copyFileSync, existsSync, mkdirSync } from "node:fs";
10
8
  import { resolve } from "node:path";
11
9
  import { readDataSources } from "./data-sources.js";
12
10
  export function copyPrebuiltDataSources(prebuiltDir, dataDir) {
13
11
  mkdirSync(dataDir, { recursive: true });
14
- const existingKeys = new Set(readDataSources(dataDir).map(source => source.key));
15
12
  const written = [];
16
13
  for (const source of readDataSources(prebuiltDir)) {
17
14
  const filename = `${source.filename}.json`;
18
15
  const target = resolve(dataDir, filename);
19
- if (existsSync(target) || existingKeys.has(source.key)) {
16
+ if (existsSync(target)) {
20
17
  continue;
21
18
  }
22
19
  copyFileSync(resolve(prebuiltDir, filename), target);
@@ -0,0 +1,39 @@
1
+ /** Validate explicit file selections before servers start. */
2
+ export function resolveLaunchBindings(blocks, sources, overrides) {
3
+ const targets = blocks.flatMap(block => Object.entries(block.config.manifest.dataSources).map(([slotKey]) => ({
4
+ blockKey: block.key,
5
+ slotKey,
6
+ target: `${block.key}.${slotKey}`,
7
+ })));
8
+ const explicit = validateOverrides(targets.map(candidate => candidate.target), sources, overrides);
9
+ const registryBindings = new Map();
10
+ for (const { blockKey, slotKey, target } of targets) {
11
+ const source = explicit.get(target);
12
+ if (source === undefined) {
13
+ continue;
14
+ }
15
+ const bindings = registryBindings.get(blockKey) ??
16
+ Object.create(null);
17
+ bindings[slotKey] = `${source.filename}.json`;
18
+ registryBindings.set(blockKey, bindings);
19
+ }
20
+ return { registryBindings };
21
+ }
22
+ function validateOverrides(targets, sources, overrides) {
23
+ const explicit = new Map();
24
+ for (const { target, filename } of overrides) {
25
+ const matches = targets.filter(candidate => candidate === target);
26
+ if (matches.length !== 1) {
27
+ throw new Error(`${matches.length === 0 ? "Unknown" : "Ambiguous"} binding target "${target}". Available targets: ${targets.join(", ") || "(none)"}.`);
28
+ }
29
+ if (explicit.has(target)) {
30
+ throw new Error(`Duplicate --bind target "${target}". Specify each target once.`);
31
+ }
32
+ const source = sources.find(candidate => `${candidate.filename}.json` === filename);
33
+ if (source === undefined) {
34
+ throw new Error(`Cannot bind ${target}: data/${filename} does not exist. Available files: ${sources.map(candidate => `${candidate.filename}.json`).join(", ") || "(none)"}.`);
35
+ }
36
+ explicit.set(target, source);
37
+ }
38
+ return explicit;
39
+ }
@@ -0,0 +1,17 @@
1
+ export function parseBindingOverride(value) {
2
+ const separator = value.indexOf("=");
3
+ const target = value.slice(0, separator);
4
+ const filename = value.slice(separator + 1);
5
+ const dot = target.indexOf(".");
6
+ if (separator < 0 ||
7
+ dot <= 0 ||
8
+ dot === target.length - 1 ||
9
+ target !== target.trim() ||
10
+ filename !== filename.trim() ||
11
+ !filename.endsWith(".json") ||
12
+ filename === ".json" ||
13
+ /[\\/\0]/.test(filename)) {
14
+ throw new Error("--bind requires <capabilityKey>.<dataSourceKey>=<sampleDataFileName.json>. Use a filename in data/.");
15
+ }
16
+ return { target, filename };
17
+ }
package/docs/bindings.md CHANGED
@@ -1,36 +1,60 @@
1
- # Dev-shell bindings
1
+ # Dev shell bindings
2
2
 
3
- The shell lists your worker's blocks in the sidebar and renders the selected
4
- block in a mock Notion host. Blocks start fully unbound: nothing renders
5
- until every data source slot the block declares is connected to a source and
6
- every declared property is mapped.
3
+ The dev shell previews your worker's custom blocks with local data from
4
+ `data/*.json`. Each data source selects its file in this order:
5
+
6
+ 1. The explicit `--bind` selection.
7
+ 2. `data/worker_<dataSourceKey>.json`, if it exists.
8
+ 3. `data/<dataSourceKey>.json`, if it exists.
9
+ 4. Otherwise, it stays unbound. The shell starts, but the block cannot initialize
10
+ until you connect the source in **Data**.
11
+
12
+ An invalid explicit selection stops startup instead of falling back to another
13
+ file. An empty file still takes precedence over later choices.
14
+
15
+ ## Choose sample data
16
+
17
+ Use `--bind <capabilityKey>.<dataSourceKey>=<sampleDataFileName>` to choose a source:
18
+
19
+ ```bash
20
+ ntn workers customblocks dev -- --bind task-board.tasks=sample_tasks.json
21
+ ```
22
+
23
+ This connects the `tasks` data source on `task-board` to `data/sample_tasks.json`.
24
+ Use a filename from `data/`, including `.json`.
25
+
26
+ Repeat `--bind` for additional data sources. Sources without an override keep
27
+ their defaults. When running `custom-blocks-dev-shell` directly, omit the `--`
28
+ separator.
29
+
30
+ An unknown target, duplicate target, or missing file stops startup.
31
+ Invalid JSON or an invalid source file also stops startup.
32
+
33
+ The browser maps compatible properties automatically. Incomplete mappings do
34
+ not stop the shell. Open **Data** to finish connecting the block.
35
+ Empty rows are valid.
7
36
 
8
37
  ## Connecting data
9
38
 
10
- **Connect data** in the toolbar opens the bindings modal: one section
39
+ **Data** in the toolbar opens the bindings modal: one section
11
40
  per slot from the block's `worker.customBlock({ dataSources })` declaration,
12
41
  each with a source picker and a per-property mapping. The sources on offer
13
42
  are the ones in your worker's `data/*.json`
14
- (format: [`data-sources.md`](./data-sources.md)); the toolbar shows a chip
15
- per slot with its current binding state.
43
+ (format: [`data-sources.md`](./data-sources.md)). The modal shows each slot
44
+ and its property mappings.
16
45
 
17
- Picking a source auto-maps its properties. For each manifest property, only
18
- schema properties of the same type are candidates. A property is a candidate
19
- on an exact ID/key match (no trim or case folding) or a display name match
20
- after trim and lowercasing. Bind when there is exactly one candidate;
21
- anything ambiguous or absent stays unmapped. Re-picking a source resets the
22
- slot's mappings and re-runs auto-map. Manual mapping offers only source
23
- properties of exactly the required type — no coercion.
46
+ Properties map automatically when there is a unique source property with a
47
+ matching key or name and the same type. Use the property pickers to adjust
48
+ mappings. Selecting a different source resets its property mappings.
24
49
 
25
50
  ## Initialization & binding edits
26
51
 
27
52
  Once every slot is bound and every property mapped, the block initializes and
28
- renders. Any binding edit restarts the block: the shell recreates the block's
29
- iframe, and the fresh initialization carries the new bindings. A block that
30
- failed to initialize recovers the same way — fix its bindings and save.
53
+ renders. Saving binding changes restarts the block with the updated data.
54
+ If initialization fails, fix the bindings and save.
31
55
 
32
56
  Bindings last for the page load, per block: refresh the shell and every block
33
- returns to unbound.
57
+ returns to its CLI selection or filename default.
34
58
 
35
59
  The shell reads `data/*.json` once at spin-up. To pick up file edits,
36
60
  restart the dev-shell command; a browser refresh is not enough.
@@ -11,10 +11,11 @@ supports the following types today:
11
11
 
12
12
  ## File format
13
13
 
14
+ For example, `data/tasks.json`:
15
+
14
16
  ```json
15
17
  {
16
18
  "type": "worker",
17
- "key": "tasks",
18
19
  "name": "Tasks",
19
20
  "icon": "✅",
20
21
  "schema": {
@@ -37,17 +38,16 @@ supports the following types today:
37
38
 
38
39
  - `type` — the source's kind. Hand-authored files declare `"worker"` (the
39
40
  default); the pre-built sources the shell adds carry `"built-in"`.
40
- - `key` — what blocks bind against; make it match a `dataSources` key from
41
- `worker.customBlock(...)` so properties auto-match. Defaults to the
42
- filename.
43
- - `name` — the label shown in the shell sidebar. Defaults to `key`.
41
+ - `name` — the label shown in the shell sidebar. If omitted, it defaults to
42
+ the filename without `.json`.
44
43
  - `icon` — optional emoji shown alongside the name.
45
- - `schema` — property key `{ name, type }`. Use the same property keys and
44
+ - `schema` — maps each property key to `{ name, type }`. If omitted, it defaults
45
+ to `{}`. Use the same property keys and
46
46
  types the block declares. Types: `title`, `rich_text`, `number`, `select`,
47
47
  `multi_select`, `status`, `date`, `checkbox`, `url`, `email`,
48
48
  `phone_number`, `people`, `files`, `relation`.
49
- - `rows` — each row needs a unique string `id`; property values live under
50
- their schema keys.
49
+ - `rows` — the source records. If omitted, it defaults to `[]`. Each row needs
50
+ a unique string `id`. Store property values under their schema keys.
51
51
 
52
52
  Value shapes by property type:
53
53
 
@@ -67,8 +67,8 @@ Value shapes by property type:
67
67
  Files are validated at spin-up, before they reach the shell UI. A malformed
68
68
  file fails the run with the file and problem named, e.g.
69
69
  `data/tasks.json: rows.0.id: Invalid key: Expected "id" but received undefined`.
70
- Omitted `key`/`name` fall back to the filename; omitted `schema`/`rows` to
71
- empty.
70
+ The filename without `.json` is the source identity used by bindings and
71
+ source pickers. Renaming a file changes its identity.
72
72
 
73
73
  ## Querying
74
74
 
@@ -86,7 +86,7 @@ materializes a file for every data source the worker declares — both
86
86
  schemas — that doesn't already have one: `data/worker_<key>.json`, the declared
87
87
  schema with empty `rows`, mirroring a managed database before any sync.
88
88
 
89
- - The `worker_` prefix on the filename and `key` namespaces generated files
89
+ - The `worker_` prefix on the filename namespaces generated files
90
90
  away from ones you author yourself, so both coexist; the display `name`
91
91
  stays as declared, so the sidebar shows the clean name.
92
92
  - Files are only ever written when absent: fill in `rows` freely and your
@@ -94,3 +94,21 @@ schema with empty `rows`, mirroring a managed database before any sync.
94
94
  schema.
95
95
 
96
96
  The shell shows exactly the sources in `data/`, nothing else.
97
+
98
+ ## Sample rows into a generated source
99
+
100
+ To populate a declared slot, use the generated file as the sample target:
101
+
102
+ ```bash
103
+ ntn workers customblocks sample <url-or-data-source-id> \
104
+ --out data/worker_tasks.json --row-only
105
+ ```
106
+
107
+ Run the dev shell once to create the generated file, then stop it before sampling.
108
+ `--row-only` preserves the schema and appends rows with new IDs. Add `--overwrite`
109
+ to replace existing rows.
110
+
111
+ Sampled property keys must match the generated schema. If they differ, sample
112
+ into a separate file and select it with [`--bind`](./bindings.md#choose-sample-data).
113
+
114
+ Restart the dev shell after sampling to load the updated data.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks-dev-shell",
3
- "version": "0.1.65",
3
+ "version": "0.1.67",
4
4
  "description": "Local preview shell for Notion custom block workers.",
5
5
  "license": "MIT",
6
6
  "type": "module",