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