@notionhq/custom-blocks-dev-shell 0.1.0 → 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/README.md +11 -0
- package/bin/cli.js +12 -34
- package/dist/assets/index-B1zP6MFl.js +9 -0
- package/dist/index.html +1 -1
- package/{cli/block-server.ts → dist-cli/block-server.js} +27 -54
- package/dist-cli/data-sources.js +107 -0
- package/dist-cli/main.js +247 -0
- package/dist-cli/serve-ui.js +81 -0
- package/dist-cli/worker-manifest.js +118 -0
- package/docs/data-sources.md +72 -0
- package/package.json +5 -4
- package/cli/main.ts +0 -293
- package/cli/serve-ui.ts +0 -100
- package/cli/worker-manifest.ts +0 -196
- package/dist/assets/index-BCi_w7Gk.js +0 -9
|
@@ -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.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Local preview shell for Notion custom block workers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,14 +17,15 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"bin",
|
|
20
|
-
"cli",
|
|
21
20
|
"dist",
|
|
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",
|
|
27
|
-
"
|
|
28
|
+
"valibot": "^1.3.1",
|
|
28
29
|
"@notionhq/custom-blocks": "0.1.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
43
|
"dev": "vite",
|
|
43
|
-
"build": "vite build",
|
|
44
|
+
"build": "vite build && tsc --project tsconfig.cli.json",
|
|
44
45
|
"test": "vitest run",
|
|
45
46
|
"typecheck": "tsc --noEmit && tsc --noEmit --project tsconfig.cli.json"
|
|
46
47
|
}
|
package/cli/main.ts
DELETED
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
/**
|
|
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
|
|
4
|
-
* worker, extract its manifest, serve one Vite dev server per custom block
|
|
5
|
-
* (using the worker's own Vite install), and serve the prebuilt dev-shell-2 UI
|
|
6
|
-
* with the block registry injected at runtime.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { spawn } from "node:child_process"
|
|
10
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
11
|
-
import { createRequire } from "node:module"
|
|
12
|
-
import { basename, dirname, join, resolve } from "node:path"
|
|
13
|
-
import { fileURLToPath } from "node:url"
|
|
14
|
-
import {
|
|
15
|
-
BLOCK_BASE_PORT,
|
|
16
|
-
buildBlockRegistry,
|
|
17
|
-
SHELL_2_PORT,
|
|
18
|
-
writeBlockViteConfig,
|
|
19
|
-
} from "./block-server"
|
|
20
|
-
import { serveUi } from "./serve-ui"
|
|
21
|
-
import {
|
|
22
|
-
blockCapabilities,
|
|
23
|
-
findWorkerDir,
|
|
24
|
-
generateWorkerManifest,
|
|
25
|
-
} from "./worker-manifest"
|
|
26
|
-
|
|
27
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
28
|
-
|
|
29
|
-
const dim = "\x1b[2m"
|
|
30
|
-
const bold = "\x1b[1m"
|
|
31
|
-
const cyan = "\x1b[36m"
|
|
32
|
-
const reset = "\x1b[0m"
|
|
33
|
-
const label = (name: string) => `${cyan}[${name}]${reset}`
|
|
34
|
-
|
|
35
|
-
type CliArgs = {
|
|
36
|
-
worker: string | undefined
|
|
37
|
-
shellPort: number
|
|
38
|
-
blockBasePort: number
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function parseCliArgs(argv: readonly string[]): CliArgs {
|
|
42
|
-
const args: CliArgs = {
|
|
43
|
-
worker: undefined,
|
|
44
|
-
shellPort: SHELL_2_PORT,
|
|
45
|
-
blockBasePort: BLOCK_BASE_PORT,
|
|
46
|
-
}
|
|
47
|
-
const takeValue = (name: string, index: number): string => {
|
|
48
|
-
const value = argv[index]
|
|
49
|
-
if (value === undefined || value.startsWith("--")) {
|
|
50
|
-
throw new Error(`${name} requires a value.`)
|
|
51
|
-
}
|
|
52
|
-
return value
|
|
53
|
-
}
|
|
54
|
-
const takePort = (name: string, raw: string): number => {
|
|
55
|
-
const port = Number(raw)
|
|
56
|
-
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
57
|
-
throw new Error(`${name} requires a port number, got "${raw}".`)
|
|
58
|
-
}
|
|
59
|
-
return port
|
|
60
|
-
}
|
|
61
|
-
for (let index = 0; index < argv.length; index++) {
|
|
62
|
-
const arg = argv[index]
|
|
63
|
-
if (arg === "--worker") {
|
|
64
|
-
args.worker = takeValue("--worker", ++index)
|
|
65
|
-
} else if (arg.startsWith("--worker=")) {
|
|
66
|
-
args.worker = arg.slice("--worker=".length)
|
|
67
|
-
if (args.worker.length === 0) {
|
|
68
|
-
throw new Error("--worker requires a path to a worker directory.")
|
|
69
|
-
}
|
|
70
|
-
} else if (arg === "--port") {
|
|
71
|
-
args.shellPort = takePort("--port", takeValue("--port", ++index))
|
|
72
|
-
} else if (arg.startsWith("--port=")) {
|
|
73
|
-
args.shellPort = takePort("--port", arg.slice("--port=".length))
|
|
74
|
-
} else if (arg === "--block-base-port") {
|
|
75
|
-
args.blockBasePort = takePort(
|
|
76
|
-
"--block-base-port",
|
|
77
|
-
takeValue("--block-base-port", ++index),
|
|
78
|
-
)
|
|
79
|
-
} else if (arg.startsWith("--block-base-port=")) {
|
|
80
|
-
args.blockBasePort = takePort(
|
|
81
|
-
"--block-base-port",
|
|
82
|
-
arg.slice("--block-base-port=".length),
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return args
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function resolveWorkerDir(workerArg: string | undefined): string {
|
|
90
|
-
if (workerArg !== undefined) {
|
|
91
|
-
return resolve(process.cwd(), workerArg)
|
|
92
|
-
}
|
|
93
|
-
const detected = findWorkerDir(process.cwd())
|
|
94
|
-
if (detected !== undefined) {
|
|
95
|
-
console.log(`Detected a worker at ${detected}.`)
|
|
96
|
-
return detected
|
|
97
|
-
}
|
|
98
|
-
throw new Error(
|
|
99
|
-
"No worker found: run from inside a worker directory, or pass --worker <dir>.",
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* The worker's own Vite binary. Blocks are served with the worker's Vite (and
|
|
105
|
-
* plugins) rather than anything bundled here, matching how the block builds in
|
|
106
|
-
* production.
|
|
107
|
-
*/
|
|
108
|
-
function resolveViteBin(workerDir: string): string {
|
|
109
|
-
let vitePkgPath: string
|
|
110
|
-
try {
|
|
111
|
-
const workerRequire = createRequire(join(workerDir, "package.json"))
|
|
112
|
-
vitePkgPath = workerRequire.resolve("vite/package.json")
|
|
113
|
-
} catch {
|
|
114
|
-
throw new Error(
|
|
115
|
-
`Could not resolve "vite" from ${workerDir}. Add vite to the worker's ` +
|
|
116
|
-
`devDependencies and reinstall.`,
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
const vitePkg = JSON.parse(readFileSync(vitePkgPath, "utf-8")) as {
|
|
120
|
-
bin?: string | Record<string, string>
|
|
121
|
-
}
|
|
122
|
-
const bin = typeof vitePkg.bin === "string" ? vitePkg.bin : vitePkg.bin?.vite
|
|
123
|
-
if (bin === undefined) {
|
|
124
|
-
throw new Error(`The vite package at ${vitePkgPath} exposes no bin.`)
|
|
125
|
-
}
|
|
126
|
-
return resolve(dirname(vitePkgPath), bin)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const procs: ReturnType<typeof spawn>[] = []
|
|
130
|
-
|
|
131
|
-
async function main() {
|
|
132
|
-
const cliArgs = parseCliArgs(process.argv.slice(2))
|
|
133
|
-
const workerDir = resolveWorkerDir(cliArgs.worker)
|
|
134
|
-
if (!existsSync(resolve(workerDir, "node_modules"))) {
|
|
135
|
-
throw new Error(
|
|
136
|
-
`No node_modules in ${workerDir}. Install the worker's dependencies first ` +
|
|
137
|
-
`(e.g. \`npm install\`), then rerun.`,
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
console.log(`${label(basename(workerDir))} Extracting worker manifest...`)
|
|
142
|
-
const { manifest, manifestPath } = await generateWorkerManifest(workerDir)
|
|
143
|
-
console.log(`${label(basename(workerDir))} Wrote ${manifestPath}`)
|
|
144
|
-
const blocks = blockCapabilities(manifest)
|
|
145
|
-
if (blocks.length === 0) {
|
|
146
|
-
// Not an error — start the shell anyway; it shows "None" under Blocks.
|
|
147
|
-
console.log(
|
|
148
|
-
`${label(basename(workerDir))} Worker declares no custom blocks.`,
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
blocks.length > 0 &&
|
|
154
|
-
cliArgs.shellPort >= cliArgs.blockBasePort &&
|
|
155
|
-
cliArgs.shellPort < cliArgs.blockBasePort + blocks.length
|
|
156
|
-
) {
|
|
157
|
-
throw new Error(
|
|
158
|
-
`--port ${cliArgs.shellPort} collides with the block server ports ` +
|
|
159
|
-
`(${cliArgs.blockBasePort}–${cliArgs.blockBasePort + blocks.length - 1}); ` +
|
|
160
|
-
`pick a port outside that range or move --block-base-port.`,
|
|
161
|
-
)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const viteBin = blocks.length > 0 ? resolveViteBin(workerDir) : undefined
|
|
165
|
-
const registry = buildBlockRegistry(blocks, cliArgs.blockBasePort)
|
|
166
|
-
|
|
167
|
-
for (const [index, capability] of blocks.entries()) {
|
|
168
|
-
const blockDir = resolve(workerDir, capability.config.source.path)
|
|
169
|
-
const configFile = writeBlockViteConfig(workerDir, blockDir, capability)
|
|
170
|
-
const port = cliArgs.blockBasePort + index
|
|
171
|
-
const proc = spawn(
|
|
172
|
-
process.execPath,
|
|
173
|
-
[
|
|
174
|
-
viteBin as string,
|
|
175
|
-
"--config",
|
|
176
|
-
configFile,
|
|
177
|
-
"--port",
|
|
178
|
-
String(port),
|
|
179
|
-
"--strictPort",
|
|
180
|
-
],
|
|
181
|
-
{
|
|
182
|
-
cwd: workerDir,
|
|
183
|
-
stdio: ["ignore", "ignore", "inherit"],
|
|
184
|
-
// Process groups (and negative-PID kills) are POSIX-only; on
|
|
185
|
-
// Windows children are killed individually in shutdown().
|
|
186
|
-
detached: process.platform !== "win32",
|
|
187
|
-
},
|
|
188
|
-
)
|
|
189
|
-
proc.on("exit", code => {
|
|
190
|
-
if (shuttingDown || code === 0 || code === null) {
|
|
191
|
-
return
|
|
192
|
-
}
|
|
193
|
-
console.error(
|
|
194
|
-
`${label(capability.key)} dev server exited with code ${code}`,
|
|
195
|
-
)
|
|
196
|
-
process.exitCode = code
|
|
197
|
-
shutdown("SIGTERM")
|
|
198
|
-
})
|
|
199
|
-
procs.push(proc)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// The published layout is dist/ next to cli/; index.html must be prebuilt.
|
|
203
|
-
const distDir = resolve(__dirname, "..", "dist")
|
|
204
|
-
if (!existsSync(join(distDir, "index.html"))) {
|
|
205
|
-
throw new Error(
|
|
206
|
-
`No prebuilt UI found at ${distDir}. This package was not assembled ` +
|
|
207
|
-
`correctly; reinstall it.`,
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
try {
|
|
211
|
-
await serveUi(distDir, cliArgs.shellPort, {
|
|
212
|
-
mode: "worker",
|
|
213
|
-
blocks: registry,
|
|
214
|
-
})
|
|
215
|
-
} catch (error) {
|
|
216
|
-
const code = (error as NodeJS.ErrnoException).code
|
|
217
|
-
if (code === "EADDRINUSE") {
|
|
218
|
-
throw new Error(
|
|
219
|
-
`Port ${cliArgs.shellPort} is already in use. Stop whatever holds it ` +
|
|
220
|
-
`or rerun with --port <port> (and --block-base-port <port> for the ` +
|
|
221
|
-
`block servers).`,
|
|
222
|
-
)
|
|
223
|
-
}
|
|
224
|
-
throw error
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
console.log("")
|
|
228
|
-
console.log(`${bold}Dev shell${reset}`)
|
|
229
|
-
console.log(
|
|
230
|
-
` ${label("dev-shell")} ${dim}http://localhost:${cliArgs.shellPort}${reset}`,
|
|
231
|
-
)
|
|
232
|
-
if (blocks.length > 0) {
|
|
233
|
-
console.log("")
|
|
234
|
-
console.log(`${bold}Blocks${reset}`)
|
|
235
|
-
for (const [index, entry] of registry.entries()) {
|
|
236
|
-
console.log(
|
|
237
|
-
` ${label(entry.key)} ${dim}http://localhost:${cliArgs.blockBasePort + index}${reset}`,
|
|
238
|
-
)
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
console.log("")
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function killProc(p: ReturnType<typeof spawn>, signal: NodeJS.Signals) {
|
|
245
|
-
if (p.pid === undefined || p.killed) {
|
|
246
|
-
return
|
|
247
|
-
}
|
|
248
|
-
try {
|
|
249
|
-
if (process.platform === "win32") {
|
|
250
|
-
p.kill(signal)
|
|
251
|
-
} else {
|
|
252
|
-
process.kill(-p.pid, signal)
|
|
253
|
-
}
|
|
254
|
-
} catch {}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
let shuttingDown = false
|
|
258
|
-
function shutdown(signal: NodeJS.Signals | "exit") {
|
|
259
|
-
if (shuttingDown) {
|
|
260
|
-
return
|
|
261
|
-
}
|
|
262
|
-
shuttingDown = true
|
|
263
|
-
if (signal !== "exit") {
|
|
264
|
-
console.log(`\n${dim}Shutting down...${reset}`)
|
|
265
|
-
}
|
|
266
|
-
for (const p of procs) {
|
|
267
|
-
killProc(p, "SIGTERM")
|
|
268
|
-
}
|
|
269
|
-
setTimeout(() => {
|
|
270
|
-
for (const p of procs) {
|
|
271
|
-
killProc(p, "SIGKILL")
|
|
272
|
-
}
|
|
273
|
-
// Preserve a failure exit code set before shutdown (startup errors,
|
|
274
|
-
// crashed block servers); plain signal shutdowns still exit 0.
|
|
275
|
-
process.exit(process.exitCode ?? 0)
|
|
276
|
-
}, 1500).unref()
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
|
|
280
|
-
process.on(signal, () => shutdown(signal))
|
|
281
|
-
}
|
|
282
|
-
process.on("exit", () => shutdown("exit"))
|
|
283
|
-
process.on("uncaughtException", err => {
|
|
284
|
-
console.error(err)
|
|
285
|
-
process.exitCode = 1
|
|
286
|
-
shutdown("SIGTERM")
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
main().catch(err => {
|
|
290
|
-
console.error(err instanceof Error ? err.message : err)
|
|
291
|
-
process.exitCode = 1
|
|
292
|
-
shutdown("SIGTERM")
|
|
293
|
-
})
|
package/cli/serve-ui.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dependency-free static server for the prebuilt dev-shell-2 UI (`dist/`).
|
|
3
|
-
*
|
|
4
|
-
* The published UI is a plain Vite build, so the block registry can't ride in
|
|
5
|
-
* through build-time `define` globals the way it does under `scripts/dev.ts`.
|
|
6
|
-
* Instead, this server injects a `window.__DEV_SHELL_2_CONFIG__` script into
|
|
7
|
-
* `index.html` as it is served; `src/helpers/templates.ts` prefers that global
|
|
8
|
-
* over the baked-in defines.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readFileSync } from "node:fs"
|
|
12
|
-
import { createServer, type Server } from "node:http"
|
|
13
|
-
import { extname, join, normalize, resolve, sep } from "node:path"
|
|
14
|
-
import type { BlockRegistryEntry } from "./block-server"
|
|
15
|
-
|
|
16
|
-
export type DevShellRuntimeConfig = {
|
|
17
|
-
mode: "worker"
|
|
18
|
-
blocks: BlockRegistryEntry[]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const CONTENT_TYPES: Record<string, string> = {
|
|
22
|
-
".html": "text/html; charset=utf-8",
|
|
23
|
-
".js": "text/javascript; charset=utf-8",
|
|
24
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
25
|
-
".css": "text/css; charset=utf-8",
|
|
26
|
-
".json": "application/json; charset=utf-8",
|
|
27
|
-
".svg": "image/svg+xml",
|
|
28
|
-
".png": "image/png",
|
|
29
|
-
".ico": "image/x-icon",
|
|
30
|
-
".woff": "font/woff",
|
|
31
|
-
".woff2": "font/woff2",
|
|
32
|
-
".map": "application/json; charset=utf-8",
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** `index.html` with the runtime config injected ahead of the bundle script. */
|
|
36
|
-
function injectConfig(html: string, config: DevShellRuntimeConfig): string {
|
|
37
|
-
const tag = `<script>window.__DEV_SHELL_2_CONFIG__ = ${JSON.stringify(config)}</script>`
|
|
38
|
-
if (html.includes("<head>")) {
|
|
39
|
-
return html.replace("<head>", `<head>\n\t\t${tag}`)
|
|
40
|
-
}
|
|
41
|
-
return `${tag}\n${html}`
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Serve `distDir` on `port`. Unknown extensionless paths fall back to
|
|
46
|
-
* `index.html`. Rejects on listen errors (e.g. the port is taken).
|
|
47
|
-
*/
|
|
48
|
-
export function serveUi(
|
|
49
|
-
distDir: string,
|
|
50
|
-
port: number,
|
|
51
|
-
config: DevShellRuntimeConfig,
|
|
52
|
-
): Promise<Server> {
|
|
53
|
-
const dist = resolve(distDir)
|
|
54
|
-
const indexHtml = injectConfig(
|
|
55
|
-
readFileSync(join(dist, "index.html"), "utf-8"),
|
|
56
|
-
config,
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
const server = createServer((req, res) => {
|
|
60
|
-
let path: string
|
|
61
|
-
try {
|
|
62
|
-
path = normalize(decodeURIComponent(req.url?.split("?", 1)[0] ?? "/"))
|
|
63
|
-
} catch {
|
|
64
|
-
res.writeHead(400)
|
|
65
|
-
res.end()
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
const filePath = join(dist, path)
|
|
69
|
-
if (!filePath.startsWith(dist + sep) && filePath !== dist) {
|
|
70
|
-
res.writeHead(403)
|
|
71
|
-
res.end()
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const ext = extname(filePath)
|
|
76
|
-
if (path === "/" || path === `${sep}index.html` || ext === "") {
|
|
77
|
-
res.writeHead(200, { "Content-Type": CONTENT_TYPES[".html"] })
|
|
78
|
-
res.end(indexHtml)
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const body = readFileSync(filePath)
|
|
84
|
-
res.writeHead(200, {
|
|
85
|
-
"Content-Type": CONTENT_TYPES[ext] ?? "application/octet-stream",
|
|
86
|
-
})
|
|
87
|
-
res.end(body)
|
|
88
|
-
} catch {
|
|
89
|
-
res.writeHead(404)
|
|
90
|
-
res.end()
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
return new Promise((resolvePromise, reject) => {
|
|
95
|
-
server.once("error", reject)
|
|
96
|
-
// Loopback only: every response embeds the worker's block registry, and
|
|
97
|
-
// the per-block Vite servers are localhost-only too.
|
|
98
|
-
server.listen(port, "127.0.0.1", () => resolvePromise(server))
|
|
99
|
-
})
|
|
100
|
-
}
|
package/cli/worker-manifest.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extract a worker's manifest without deploying it, following the localhost
|
|
3
|
-
* verification strategy: build the worker, then read the manifest off the
|
|
4
|
-
* built module's default export. This is the one prerequisite the dev shell
|
|
5
|
-
* needs before it can list a worker's blocks and data sources.
|
|
6
|
-
*
|
|
7
|
-
* The worker manifest is otherwise in-memory only (tied to `worker.ts` + cloud
|
|
8
|
-
* build), so we materialize it under the worker's git-ignored `.dev-shell/` dir
|
|
9
|
-
* (clearly a dev-shell artifact, not something the author maintains).
|
|
10
|
-
* Regenerated on every spin-up.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { execSync } from "node:child_process"
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
15
|
-
import { dirname, resolve } from "node:path"
|
|
16
|
-
import { pathToFileURL } from "node:url"
|
|
17
|
-
|
|
18
|
-
/** One custom-block capability in the worker manifest. */
|
|
19
|
-
export type WorkerBlockCapability = {
|
|
20
|
-
_tag: "custom_block"
|
|
21
|
-
key: string
|
|
22
|
-
config: {
|
|
23
|
-
source: { type: string; path: string; command?: string; output?: string }
|
|
24
|
-
manifest: {
|
|
25
|
-
version: number
|
|
26
|
-
dataSources: Record<
|
|
27
|
-
string,
|
|
28
|
-
{
|
|
29
|
-
name: string
|
|
30
|
-
description?: string
|
|
31
|
-
icon?: unknown
|
|
32
|
-
properties?: Record<string, { name: string; type: string }>
|
|
33
|
-
}
|
|
34
|
-
>
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** One worker-defined database in the manifest. */
|
|
40
|
-
export type WorkerDatabaseEntry = {
|
|
41
|
-
key: string
|
|
42
|
-
config: {
|
|
43
|
-
type: string
|
|
44
|
-
initialTitle?: string
|
|
45
|
-
primaryKeyProperty?: string
|
|
46
|
-
schema?: {
|
|
47
|
-
properties?: Record<string, { type: string; [k: string]: unknown }>
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export type WorkerManifest = {
|
|
53
|
-
sdkVersion?: string
|
|
54
|
-
databases: WorkerDatabaseEntry[]
|
|
55
|
-
pacers: { key: string; config: unknown }[]
|
|
56
|
-
capabilities: { _tag: string; key: string; config: unknown }[]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const WORKER_MANIFEST_FILENAME = "worker_manifest.json"
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Heuristic for the no-flag fallback: does `dir` look like a worker project?
|
|
63
|
-
* True if it carries a worker deploy binding (`worker.json`/`workers.json`) or
|
|
64
|
-
* depends on `@notionhq/workers`.
|
|
65
|
-
*/
|
|
66
|
-
export function looksLikeWorkerDir(dir: string): boolean {
|
|
67
|
-
if (
|
|
68
|
-
existsSync(resolve(dir, "worker.json")) ||
|
|
69
|
-
existsSync(resolve(dir, "workers.json"))
|
|
70
|
-
) {
|
|
71
|
-
return true
|
|
72
|
-
}
|
|
73
|
-
const pkgPath = resolve(dir, "package.json")
|
|
74
|
-
if (!existsSync(pkgPath)) {
|
|
75
|
-
return false
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
|
|
79
|
-
dependencies?: Record<string, string>
|
|
80
|
-
devDependencies?: Record<string, string>
|
|
81
|
-
}
|
|
82
|
-
return Boolean(
|
|
83
|
-
pkg.dependencies?.["@notionhq/workers"] ??
|
|
84
|
-
pkg.devDependencies?.["@notionhq/workers"],
|
|
85
|
-
)
|
|
86
|
-
} catch {
|
|
87
|
-
return false
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Walk up from `startDir` looking for a worker directory (see
|
|
93
|
-
* `looksLikeWorkerDir`), so the shell can be launched from anywhere inside a
|
|
94
|
-
* worker, not only its root. Stops once it reaches a git-repo boundary (a
|
|
95
|
-
* `.git` at the current level, checked after the worker test) or the filesystem
|
|
96
|
-
* root — it should never escape the project the user is in.
|
|
97
|
-
*/
|
|
98
|
-
export function findWorkerDir(startDir: string): string | undefined {
|
|
99
|
-
let current = resolve(startDir)
|
|
100
|
-
while (true) {
|
|
101
|
-
if (looksLikeWorkerDir(current)) {
|
|
102
|
-
return current
|
|
103
|
-
}
|
|
104
|
-
if (existsSync(resolve(current, ".git"))) {
|
|
105
|
-
return undefined
|
|
106
|
-
}
|
|
107
|
-
const parent = dirname(current)
|
|
108
|
-
if (parent === current) {
|
|
109
|
-
return undefined
|
|
110
|
-
}
|
|
111
|
-
current = parent
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** The custom-block capabilities in a manifest, narrowed by `_tag`. */
|
|
116
|
-
export function blockCapabilities(
|
|
117
|
-
manifest: WorkerManifest,
|
|
118
|
-
): WorkerBlockCapability[] {
|
|
119
|
-
return manifest.capabilities.filter(
|
|
120
|
-
(capability): capability is WorkerBlockCapability =>
|
|
121
|
-
capability._tag === "custom_block",
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function isManifestShape(value: unknown): value is WorkerManifest {
|
|
126
|
-
return (
|
|
127
|
-
typeof value === "object" &&
|
|
128
|
-
value !== null &&
|
|
129
|
-
Array.isArray((value as WorkerManifest).capabilities)
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Build the worker in `workerDir` and read its manifest off the built default
|
|
135
|
-
* export. Mirrors the deploy path (build → read the built module) rather than
|
|
136
|
-
* importing TypeScript source, so what the dev shell sees matches what a deploy
|
|
137
|
-
* would produce.
|
|
138
|
-
*/
|
|
139
|
-
export async function extractWorkerManifest(
|
|
140
|
-
workerDir: string,
|
|
141
|
-
options: { build?: boolean } = {},
|
|
142
|
-
): Promise<WorkerManifest> {
|
|
143
|
-
const root = resolve(workerDir)
|
|
144
|
-
if (!existsSync(resolve(root, "package.json"))) {
|
|
145
|
-
throw new Error(`No package.json found in worker directory: ${root}`)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (options.build !== false) {
|
|
149
|
-
execSync("npm run build", { cwd: root, stdio: "inherit" })
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const entry = resolve(root, "dist/index.js")
|
|
153
|
-
if (!existsSync(entry)) {
|
|
154
|
-
throw new Error(
|
|
155
|
-
`Built worker entry not found at ${entry}. The worker's build must emit dist/index.js.`,
|
|
156
|
-
)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const mod = (await import(pathToFileURL(entry).href)) as {
|
|
160
|
-
default?: { manifest?: unknown; capabilities?: unknown }
|
|
161
|
-
}
|
|
162
|
-
const worker = mod.default
|
|
163
|
-
if (worker === undefined) {
|
|
164
|
-
throw new Error(`Built worker at ${entry} has no default export.`)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Read the built worker's manifest, falling back to a bare capabilities array
|
|
168
|
-
// (per the localhost verification doc's `w.manifest || w.capabilities`).
|
|
169
|
-
const raw = worker.manifest ?? worker.capabilities
|
|
170
|
-
if (isManifestShape(raw)) {
|
|
171
|
-
return raw
|
|
172
|
-
}
|
|
173
|
-
if (Array.isArray(raw)) {
|
|
174
|
-
return { databases: [], pacers: [], capabilities: raw }
|
|
175
|
-
}
|
|
176
|
-
throw new Error(
|
|
177
|
-
`Built worker at ${entry} exposed no usable manifest (expected .manifest or .capabilities).`,
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Extract the worker manifest and write it to `.dev-shell/worker_manifest.json`
|
|
183
|
-
* inside the worker (a git-ignored dev-shell artifact dir). Returns the parsed
|
|
184
|
-
* manifest and the path written.
|
|
185
|
-
*/
|
|
186
|
-
export async function generateWorkerManifest(
|
|
187
|
-
workerDir: string,
|
|
188
|
-
options: { build?: boolean } = {},
|
|
189
|
-
): Promise<{ manifest: WorkerManifest; manifestPath: string }> {
|
|
190
|
-
const manifest = await extractWorkerManifest(workerDir, options)
|
|
191
|
-
const dir = resolve(workerDir, ".dev-shell")
|
|
192
|
-
mkdirSync(dir, { recursive: true })
|
|
193
|
-
const manifestPath = resolve(dir, WORKER_MANIFEST_FILENAME)
|
|
194
|
-
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
195
|
-
return { manifest, manifestPath }
|
|
196
|
-
}
|