@notionhq/custom-blocks-dev-shell 0.1.9 → 0.1.11

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.
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink:#32302c;--color-ink-muted:#6b6b68;--color-ink-faint:#9b9a97;--color-ink-fainter:#b3b3b0;--color-nav-ink:#2c2c2b;--color-paper:#fbfbfa;--color-hairline:#e6e5e3;--color-ui-hover:#3b14000d;--color-panel:#42230308;--color-canvas-dark:#191919;--color-pill-dark:#ffffff0e;--color-dark-ink:#ffffffcf;--color-dark-ink-muted:#ffffff75;--color-sidebar:#f7f7f5;--color-sidebar-active:#00000008;--color-sidebar-ink:#5f5e59;--color-sidebar-ink-strong:#1d1b16;--color-sidebar-ink-muted:#7d7a75;--color-hairline-soft:#f0efed;--color-toggle:#e9e9e7;--color-accent:#2383e2;--color-mismatch-ink:#a33}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-1\/2{top:50%}.top-full{top:100%}.right-0{right:calc(var(--spacing) * 0)}.right-1\.5{right:calc(var(--spacing) * 1.5)}.bottom-full{bottom:100%}.left-0{left:calc(var(--spacing) * 0)}.z-50{z-index:50}.row-1{grid-row:1}.row-2{grid-row:2}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.block{display:block}.flex{display:flex}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-9{height:calc(var(--spacing) * 9)}.h-100{height:calc(var(--spacing) * 100)}.h-\[30px\]{height:30px}.h-\[38px\]{height:38px}.h-screen{height:100vh}.max-h-\[85vh\]{max-height:85vh}.min-h-0{min-height:calc(var(--spacing) * 0)}.w-70{width:calc(var(--spacing) * 70)}.w-\[172px\]{width:172px}.w-full{width:100%}.max-w-190{max-width:calc(var(--spacing) * 190)}.max-w-\[720px\]{max-width:720px}.max-w-\[960px\]{max-width:960px}.max-w-full{max-width:100%}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-40{min-width:calc(var(--spacing) * 40)}.flex-1{flex:1}.flex-none{flex:none}.shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-disc{list-style-type:disc}.appearance-none{appearance:none}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-px{gap:1px}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-\[4px\]{border-radius:4px}.rounded-\[6px\]{border-radius:6px}.rounded-\[10px\]{border-radius:10px}.rounded-\[12px\]{border-radius:12px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-hairline{border-color:var(--color-hairline)}.border-hairline-soft{border-color:var(--color-hairline-soft)}.bg-accent{background-color:var(--color-accent)}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab, red, red)){.bg-black\/40{background-color:color-mix(in oklab, var(--color-black) 40%, transparent)}}.bg-canvas-dark{background-color:var(--color-canvas-dark)}.bg-panel{background-color:var(--color-panel)}.bg-paper{background-color:var(--color-paper)}.bg-pill-dark{background-color:var(--color-pill-dark)}.bg-sidebar{background-color:var(--color-sidebar)}.bg-sidebar-active{background-color:var(--color-sidebar-active)}.bg-toggle{background-color:var(--color-toggle)}.bg-ui-hover{background-color:var(--color-ui-hover)}.bg-white{background-color:var(--color-white)}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-10{padding-inline:calc(var(--spacing) * 10)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.pt-10{padding-top:calc(var(--spacing) * 10)}.pt-24{padding-top:calc(var(--spacing) * 24)}.pr-1{padding-right:calc(var(--spacing) * 1)}.pr-1\.5{padding-right:calc(var(--spacing) * 1.5)}.pr-6{padding-right:calc(var(--spacing) * 6)}.pb-1{padding-bottom:calc(var(--spacing) * 1)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-6{padding-bottom:calc(var(--spacing) * 6)}.pl-0\.5{padding-left:calc(var(--spacing) * .5)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-5{padding-left:calc(var(--spacing) * 5)}.text-center{text-align:center}.text-left{text-align:left}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.text-body{letter-spacing:-.154px;font-size:14px;line-height:20px}.text-caption{font-size:12px;line-height:16px}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[17px\]{font-size:17px}.text-\[32px\]{font-size:32px}.text-\[40px\]{font-size:40px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-10{--tw-leading:calc(var(--spacing) * 10);line-height:calc(var(--spacing) * 10)}.leading-\[1\.2\]{--tw-leading:1.2;line-height:1.2}.leading-\[22px\]{--tw-leading:22px;line-height:22px}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[-0\.442px\]{--tw-tracking:-.442px;letter-spacing:-.442px}.tracking-\[-1\.55px\]{--tw-tracking:-1.55px;letter-spacing:-1.55px}.whitespace-nowrap{white-space:nowrap}.text-dark-ink{color:var(--color-dark-ink)}.text-dark-ink-muted{color:var(--color-dark-ink-muted)}.text-ink{color:var(--color-ink)}.text-ink-faint{color:var(--color-ink-faint)}.text-ink-fainter{color:var(--color-ink-fainter)}.text-ink-muted{color:var(--color-ink-muted)}.text-mismatch-ink{color:var(--color-mismatch-ink)}.text-nav-ink{color:var(--color-nav-ink)}.text-sidebar-ink{color:var(--color-sidebar-ink)}.text-sidebar-ink-muted{color:var(--color-sidebar-ink-muted)}.text-sidebar-ink-strong{color:var(--color-sidebar-ink-strong)}.text-white{color:var(--color-white)}.opacity-0{opacity:0}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_4px_12px_rgb\(0_0_0\/10\%\)\]{--tw-shadow:0 4px 12px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_12px_40px_rgb\(0_0_0\/25\%\)\]{--tw-shadow:0 12px 40px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){.hover\:bg-sidebar-active:hover{background-color:var(--color-sidebar-active)}.hover\:bg-ui-hover:hover{background-color:var(--color-ui-hover)}.hover\:opacity-90:hover{opacity:.9}}.focus-visible\:opacity-100:focus-visible{opacity:1}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
package/dist/index.html CHANGED
@@ -4,8 +4,8 @@
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-CHk4nghz.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-x1e1jng3.css">
7
+ <script type="module" crossorigin src="/assets/index-BWWoeYho.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-D7A1CDPg.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -74,6 +74,14 @@ export default {
74
74
  ...resolved,
75
75
  root: here("${toRoot}"),
76
76
  cacheDir: here("${toCache}"),
77
+ define: {
78
+ ...(resolved.define ?? {}),
79
+ // Initialization handshake scenarios fixture uses this key to identify the
80
+ // selected scenario, via the custom block capability key. The key needs to be
81
+ // serialized as a JavaScript string literal, then serialized again for this
82
+ // generated config.
83
+ "import.meta.env.VITE_CUSTOM_BLOCK_KEY": ${JSON.stringify(JSON.stringify(key))},
84
+ },
77
85
  plugins: [serveWorkerManifest, ...(resolved.plugins ?? [])],
78
86
  }
79
87
  `);
@@ -0,0 +1,241 @@
1
+ import * as v from "valibot";
2
+ import { unreachable } from "./utils.js";
3
+ /**
4
+ * Convert one public API property value object to a local data source value.
5
+ * Returns undefined for empty values (blank cells are normal API output; the
6
+ * key is omitted from the row).
7
+ */
8
+ export function convertPublicApiPropertyValue(publicType, value) {
9
+ if (typeof value !== "object" || value === null) {
10
+ return undefined;
11
+ }
12
+ const payload = value[publicType];
13
+ if (payload === null || payload === undefined) {
14
+ return undefined;
15
+ }
16
+ switch (publicType) {
17
+ case "title":
18
+ case "rich_text":
19
+ return payload
20
+ .map(part => part.plain_text ?? "")
21
+ .join("");
22
+ case "number":
23
+ return payload;
24
+ case "checkbox":
25
+ return payload;
26
+ case "url":
27
+ case "email":
28
+ case "phone_number":
29
+ return payload;
30
+ case "select":
31
+ case "status":
32
+ return payload.name;
33
+ case "multi_select":
34
+ return payload.map(option => option.name ?? "");
35
+ case "date":
36
+ return publicDateToLocal(payload);
37
+ case "people":
38
+ return payload.map(user => ({
39
+ id: user.id,
40
+ table: "notion_user",
41
+ }));
42
+ case "files":
43
+ return filesToText(payload);
44
+ case "relation":
45
+ return payload.map(target => ({
46
+ id: target.id,
47
+ table: "block",
48
+ }));
49
+ case "unique_id": {
50
+ const uniqueId = payload;
51
+ return uniqueId.prefix
52
+ ? `${uniqueId.prefix}-${uniqueId.number}`
53
+ : String(uniqueId.number);
54
+ }
55
+ // The production host still serializes formula, rollup, and
56
+ // last_visited_time as plain text. Match it so fixtures predict what a
57
+ // block receives from the real host.
58
+ case "formula":
59
+ return formulaToText(payload);
60
+ case "rollup":
61
+ return rollupToText(payload);
62
+ case "last_visited_time":
63
+ return payload;
64
+ case "created_time":
65
+ case "last_edited_time":
66
+ return timestampToLocal(payload);
67
+ case "created_by":
68
+ case "last_edited_by":
69
+ return [{ id: payload.id, table: "notion_user" }];
70
+ case "verification":
71
+ return payload.state;
72
+ case "button":
73
+ // Buttons carry no data; there is nothing to sample.
74
+ return undefined;
75
+ case "place":
76
+ case "location":
77
+ // No public API payload converter yet: keep something inspectable.
78
+ return JSON.stringify(payload);
79
+ default:
80
+ return unreachable(publicType);
81
+ }
82
+ }
83
+ function filesToText(files) {
84
+ return files
85
+ .map(file => file.name ?? "")
86
+ .filter(name => name.length > 0)
87
+ .join(", ");
88
+ }
89
+ function formulaToText(formula) {
90
+ switch (formula.type) {
91
+ case "string":
92
+ return formula.string ?? undefined;
93
+ case "number":
94
+ return formula.number != null ? String(formula.number) : undefined;
95
+ case "boolean":
96
+ return formula.boolean != null ? String(formula.boolean) : undefined;
97
+ case "date":
98
+ return formula.date ? dateToText(formula.date) : undefined;
99
+ default:
100
+ return undefined;
101
+ }
102
+ }
103
+ function rollupToText(rollup) {
104
+ switch (rollup.type) {
105
+ case "number":
106
+ return rollup.number != null ? String(rollup.number) : undefined;
107
+ case "date":
108
+ return rollup.date ? dateToText(rollup.date) : undefined;
109
+ default: {
110
+ const raw = rollup.type === undefined
111
+ ? undefined
112
+ : rollup[rollup.type];
113
+ return raw === null || raw === undefined ? undefined : JSON.stringify(raw);
114
+ }
115
+ }
116
+ }
117
+ /**
118
+ * Best-effort text for a public API date payload: matches production's
119
+ * string shape, not its exact date formatting.
120
+ */
121
+ function dateToText(date) {
122
+ return date.end ? `${date.start} → ${date.end}` : date.start;
123
+ }
124
+ /**
125
+ * Public API date value (`{ start, end, time_zone }`, ISO 8601 strings) to
126
+ * the local `NotionDateValue` shape. Date-only starts become `date` /
127
+ * `daterange`; timestamped starts become `datetime` / `datetimerange`.
128
+ */
129
+ function publicDateToLocal(date) {
130
+ const start = date.start;
131
+ const end = date.end ?? undefined;
132
+ const timeZone = date.time_zone ?? undefined;
133
+ if (!start.includes("T")) {
134
+ return end === undefined
135
+ ? { type: "date", start_date: start.slice(0, 10) }
136
+ : {
137
+ type: "daterange",
138
+ start_date: start.slice(0, 10),
139
+ end_date: end.slice(0, 10),
140
+ };
141
+ }
142
+ if (end === undefined) {
143
+ return {
144
+ type: "datetime",
145
+ start_date: start.slice(0, 10),
146
+ start_time: start.slice(11, 16),
147
+ ...(timeZone !== undefined ? { time_zone: timeZone } : {}),
148
+ };
149
+ }
150
+ return {
151
+ type: "datetimerange",
152
+ start_date: start.slice(0, 10),
153
+ start_time: start.slice(11, 16),
154
+ end_date: end.slice(0, 10),
155
+ end_time: end.slice(11, 16),
156
+ ...(timeZone !== undefined ? { time_zone: timeZone } : {}),
157
+ };
158
+ }
159
+ function timestampToLocal(timestamp) {
160
+ return timestamp.includes("T")
161
+ ? {
162
+ type: "datetime",
163
+ start_date: timestamp.slice(0, 10),
164
+ start_time: timestamp.slice(11, 16),
165
+ }
166
+ : { type: "date", start_date: timestamp.slice(0, 10) };
167
+ }
168
+ const stringValue = v.string();
169
+ const numberValue = v.pipe(v.number(), v.finite());
170
+ const booleanValue = v.boolean();
171
+ const stringArrayValue = v.array(v.string());
172
+ const pointerArrayValue = v.array(v.object({ id: v.string(), table: v.string() }));
173
+ const dateValue = v.variant("type", [
174
+ v.object({ type: v.literal("date"), start_date: v.string() }),
175
+ v.object({
176
+ type: v.literal("daterange"),
177
+ start_date: v.string(),
178
+ end_date: v.string(),
179
+ }),
180
+ v.object({
181
+ type: v.literal("datetime"),
182
+ start_date: v.string(),
183
+ start_time: v.string(),
184
+ }),
185
+ v.object({
186
+ type: v.literal("datetimerange"),
187
+ start_date: v.string(),
188
+ start_time: v.string(),
189
+ end_date: v.string(),
190
+ end_time: v.string(),
191
+ }),
192
+ ]);
193
+ const anyLocalValue = v.union([
194
+ stringValue,
195
+ numberValue,
196
+ booleanValue,
197
+ dateValue,
198
+ stringArrayValue,
199
+ pointerArrayValue,
200
+ ]);
201
+ const _valueUnionsMatch = true;
202
+ void _valueUnionsMatch;
203
+ /**
204
+ * The value shape each property type must hold in a local file. Mirrors the
205
+ * production host's serialization: the types production still serializes as
206
+ * text are pinned to strings here, deliberately stricter than the dev
207
+ * shell's binding-time checks.
208
+ */
209
+ const VALUE_SCHEMAS = {
210
+ title: stringValue,
211
+ rich_text: stringValue,
212
+ url: stringValue,
213
+ email: stringValue,
214
+ phone_number: stringValue,
215
+ select: stringValue,
216
+ status: stringValue,
217
+ number: numberValue,
218
+ checkbox: booleanValue,
219
+ multi_select: stringArrayValue,
220
+ date: dateValue,
221
+ created_time: dateValue,
222
+ last_edited_time: dateValue,
223
+ people: pointerArrayValue,
224
+ relation: pointerArrayValue,
225
+ created_by: pointerArrayValue,
226
+ last_edited_by: pointerArrayValue,
227
+ unique_id: stringValue,
228
+ files: stringValue,
229
+ formula: stringValue,
230
+ rollup: stringValue,
231
+ verification: stringValue,
232
+ place: stringValue,
233
+ location: stringValue,
234
+ last_visited_time: stringValue,
235
+ };
236
+ export function matchesPropertyType(value, type) {
237
+ return v.is(VALUE_SCHEMAS[type] ?? anyLocalValue, value);
238
+ }
239
+ export function isDataSourceValue(value) {
240
+ return v.is(anyLocalValue, value);
241
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * `convert` subcommand: translate a sampled Notion data source (public API
3
+ * shapes: schema `properties`, query `results`) into the dev shell's local
4
+ * JSON data source format, validate the result, and print it to stdout.
5
+ * `ntn customblocks sample` pipes through this via stdin; it also runs
6
+ * standalone: `npx @notionhq/custom-blocks-dev-shell convert --in sample.json`.
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+ import * as v from "valibot";
10
+ import { convertPublicApiPropertyValue, isDataSourceValue, matchesPropertyType, } from "./convert-values.js";
11
+ import { NOTION_PROPERTY_TYPES, sourceFileSchema } from "./data-sources.js";
12
+ import { claimUniqueKey, slugify } from "./utils.js";
13
+ const SUPPORTED_TYPES = new Set(NOTION_PROPERTY_TYPES);
14
+ /** The spelling of "text" in the local file format. */
15
+ const UNSUPPORTED_TYPE_FALLBACK = "rich_text";
16
+ export function parseConvertArgs(argv) {
17
+ const args = {
18
+ input: undefined,
19
+ key: undefined,
20
+ name: undefined,
21
+ };
22
+ const takeValue = (name, index) => {
23
+ const value = argv[index];
24
+ if (value === undefined || value.startsWith("--")) {
25
+ throw new Error(`${name} requires a value.`);
26
+ }
27
+ return value;
28
+ };
29
+ for (let index = 0; index < argv.length; index++) {
30
+ const arg = argv[index];
31
+ if (arg === "--in") {
32
+ args.input = takeValue("--in", ++index);
33
+ }
34
+ else if (arg.startsWith("--in=")) {
35
+ args.input = arg.slice("--in=".length);
36
+ }
37
+ else if (arg === "--key") {
38
+ args.key = takeValue("--key", ++index);
39
+ }
40
+ else if (arg.startsWith("--key=")) {
41
+ args.key = arg.slice("--key=".length);
42
+ }
43
+ else if (arg === "--name") {
44
+ args.name = takeValue("--name", ++index);
45
+ }
46
+ else if (arg.startsWith("--name=")) {
47
+ args.name = arg.slice("--name=".length);
48
+ }
49
+ else {
50
+ throw new Error(`Unknown convert option "${arg}". Supported: --in <file>, --key <key>, --name <name>.`);
51
+ }
52
+ }
53
+ return args;
54
+ }
55
+ const sampleInputSchema = v.pipe(v.looseObject({
56
+ name: v.optional(v.string()),
57
+ properties: v.optional(v.record(v.string(), v.looseObject({ type: v.string() }))),
58
+ results: v.optional(v.array(v.looseObject({
59
+ id: v.string(),
60
+ properties: v.optional(v.record(v.string(), v.unknown())),
61
+ }))),
62
+ }), v.check(input => input.properties !== undefined || input.results !== undefined, "expected a schema under `properties` and/or sampled pages under `results`"));
63
+ /**
64
+ * Convert a sampled data source (public API shapes) into a local data source
65
+ * object. The input carries the schema under `properties` and/or sampled
66
+ * pages under `results` (query output verbatim). The emitted object always
67
+ * carries a `schema` — taken from `properties` when present, otherwise
68
+ * inferred from the row values — because the shell derives table columns and
69
+ * bindings from the schema alone.
70
+ */
71
+ export function convertSample(input, overrides = {}) {
72
+ const parsed = v.safeParse(sampleInputSchema, input);
73
+ if (!parsed.success) {
74
+ const issue = parsed.issues[0];
75
+ const path = v.getDotPath(issue);
76
+ throw new Error(`Input is not a sampled data source: ${path ? `${path}: ` : ""}${issue.message}`);
77
+ }
78
+ const sample = parsed.output;
79
+ const warnings = [];
80
+ const mappings = buildPropertyMappings(sample.properties, sample.results, warnings);
81
+ const key = overrides.key ?? slugify(sample.name ?? "") ?? "data_source";
82
+ const name = overrides.name ?? (sample.name || undefined) ?? key;
83
+ const schema = {};
84
+ for (const mapping of mappings.values()) {
85
+ schema[mapping.key] = { name: mapping.name, type: mapping.type };
86
+ }
87
+ const source = {
88
+ type: "syncedFromProd",
89
+ key,
90
+ name,
91
+ schema,
92
+ rows: (sample.results ?? []).map(row => convertRow(row, mappings)),
93
+ };
94
+ validateSource(source);
95
+ return { source, warnings };
96
+ }
97
+ /**
98
+ * One mapping per public property name. For schema-only and full samples the
99
+ * mappings come from `properties`; for rows-only samples they are derived
100
+ * from the `type` field each row value carries.
101
+ */
102
+ function buildPropertyMappings(properties, sampledRows, warnings) {
103
+ const mappings = new Map();
104
+ // `id` is the row identifier; a property named "ID" must not overwrite it.
105
+ const usedKeys = new Set(["id"]);
106
+ const add = (propertyName, publicType) => {
107
+ if (mappings.has(propertyName)) {
108
+ return;
109
+ }
110
+ const unsupported = !SUPPORTED_TYPES.has(publicType);
111
+ if (unsupported) {
112
+ warnings.push(`Property ${JSON.stringify(propertyName)}: public API type ` +
113
+ `${JSON.stringify(publicType)} is not supported by custom blocks; ` +
114
+ `converted to text ("${UNSUPPORTED_TYPE_FALLBACK}") with the ` +
115
+ `placeholder value ${JSON.stringify(`unsupported type: ${publicType}`)}.`);
116
+ }
117
+ mappings.set(propertyName, {
118
+ key: claimUniqueKey(slugify(propertyName) ?? "property", usedKeys),
119
+ name: propertyName,
120
+ type: unsupported ? UNSUPPORTED_TYPE_FALLBACK : publicType,
121
+ publicType,
122
+ unsupported,
123
+ });
124
+ };
125
+ for (const [propertyName, property] of Object.entries(properties ?? {})) {
126
+ add(propertyName, property.type);
127
+ }
128
+ for (const row of sampledRows ?? []) {
129
+ for (const [propertyName, value] of Object.entries(row.properties ?? {})) {
130
+ const publicType = value?.type;
131
+ if (typeof publicType === "string") {
132
+ add(propertyName, publicType);
133
+ }
134
+ }
135
+ }
136
+ return mappings;
137
+ }
138
+ function convertRow(row, mappings) {
139
+ const converted = { id: row.id };
140
+ for (const [propertyName, value] of Object.entries(row.properties ?? {})) {
141
+ const mapping = mappings.get(propertyName);
142
+ if (mapping === undefined) {
143
+ continue;
144
+ }
145
+ if (mapping.unsupported) {
146
+ converted[mapping.key] = `unsupported type: ${mapping.publicType}`;
147
+ continue;
148
+ }
149
+ const local = convertPublicApiPropertyValue(mapping.publicType, value);
150
+ if (local !== undefined) {
151
+ converted[mapping.key] = local;
152
+ }
153
+ }
154
+ return converted;
155
+ }
156
+ /**
157
+ * Validate the built source the same way the dev shell will at spin-up, plus
158
+ * a row-value pass the loader doesn't do: every emitted value must match its
159
+ * declared schema type. Failures here are conversion bugs or malformed input;
160
+ * the command must never print a file the shell would reject.
161
+ */
162
+ export function validateSource(source) {
163
+ const hasSchema = source.schema !== undefined;
164
+ const result = v.safeParse(sourceFileSchema, source);
165
+ if (!result.success) {
166
+ const issue = result.issues[0];
167
+ const path = v.getDotPath(issue);
168
+ throw new Error(`Converted data source is invalid: ${path ? `${path}: ` : ""}${issue.message}`);
169
+ }
170
+ const schema = (source.schema ?? {});
171
+ const rows = (source.rows ?? []);
172
+ const issues = [];
173
+ for (const row of rows) {
174
+ for (const [key, value] of Object.entries(row)) {
175
+ if (key === "id") {
176
+ continue;
177
+ }
178
+ const declared = hasSchema ? schema[key] : undefined;
179
+ if (hasSchema && declared === undefined) {
180
+ issues.push(`row ${JSON.stringify(row.id)}: ${JSON.stringify(key)} is not in the schema`);
181
+ continue;
182
+ }
183
+ const matches = declared
184
+ ? matchesPropertyType(value, declared.type)
185
+ : isDataSourceValue(value);
186
+ if (!matches) {
187
+ issues.push(`row ${JSON.stringify(row.id)}: ${JSON.stringify(key)} does not match ${declared ? `type ${JSON.stringify(declared.type)}` : "any supported value shape"}`);
188
+ }
189
+ }
190
+ }
191
+ if (issues.length > 0) {
192
+ throw new Error(`Converted rows are invalid: ${issues.join("; ")}`);
193
+ }
194
+ }
195
+ export async function runConvert(argv) {
196
+ const args = parseConvertArgs(argv);
197
+ let raw;
198
+ if (args.input !== undefined) {
199
+ try {
200
+ raw = readFileSync(args.input, "utf-8");
201
+ }
202
+ catch (error) {
203
+ throw new Error(`Could not read ${args.input}: ${error.message}`);
204
+ }
205
+ }
206
+ else {
207
+ if (process.stdin.isTTY) {
208
+ throw new Error("No input: pipe `ntn customblocks sample` output to convert, or pass --in <file>.");
209
+ }
210
+ raw = readFileSync(0, "utf-8");
211
+ }
212
+ let parsed;
213
+ try {
214
+ parsed = JSON.parse(raw);
215
+ }
216
+ catch (error) {
217
+ throw new Error(`Input is not valid JSON — ${error.message}`);
218
+ }
219
+ const { source, warnings } = convertSample(parsed, {
220
+ ...(args.key !== undefined ? { key: args.key } : {}),
221
+ ...(args.name !== undefined ? { name: args.name } : {}),
222
+ });
223
+ for (const warning of warnings) {
224
+ console.warn(warning);
225
+ }
226
+ process.stdout.write(`${JSON.stringify(source, null, "\t")}\n`);
227
+ }
@@ -17,7 +17,7 @@ const DEV_SHELL_DATA_SOURCE_TYPES = [
17
17
  // plain node, which can't import the SDK's published TS source at runtime.
18
18
  // The `satisfies` below plus the type-only import keep the two lists locked
19
19
  // together — a drift on either side fails typecheck.
20
- const NOTION_PROPERTY_TYPES = [
20
+ export const NOTION_PROPERTY_TYPES = [
21
21
  "title",
22
22
  "rich_text",
23
23
  "number",
@@ -57,8 +57,8 @@ export function readDataSources(dataDir) {
57
57
  .sort((left, right) => left.name.localeCompare(right.name));
58
58
  }
59
59
  const nonEmptyString = v.pipe(v.string(), v.nonEmpty());
60
- const sourceFileSchema = v.object({
61
- type: v.optional(v.literal("worker"), "worker"),
60
+ export const sourceFileSchema = v.object({
61
+ type: v.optional(v.picklist(DEV_SHELL_DATA_SOURCE_TYPES), "worker"),
62
62
  key: v.optional(nonEmptyString),
63
63
  name: v.optional(nonEmptyString),
64
64
  icon: v.optional(v.string()),
package/dist-cli/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Entry point for the published dev-shell CLI (`npx`-run from a worker
3
- * project). Mirrors the worker mode of the repo's `scripts/dev.ts`: build the
3
+ * project). Mirrors the repo's `scripts/dev.ts`: build the
4
4
  * worker, extract its manifest, serve one Vite dev server per custom block
5
5
  * (using the worker's own Vite install), and serve the prebuilt dev-shell UI
6
6
  * with the block registry injected at runtime.
@@ -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_PORT, writeBlockViteConfig, } from "./block-server.js";
14
+ import { runConvert } from "./convert.js";
14
15
  import { readDataSources } from "./data-sources.js";
15
16
  import { materializeWorkerSchemaDataSources } from "./materialize.js";
16
17
  import { serveUi } from "./serve-ui.js";
@@ -102,7 +103,14 @@ function resolveViteBin(workerDir) {
102
103
  }
103
104
  const procs = [];
104
105
  async function main() {
105
- const cliArgs = parseCliArgs(process.argv.slice(2));
106
+ const argv = process.argv.slice(2);
107
+ // One-shot subcommands return before any dev-server machinery starts;
108
+ // with no subcommand the CLI is the dev shell, as it always was.
109
+ if (argv[0] === "convert") {
110
+ await runConvert(argv.slice(1));
111
+ return;
112
+ }
113
+ const cliArgs = parseCliArgs(argv);
106
114
  const workerDir = resolveWorkerDir(cliArgs.worker);
107
115
  if (!existsSync(resolve(workerDir, "node_modules"))) {
108
116
  throw new Error(`No node_modules in ${workerDir}. Install the worker's dependencies first ` +
@@ -172,7 +180,6 @@ async function main() {
172
180
  }
173
181
  try {
174
182
  await serveUi(distDir, cliArgs.shellPort, {
175
- mode: "worker",
176
183
  blocks: registry,
177
184
  dataSources,
178
185
  });
@@ -218,7 +225,7 @@ function shutdown(signal) {
218
225
  return;
219
226
  }
220
227
  shuttingDown = true;
221
- if (signal !== "exit") {
228
+ if (signal !== "exit" && procs.length > 0) {
222
229
  console.log(`\n${dim}Shutting down...${reset}`);
223
230
  }
224
231
  for (const p of procs) {
@@ -16,7 +16,7 @@
16
16
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
17
17
  import { resolve } from "node:path";
18
18
  import { blockCapabilities } from "./worker-manifest.js";
19
- // Worker authoring type names → public data-source (NotionPropertyType) names.
19
+ // Worker authoring type names → public data source (NotionPropertyType) names.
20
20
  const TYPE_ALIASES = {
21
21
  text: "rich_text",
22
22
  file: "files",
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Throws when a discriminated union switch is not exhaustive.
3
+ */
4
+ export function unreachable(value) {
5
+ throw new Error(`[custom-blocks-dev-shell] Unexpected value encountered: ${JSON.stringify(value)}`);
6
+ }
7
+ /** Lowercase, non-alphanumerics collapsed to `_`; undefined when nothing survives. */
8
+ export function slugify(name) {
9
+ const slug = name
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, "_")
12
+ .replace(/^_+|_+$/g, "");
13
+ return slug.length > 0 ? slug : undefined;
14
+ }
15
+ /**
16
+ * Claim `base` as a key in `used`, suffixing `_2`, `_3`, … while it is
17
+ * already taken. Mutates `used` so later claims see this one.
18
+ */
19
+ export function claimUniqueKey(base, used) {
20
+ let key = base;
21
+ for (let counter = 2; used.has(key); counter++) {
22
+ key = `${base}_${counter}`;
23
+ }
24
+ used.add(key);
25
+ return key;
26
+ }
package/docs/bindings.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The shell lists your worker's blocks in the sidebar and renders the selected
4
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
5
+ until every data source slot the block declares is connected to a source and
6
6
  every declared property is mapped.
7
7
 
8
8
  ## Connecting data