@notionhq/custom-blocks-dev-shell 0.1.33 → 0.1.35

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-kNlMQeXD.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-kz317NXT.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-DpKdfNws.css">
9
9
  </head>
10
10
  <body>
@@ -1,13 +1,10 @@
1
1
  /**
2
- * Per-block Vite server plumbing shared by the repo dev script
3
- * (`scripts/dev.ts`) and the published dev shell CLI (`cli/main.ts`).
2
+ * Provides shared Vite server setup for both dev shell launchers. It builds the block registry,
3
+ * generates a Vite config for each block, and formats Vite change logs.
4
4
  */
5
5
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
6
6
  import { relative, resolve } from "node:path";
7
- /** First port handed to per-block Vite servers; blocks count up from here. */
8
- export const BLOCK_BASE_PORT = 9876;
9
- /** Port the dev shell UI is served on. */
10
- export const SHELL_PORT = 9873;
7
+ import { BLOCK_BASE_PORT } from "./ports.js";
11
8
  /** The registry for a worker's blocks, assuming sequential port assignment. */
12
9
  export function buildBlockRegistry(blocks, basePort = BLOCK_BASE_PORT) {
13
10
  return blocks.map((capability, index) => ({
@@ -9,7 +9,7 @@ import { readFileSync } from "node:fs";
9
9
  import * as v from "valibot";
10
10
  import { convertPublicApiPropertyValue, isDataSourceValue, matchesPropertyType, } from "./convert-values.js";
11
11
  import { NOTION_PROPERTY_TYPES, sourceFileSchema } from "./data-sources.js";
12
- import { claimUniqueKey, slugify } from "./utils.js";
12
+ import { claimUniqueKey, parseLongOptions, slugify } from "./utils.js";
13
13
  const SUPPORTED_TYPES = new Set(NOTION_PROPERTY_TYPES);
14
14
  /** The spelling of "text" in the local file format. */
15
15
  const UNSUPPORTED_TYPE_FALLBACK = "rich_text";
@@ -19,37 +19,19 @@ export function parseConvertArgs(argv) {
19
19
  key: undefined,
20
20
  name: undefined,
21
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
- }
22
+ parseLongOptions(argv, {
23
+ "--in": value => {
24
+ args.input = value;
25
+ },
26
+ "--key": value => {
27
+ args.key = value;
28
+ },
29
+ "--name": value => {
30
+ args.name = value;
31
+ },
32
+ }, arg => {
33
+ throw new Error(`Unknown convert option "${arg}". Supported: --in <file>, --key <key>, --name <name>.`);
34
+ });
53
35
  return args;
54
36
  }
55
37
  const sampleInputSchema = v.pipe(v.looseObject({
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Shared orchestration for the repository and published dev shell launchers.
3
+ *
4
+ * It builds the worker and reads its manifest, starts one Vite server per
5
+ * custom block, and owns process and resource cleanup. Each launcher entry point
6
+ * supplies a small shell adapter, but otherwise delegates to this module.
7
+ */
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { createRequire } from "node:module";
10
+ import { basename, dirname, join, resolve } from "node:path";
11
+ import { buildBlockRegistry, writeBlockViteConfig, } from "./block-server.js";
12
+ import { readDataSources } from "./data-sources.js";
13
+ import { materializeWorkerSchemaDataSources } from "./materialize.js";
14
+ import { BLOCK_BASE_PORT, parsePort, SHELL_PORT, validateBlockPortRange, validateShellPort, } from "./ports.js";
15
+ import { installProcessSignalHandlers, ProcessSupervisor, } from "./process-supervisor.js";
16
+ import { parseLongOptions } from "./utils.js";
17
+ import { blockCapabilities, findWorkerDir, generateWorkerManifest, } from "./worker-manifest.js";
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
+ export async function prepareWorkerLaunchPlan(workerDir, options = {}) {
24
+ const root = resolve(workerDir);
25
+ if (!existsSync(resolve(root, "node_modules"))) {
26
+ throw new Error(`No node_modules in ${root}. Install the worker's dependencies first ` +
27
+ `(e.g. \`npm install\`), then rerun.`);
28
+ }
29
+ const { manifest, manifestPath } = await generateWorkerManifest(root, {
30
+ build: options.build,
31
+ });
32
+ const capabilities = blockCapabilities(manifest);
33
+ const blockBasePort = options.blockBasePort ?? BLOCK_BASE_PORT;
34
+ validateBlockPortRange(blockBasePort, capabilities.length);
35
+ const blocks = capabilities.map((capability, index) => {
36
+ const blockDir = resolve(root, capability.config.source.path);
37
+ return {
38
+ capability,
39
+ blockDir,
40
+ configFile: writeBlockViteConfig(root, blockDir, capability),
41
+ port: blockBasePort + index,
42
+ };
43
+ });
44
+ const dataDir = resolve(root, "src/data");
45
+ materializeWorkerSchemaDataSources(manifest, dataDir);
46
+ return {
47
+ workerDir: root,
48
+ dataDir,
49
+ manifestPath,
50
+ blocks,
51
+ registry: buildBlockRegistry(capabilities, blockBasePort),
52
+ dataSources: readDataSources(dataDir),
53
+ };
54
+ }
55
+ export function parseWorkerLaunchArgs(argv) {
56
+ const args = {
57
+ worker: undefined,
58
+ shellPort: SHELL_PORT,
59
+ blockBasePort: BLOCK_BASE_PORT,
60
+ };
61
+ parseLongOptions(argv, {
62
+ "--worker": value => {
63
+ if (value.length === 0) {
64
+ throw new Error("--worker requires a path to a worker directory.");
65
+ }
66
+ args.worker = value;
67
+ },
68
+ "--port": value => {
69
+ args.shellPort = parsePort("--port", value);
70
+ },
71
+ "--block-base-port": value => {
72
+ args.blockBasePort = parsePort("--block-base-port", value);
73
+ },
74
+ });
75
+ return args;
76
+ }
77
+ function resolveWorkerViteBin(workerDir) {
78
+ let vitePkgPath;
79
+ try {
80
+ const workerRequire = createRequire(join(workerDir, "package.json"));
81
+ vitePkgPath = workerRequire.resolve("vite/package.json");
82
+ }
83
+ catch {
84
+ throw new Error(`Could not resolve "vite" from ${workerDir}. Add vite to the worker's ` +
85
+ `devDependencies and reinstall.`);
86
+ }
87
+ const vitePkg = JSON.parse(readFileSync(vitePkgPath, "utf-8"));
88
+ const bin = typeof vitePkg.bin === "string" ? vitePkg.bin : vitePkg.bin?.vite;
89
+ if (bin === undefined) {
90
+ throw new Error(`The vite package at ${vitePkgPath} exposes no bin.`);
91
+ }
92
+ return resolve(dirname(vitePkgPath), bin);
93
+ }
94
+ function createBlockProcesses(plan) {
95
+ if (plan.blocks.length === 0) {
96
+ return [];
97
+ }
98
+ const viteBin = resolveWorkerViteBin(plan.workerDir);
99
+ return plan.blocks.map(block => ({
100
+ name: block.capability.key,
101
+ command: process.execPath,
102
+ args: [
103
+ viteBin,
104
+ "--config",
105
+ block.configFile,
106
+ "--port",
107
+ String(block.port),
108
+ "--strictPort",
109
+ ],
110
+ cwd: plan.workerDir,
111
+ detached: process.platform !== "win32",
112
+ }));
113
+ }
114
+ function logNamed(name, message) {
115
+ console.log(`${label(name)} ${message}`);
116
+ }
117
+ function printSummary(args, registry) {
118
+ console.log("");
119
+ console.log(`${bold}Dev shell${reset}`);
120
+ console.log(` ${label("dev-shell")} ${dim}http://localhost:${args.shellPort}${reset}`);
121
+ if (registry.length > 0) {
122
+ console.log("");
123
+ console.log(`${bold}Blocks${reset}`);
124
+ for (const entry of registry) {
125
+ console.log(` ${label(entry.key)} ${dim}${entry.url}${reset}`);
126
+ }
127
+ }
128
+ console.log("");
129
+ }
130
+ function resolveWorkerLaunch(options) {
131
+ const args = parseWorkerLaunchArgs(options.argv);
132
+ const workerDir = args.worker === undefined
133
+ ? findWorkerDir(options.detectFromDir)
134
+ : resolve(options.workerBaseDir, args.worker);
135
+ if (workerDir === undefined) {
136
+ throw new Error("No worker found: run from inside a worker directory, or pass --worker <dir>.");
137
+ }
138
+ if (args.worker === undefined) {
139
+ console.log(`Detected a worker at ${workerDir}.`);
140
+ }
141
+ return { args, workerDir };
142
+ }
143
+ function logWorkerPlan(workerName, plan) {
144
+ logNamed(workerName, `Wrote ${plan.manifestPath}`);
145
+ if (plan.blocks.length === 0) {
146
+ logNamed(workerName, "Worker declares no custom blocks.");
147
+ }
148
+ logNamed(workerName, plan.dataSources.length > 0
149
+ ? `Data sources from src/data: ${plan.dataSources
150
+ .map(source => source.name)
151
+ .join(", ")}`
152
+ : "No data sources — create src/data/<key>.json files " +
153
+ "(format: node_modules/@notionhq/custom-blocks-dev-shell/docs/data-sources.md).");
154
+ }
155
+ async function prepareWorkerLaunch(options) {
156
+ const { args, workerDir } = resolveWorkerLaunch(options);
157
+ const workerName = basename(workerDir);
158
+ logNamed(workerName, "Extracting worker manifest...");
159
+ const plan = await prepareWorkerLaunchPlan(workerDir, {
160
+ blockBasePort: args.blockBasePort,
161
+ });
162
+ logWorkerPlan(workerName, plan);
163
+ validateShellPort(args.shellPort, args.blockBasePort, plan.blocks.length);
164
+ const shell = await options.createShellLaunch({
165
+ args,
166
+ plan,
167
+ log: logNamed,
168
+ });
169
+ return { args, plan, shell };
170
+ }
171
+ export async function launchDevShell(options) {
172
+ const { installSignalHandlers = true } = options;
173
+ const callbacks = {
174
+ onChange: logNamed,
175
+ onUnexpectedExit: (name, code) => console.error(`${label(name)} dev server exited with code ${code}`),
176
+ onProcessError: (name, error) => console.error(`${label(name)} dev server failed: ${error.message}`),
177
+ };
178
+ const run = options.createRun?.() ?? new ProcessSupervisor(callbacks);
179
+ if (installSignalHandlers) {
180
+ installProcessSignalHandlers(run);
181
+ }
182
+ try {
183
+ const { args, plan, shell } = await prepareWorkerLaunch(options);
184
+ for (const resource of shell.resources) {
185
+ run.addResource(resource);
186
+ }
187
+ run.start([...shell.processes, ...createBlockProcesses(plan)]);
188
+ printSummary(args, plan.registry);
189
+ }
190
+ catch (error) {
191
+ console.error(`Failed to start dev shell: ${error instanceof Error ? error.message : error}`);
192
+ process.exitCode = 1;
193
+ run.shutdown("SIGTERM");
194
+ }
195
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Port defaults and validation shared by the repository and published launchers.
3
+ */
4
+ /** First port handed to per-block Vite servers; blocks count up from here. */
5
+ export const BLOCK_BASE_PORT = 9876;
6
+ /** Port the dev shell UI is served on. */
7
+ export const SHELL_PORT = 9873;
8
+ /** Highest valid TCP port. */
9
+ const MAX_PORT = 65535;
10
+ /** Parse and validate a named CLI port option. */
11
+ export function parsePort(name, raw) {
12
+ const port = Number(raw);
13
+ if (!Number.isInteger(port) || port <= 0 || port > MAX_PORT) {
14
+ throw new Error(`${name} requires a port number, got "${raw}".`);
15
+ }
16
+ return port;
17
+ }
18
+ /** Reject a sequential block-port range that would exceed the TCP port limit. */
19
+ export function validateBlockPortRange(blockBasePort, blockCount) {
20
+ const lastPort = blockBasePort + blockCount - 1;
21
+ if (blockCount > 0 && lastPort > MAX_PORT) {
22
+ throw new Error(`--block-base-port ${blockBasePort} is too high for ${blockCount} ` +
23
+ `block servers; the last block port would be ${lastPort}, but ports ` +
24
+ `cannot exceed ${MAX_PORT}.`);
25
+ }
26
+ }
27
+ /** Reject a shell port that overlaps the sequential block-port range. */
28
+ export function validateShellPort(shellPort, blockBasePort, blockCount) {
29
+ if (blockCount > 0 &&
30
+ shellPort >= blockBasePort &&
31
+ shellPort < blockBasePort + blockCount) {
32
+ throw new Error(`--port ${shellPort} collides with the block server ports ` +
33
+ `(${blockBasePort}–${blockBasePort + blockCount - 1}); ` +
34
+ `pick a port outside that range or move --block-base-port.`);
35
+ }
36
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Own child processes and non-child resources for one dev shell run.
3
+ */
4
+ import { spawn } from "node:child_process";
5
+ import { createInterface } from "node:readline";
6
+ import { makeChangeLogger } from "./block-server.js";
7
+ const SHUTDOWN_GRACE_MS = 1500;
8
+ const dim = "\x1b[2m";
9
+ const reset = "\x1b[0m";
10
+ export class ProcessSupervisor {
11
+ callbacks;
12
+ processes = new Map();
13
+ resources = [];
14
+ shuttingDown = false;
15
+ constructor(callbacks) {
16
+ this.callbacks = callbacks;
17
+ }
18
+ addResource(resource) {
19
+ if (this.shuttingDown) {
20
+ closeResource(resource);
21
+ return;
22
+ }
23
+ this.resources.push(resource);
24
+ }
25
+ start(specs) {
26
+ for (const spec of specs) {
27
+ if (this.shuttingDown) {
28
+ return;
29
+ }
30
+ const detached = spec.detached ?? process.platform !== "win32";
31
+ const proc = spawn(spec.command, [...spec.args], {
32
+ cwd: spec.cwd,
33
+ stdio: ["ignore", "pipe", "inherit"],
34
+ detached,
35
+ env: spec.env === undefined
36
+ ? process.env
37
+ : { ...process.env, ...spec.env },
38
+ });
39
+ this.processes.set(proc, detached);
40
+ if (proc.stdout !== null) {
41
+ createInterface({ input: proc.stdout }).on("line", makeChangeLogger(message => this.callbacks.onChange(spec.name, message)));
42
+ }
43
+ proc.on("error", error => {
44
+ if (this.shuttingDown) {
45
+ return;
46
+ }
47
+ this.callbacks.onProcessError(spec.name, error);
48
+ process.exitCode = 1;
49
+ this.shutdown("SIGTERM");
50
+ });
51
+ proc.on("exit", code => {
52
+ this.processes.delete(proc);
53
+ if (this.shuttingDown || code === 0 || code === null) {
54
+ return;
55
+ }
56
+ this.callbacks.onUnexpectedExit(spec.name, code);
57
+ process.exitCode = code;
58
+ this.shutdown("SIGTERM");
59
+ });
60
+ }
61
+ }
62
+ shutdown(signal) {
63
+ if (this.shuttingDown) {
64
+ return;
65
+ }
66
+ this.shuttingDown = true;
67
+ if (signal !== "exit" &&
68
+ (this.processes.size > 0 || this.resources.length > 0)) {
69
+ console.log(`\n${dim}Shutting down...${reset}`);
70
+ }
71
+ for (const resource of this.resources) {
72
+ closeResource(resource);
73
+ }
74
+ for (const [proc, detached] of this.processes) {
75
+ killProcess(proc, detached, "SIGTERM");
76
+ }
77
+ if (signal === "exit") {
78
+ return;
79
+ }
80
+ const timer = setTimeout(() => {
81
+ for (const [proc, detached] of this.processes) {
82
+ killProcess(proc, detached, "SIGKILL");
83
+ }
84
+ process.exit(process.exitCode ?? 0);
85
+ }, SHUTDOWN_GRACE_MS);
86
+ timer.unref();
87
+ }
88
+ }
89
+ function closeResource(resource) {
90
+ try {
91
+ resource();
92
+ }
93
+ catch { }
94
+ }
95
+ function killProcess(proc, detached, signal) {
96
+ if (proc.pid === undefined || proc.killed) {
97
+ return;
98
+ }
99
+ try {
100
+ if (detached && process.platform !== "win32") {
101
+ process.kill(-proc.pid, signal);
102
+ }
103
+ else {
104
+ proc.kill(signal);
105
+ }
106
+ }
107
+ catch { }
108
+ }
109
+ export function installProcessSignalHandlers(run) {
110
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
111
+ process.on(signal, () => run.shutdown(signal));
112
+ }
113
+ process.on("exit", () => run.shutdown("exit"));
114
+ process.on("uncaughtException", error => {
115
+ console.error(error);
116
+ process.exitCode = 1;
117
+ run.shutdown("SIGTERM");
118
+ });
119
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Published launcher entry point for custom block authors. This file is the
3
+ * entry point for `ntn customblocks dev` and `npx @notionhq/custom-blocks-dev-shell`.
4
+ *
5
+ * The shared launcher orchestrates building the worker, reading its manifest,
6
+ * starting one Vite server per custom block, and owning the ports, output, and
7
+ * lifecycle. This entry point starts the dev shell and supplies the package's
8
+ * prebuilt shell adapter with the block registry and data sources.
9
+ *
10
+ * Other subcommands like `convert` exit before any dev shell machinery starts.
11
+ */
12
+ import { existsSync } from "node:fs";
13
+ import { basename, dirname, join, resolve } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { runConvert } from "./convert.js";
16
+ import { readDataSources } from "./data-sources.js";
17
+ import { launchDevShell } from "./dev-shell-launcher.js";
18
+ import { copyPrebuiltDataSources } from "./prebuilt.js";
19
+ import { serveUi } from "./serve-ui.js";
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const createShellLaunch = async ({ args, plan, log }) => {
22
+ const distDir = resolve(__dirname, "..", "dist");
23
+ if (!existsSync(join(distDir, "index.html"))) {
24
+ throw new Error(`No prebuilt UI found at ${distDir}. This package was not assembled ` +
25
+ `correctly; reinstall it.`);
26
+ }
27
+ const prebuiltDir = resolve(__dirname, "..", "data");
28
+ let server;
29
+ try {
30
+ server = await serveUi(distDir, args.shellPort, {
31
+ blocks: plan.registry,
32
+ dataSources: plan.dataSources,
33
+ prebuiltFilenames: readDataSources(prebuiltDir).map(source => source.filename),
34
+ }, () => {
35
+ const written = copyPrebuiltDataSources(prebuiltDir, plan.dataDir);
36
+ if (written.length > 0) {
37
+ log(basename(plan.workerDir), `Added pre-built data sources: ${written.join(", ")}`);
38
+ }
39
+ return readDataSources(plan.dataDir);
40
+ });
41
+ }
42
+ catch (error) {
43
+ const code = error.code;
44
+ if (code === "EADDRINUSE") {
45
+ throw new Error(`Port ${args.shellPort} is already in use. Stop whatever holds it ` +
46
+ `or rerun with --port <port> (and --block-base-port <port> for the ` +
47
+ `block servers).`);
48
+ }
49
+ throw error;
50
+ }
51
+ return {
52
+ processes: [],
53
+ resources: [() => server.close()],
54
+ };
55
+ };
56
+ async function main() {
57
+ const argv = process.argv.slice(2);
58
+ // One-shot subcommands return before any dev shell machinery starts.
59
+ if (argv[0] === "convert") {
60
+ await runConvert(argv.slice(1));
61
+ return;
62
+ }
63
+ await launchDevShell({
64
+ argv,
65
+ workerBaseDir: process.cwd(),
66
+ detectFromDir: process.cwd(),
67
+ createShellLaunch,
68
+ });
69
+ }
70
+ main().catch(error => {
71
+ console.error(error instanceof Error ? error.message : error);
72
+ process.exitCode = 1;
73
+ });
package/dist-cli/utils.js CHANGED
@@ -24,3 +24,28 @@ export function claimUniqueKey(base, used) {
24
24
  used.add(key);
25
25
  return key;
26
26
  }
27
+ /**
28
+ * Parse long options in both `--name value` and `--name=value` forms.
29
+ * Unknown options are passed to `onUnknown`. Callers can ignore or reject them.
30
+ */
31
+ export function parseLongOptions(argv, handlers, onUnknown = () => { }) {
32
+ const takeValue = (name, index) => {
33
+ const value = argv[index];
34
+ if (value === undefined || value.startsWith("--")) {
35
+ throw new Error(`${name} requires a value.`);
36
+ }
37
+ return value;
38
+ };
39
+ for (let index = 0; index < argv.length; index++) {
40
+ const arg = argv[index];
41
+ const separator = arg.indexOf("=");
42
+ const name = separator === -1 ? arg : arg.slice(0, separator);
43
+ const handler = handlers[name];
44
+ if (handler === undefined) {
45
+ onUnknown(arg);
46
+ continue;
47
+ }
48
+ const value = separator === -1 ? takeValue(name, ++index) : arg.slice(separator + 1);
49
+ handler(value);
50
+ }
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks-dev-shell",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "Local preview shell for Notion custom block workers.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,8 +38,8 @@
38
38
  "typescript": "^6.0.3",
39
39
  "vite": "^8.0.10",
40
40
  "vitest": "^4.1.5",
41
- "@notionhq/custom-blocks-host": "0.0.0",
42
- "@notionhq/custom-blocks-protocol": "0.1.0"
41
+ "@notionhq/custom-blocks-protocol": "0.1.0",
42
+ "@notionhq/custom-blocks-host": "0.0.0"
43
43
  },
44
44
  "scripts": {
45
45
  "dev": "vite",