@notionhq/custom-blocks-dev-shell 0.1.33 → 0.1.34
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/bin/cli.js +6 -1
- package/dist-cli/block-server.js +3 -6
- package/dist-cli/convert.js +14 -32
- package/dist-cli/dev-shell-launcher.js +195 -0
- package/dist-cli/ports.js +36 -0
- package/dist-cli/process-supervisor.js +119 -0
- package/dist-cli/published-cli.js +73 -0
- package/dist-cli/utils.js +25 -0
- package/package.json +1 -1
- package/dist-cli/main.js +0 -271
package/bin/cli.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Executable wrapper for the published dev shell package.
|
|
4
|
+
*
|
|
5
|
+
* The package exposes this file as its CLI entry point. Keep the wrapper dependency-free.
|
|
6
|
+
*/
|
|
2
7
|
try {
|
|
3
|
-
await import(new URL("../dist-cli/
|
|
8
|
+
await import(new URL("../dist-cli/published-cli.js", import.meta.url))
|
|
4
9
|
} catch (error) {
|
|
5
10
|
if (
|
|
6
11
|
error?.code === "ERR_MODULE_NOT_FOUND" &&
|
package/dist-cli/block-server.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
-
|
|
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) => ({
|
package/dist-cli/convert.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
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
package/dist-cli/main.js
DELETED
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entry point for the published dev shell CLI (`npx`-run from a worker
|
|
3
|
-
* project). Mirrors 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 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 { createInterface } from "node:readline";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { BLOCK_BASE_PORT, buildBlockRegistry, makeChangeLogger, SHELL_PORT, writeBlockViteConfig, } from "./block-server.js";
|
|
15
|
-
import { runConvert } from "./convert.js";
|
|
16
|
-
import { readDataSources } from "./data-sources.js";
|
|
17
|
-
import { materializeWorkerSchemaDataSources } from "./materialize.js";
|
|
18
|
-
import { copyPrebuiltDataSources } from "./prebuilt.js";
|
|
19
|
-
import { serveUi } from "./serve-ui.js";
|
|
20
|
-
import { blockCapabilities, findWorkerDir, generateWorkerManifest, } from "./worker-manifest.js";
|
|
21
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
const dim = "\x1b[2m";
|
|
23
|
-
const bold = "\x1b[1m";
|
|
24
|
-
const cyan = "\x1b[36m";
|
|
25
|
-
const reset = "\x1b[0m";
|
|
26
|
-
const label = (name) => `${cyan}[${name}]${reset}`;
|
|
27
|
-
function parseCliArgs(argv) {
|
|
28
|
-
const args = {
|
|
29
|
-
worker: undefined,
|
|
30
|
-
shellPort: SHELL_PORT,
|
|
31
|
-
blockBasePort: BLOCK_BASE_PORT,
|
|
32
|
-
};
|
|
33
|
-
const takeValue = (name, index) => {
|
|
34
|
-
const value = argv[index];
|
|
35
|
-
if (value === undefined || value.startsWith("--")) {
|
|
36
|
-
throw new Error(`${name} requires a value.`);
|
|
37
|
-
}
|
|
38
|
-
return value;
|
|
39
|
-
};
|
|
40
|
-
const takePort = (name, raw) => {
|
|
41
|
-
const port = Number(raw);
|
|
42
|
-
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
43
|
-
throw new Error(`${name} requires a port number, got "${raw}".`);
|
|
44
|
-
}
|
|
45
|
-
return port;
|
|
46
|
-
};
|
|
47
|
-
for (let index = 0; index < argv.length; index++) {
|
|
48
|
-
const arg = argv[index];
|
|
49
|
-
if (arg === "--worker") {
|
|
50
|
-
args.worker = takeValue("--worker", ++index);
|
|
51
|
-
}
|
|
52
|
-
else if (arg.startsWith("--worker=")) {
|
|
53
|
-
args.worker = arg.slice("--worker=".length);
|
|
54
|
-
if (args.worker.length === 0) {
|
|
55
|
-
throw new Error("--worker requires a path to a worker directory.");
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
else if (arg === "--port") {
|
|
59
|
-
args.shellPort = takePort("--port", takeValue("--port", ++index));
|
|
60
|
-
}
|
|
61
|
-
else if (arg.startsWith("--port=")) {
|
|
62
|
-
args.shellPort = takePort("--port", arg.slice("--port=".length));
|
|
63
|
-
}
|
|
64
|
-
else if (arg === "--block-base-port") {
|
|
65
|
-
args.blockBasePort = takePort("--block-base-port", takeValue("--block-base-port", ++index));
|
|
66
|
-
}
|
|
67
|
-
else if (arg.startsWith("--block-base-port=")) {
|
|
68
|
-
args.blockBasePort = takePort("--block-base-port", arg.slice("--block-base-port=".length));
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return args;
|
|
72
|
-
}
|
|
73
|
-
function resolveWorkerDir(workerArg) {
|
|
74
|
-
if (workerArg !== undefined) {
|
|
75
|
-
return resolve(process.cwd(), workerArg);
|
|
76
|
-
}
|
|
77
|
-
const detected = findWorkerDir(process.cwd());
|
|
78
|
-
if (detected !== undefined) {
|
|
79
|
-
console.log(`Detected a worker at ${detected}.`);
|
|
80
|
-
return detected;
|
|
81
|
-
}
|
|
82
|
-
throw new Error("No worker found: run from inside a worker directory, or pass --worker <dir>.");
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* The worker's own Vite binary. Blocks are served with the worker's Vite (and
|
|
86
|
-
* plugins) rather than anything bundled here, matching how the block builds in
|
|
87
|
-
* production.
|
|
88
|
-
*/
|
|
89
|
-
function resolveViteBin(workerDir) {
|
|
90
|
-
let vitePkgPath;
|
|
91
|
-
try {
|
|
92
|
-
const workerRequire = createRequire(join(workerDir, "package.json"));
|
|
93
|
-
vitePkgPath = workerRequire.resolve("vite/package.json");
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
throw new Error(`Could not resolve "vite" from ${workerDir}. Add vite to the worker's ` +
|
|
97
|
-
`devDependencies and reinstall.`);
|
|
98
|
-
}
|
|
99
|
-
const vitePkg = JSON.parse(readFileSync(vitePkgPath, "utf-8"));
|
|
100
|
-
const bin = typeof vitePkg.bin === "string" ? vitePkg.bin : vitePkg.bin?.vite;
|
|
101
|
-
if (bin === undefined) {
|
|
102
|
-
throw new Error(`The vite package at ${vitePkgPath} exposes no bin.`);
|
|
103
|
-
}
|
|
104
|
-
return resolve(dirname(vitePkgPath), bin);
|
|
105
|
-
}
|
|
106
|
-
const procs = [];
|
|
107
|
-
async function main() {
|
|
108
|
-
const argv = process.argv.slice(2);
|
|
109
|
-
// One-shot subcommands return before any dev-server machinery starts;
|
|
110
|
-
// with no subcommand the CLI is the dev shell, as it always was.
|
|
111
|
-
if (argv[0] === "convert") {
|
|
112
|
-
await runConvert(argv.slice(1));
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const cliArgs = parseCliArgs(argv);
|
|
116
|
-
const workerDir = resolveWorkerDir(cliArgs.worker);
|
|
117
|
-
if (!existsSync(resolve(workerDir, "node_modules"))) {
|
|
118
|
-
throw new Error(`No node_modules in ${workerDir}. Install the worker's dependencies first ` +
|
|
119
|
-
`(e.g. \`npm install\`), then rerun.`);
|
|
120
|
-
}
|
|
121
|
-
console.log(`${label(basename(workerDir))} Extracting worker manifest...`);
|
|
122
|
-
const { manifest, manifestPath } = await generateWorkerManifest(workerDir);
|
|
123
|
-
console.log(`${label(basename(workerDir))} Wrote ${manifestPath}`);
|
|
124
|
-
const blocks = blockCapabilities(manifest);
|
|
125
|
-
if (blocks.length === 0) {
|
|
126
|
-
// Not an error — start the shell anyway; it shows "None" under Blocks.
|
|
127
|
-
console.log(`${label(basename(workerDir))} Worker declares no custom blocks.`);
|
|
128
|
-
}
|
|
129
|
-
// Materialize schema-only files for the worker's declared sources, then
|
|
130
|
-
// load the directory. Files are validated before injection; a malformed
|
|
131
|
-
// file fails spin-up with the problem named.
|
|
132
|
-
const dataDir = resolve(workerDir, "src/data");
|
|
133
|
-
materializeWorkerSchemaDataSources(manifest, dataDir);
|
|
134
|
-
const dataSources = readDataSources(dataDir);
|
|
135
|
-
console.log(dataSources.length > 0
|
|
136
|
-
? `${label(basename(workerDir))} Data sources from src/data: ${dataSources
|
|
137
|
-
.map(source => source.name)
|
|
138
|
-
.join(", ")}`
|
|
139
|
-
: `${label(basename(workerDir))} No data sources — create src/data/<key>.json files ` +
|
|
140
|
-
`(format: node_modules/@notionhq/custom-blocks-dev-shell/docs/data-sources.md).`);
|
|
141
|
-
if (blocks.length > 0 &&
|
|
142
|
-
cliArgs.shellPort >= cliArgs.blockBasePort &&
|
|
143
|
-
cliArgs.shellPort < cliArgs.blockBasePort + blocks.length) {
|
|
144
|
-
throw new Error(`--port ${cliArgs.shellPort} collides with the block server ports ` +
|
|
145
|
-
`(${cliArgs.blockBasePort}–${cliArgs.blockBasePort + blocks.length - 1}); ` +
|
|
146
|
-
`pick a port outside that range or move --block-base-port.`);
|
|
147
|
-
}
|
|
148
|
-
const viteBin = blocks.length > 0 ? resolveViteBin(workerDir) : undefined;
|
|
149
|
-
const registry = buildBlockRegistry(blocks, cliArgs.blockBasePort);
|
|
150
|
-
for (const [index, capability] of blocks.entries()) {
|
|
151
|
-
const blockDir = resolve(workerDir, capability.config.source.path);
|
|
152
|
-
const configFile = writeBlockViteConfig(workerDir, blockDir, capability);
|
|
153
|
-
const port = cliArgs.blockBasePort + index;
|
|
154
|
-
const proc = spawn(process.execPath, [
|
|
155
|
-
viteBin,
|
|
156
|
-
"--config",
|
|
157
|
-
configFile,
|
|
158
|
-
"--port",
|
|
159
|
-
String(port),
|
|
160
|
-
"--strictPort",
|
|
161
|
-
], {
|
|
162
|
-
cwd: workerDir,
|
|
163
|
-
stdio: ["ignore", "pipe", "inherit"],
|
|
164
|
-
// Process groups (and negative-PID kills) are POSIX-only; on
|
|
165
|
-
// Windows children are killed individually in shutdown().
|
|
166
|
-
detached: process.platform !== "win32",
|
|
167
|
-
});
|
|
168
|
-
if (proc.stdout !== null) {
|
|
169
|
-
createInterface({ input: proc.stdout }).on("line", makeChangeLogger(message => console.log(`${label(capability.key)} ${message}`)));
|
|
170
|
-
}
|
|
171
|
-
proc.on("exit", code => {
|
|
172
|
-
if (shuttingDown || code === 0 || code === null) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
console.error(`${label(capability.key)} dev server exited with code ${code}`);
|
|
176
|
-
process.exitCode = code;
|
|
177
|
-
shutdown("SIGTERM");
|
|
178
|
-
});
|
|
179
|
-
procs.push(proc);
|
|
180
|
-
}
|
|
181
|
-
// The published layout is dist/ next to cli/; index.html must be prebuilt.
|
|
182
|
-
const distDir = resolve(__dirname, "..", "dist");
|
|
183
|
-
if (!existsSync(join(distDir, "index.html"))) {
|
|
184
|
-
throw new Error(`No prebuilt UI found at ${distDir}. This package was not assembled ` +
|
|
185
|
-
`correctly; reinstall it.`);
|
|
186
|
-
}
|
|
187
|
-
const prebuiltDir = resolve(__dirname, "..", "data");
|
|
188
|
-
try {
|
|
189
|
-
await serveUi(distDir, cliArgs.shellPort, {
|
|
190
|
-
blocks: registry,
|
|
191
|
-
dataSources,
|
|
192
|
-
prebuiltFilenames: readDataSources(prebuiltDir).map(source => source.filename),
|
|
193
|
-
}, () => {
|
|
194
|
-
const written = copyPrebuiltDataSources(prebuiltDir, dataDir);
|
|
195
|
-
if (written.length > 0) {
|
|
196
|
-
console.log(`${label(basename(workerDir))} Added pre-built data sources: ` +
|
|
197
|
-
written.join(", "));
|
|
198
|
-
}
|
|
199
|
-
return readDataSources(dataDir);
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
catch (error) {
|
|
203
|
-
const code = error.code;
|
|
204
|
-
if (code === "EADDRINUSE") {
|
|
205
|
-
throw new Error(`Port ${cliArgs.shellPort} is already in use. Stop whatever holds it ` +
|
|
206
|
-
`or rerun with --port <port> (and --block-base-port <port> for the ` +
|
|
207
|
-
`block servers).`);
|
|
208
|
-
}
|
|
209
|
-
throw error;
|
|
210
|
-
}
|
|
211
|
-
console.log("");
|
|
212
|
-
console.log(`${bold}Dev shell${reset}`);
|
|
213
|
-
console.log(` ${label("dev-shell")} ${dim}http://localhost:${cliArgs.shellPort}${reset}`);
|
|
214
|
-
if (blocks.length > 0) {
|
|
215
|
-
console.log("");
|
|
216
|
-
console.log(`${bold}Blocks${reset}`);
|
|
217
|
-
for (const [index, entry] of registry.entries()) {
|
|
218
|
-
console.log(` ${label(entry.key)} ${dim}http://localhost:${cliArgs.blockBasePort + index}${reset}`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
console.log("");
|
|
222
|
-
}
|
|
223
|
-
function killProc(p, signal) {
|
|
224
|
-
if (p.pid === undefined || p.killed) {
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
try {
|
|
228
|
-
if (process.platform === "win32") {
|
|
229
|
-
p.kill(signal);
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
process.kill(-p.pid, signal);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
catch { }
|
|
236
|
-
}
|
|
237
|
-
let shuttingDown = false;
|
|
238
|
-
function shutdown(signal) {
|
|
239
|
-
if (shuttingDown) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
shuttingDown = true;
|
|
243
|
-
if (signal !== "exit" && procs.length > 0) {
|
|
244
|
-
console.log(`\n${dim}Shutting down...${reset}`);
|
|
245
|
-
}
|
|
246
|
-
for (const p of procs) {
|
|
247
|
-
killProc(p, "SIGTERM");
|
|
248
|
-
}
|
|
249
|
-
setTimeout(() => {
|
|
250
|
-
for (const p of procs) {
|
|
251
|
-
killProc(p, "SIGKILL");
|
|
252
|
-
}
|
|
253
|
-
// Preserve a failure exit code set before shutdown (startup errors,
|
|
254
|
-
// crashed block servers); plain signal shutdowns still exit 0.
|
|
255
|
-
process.exit(process.exitCode ?? 0);
|
|
256
|
-
}, 1500).unref();
|
|
257
|
-
}
|
|
258
|
-
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
259
|
-
process.on(signal, () => shutdown(signal));
|
|
260
|
-
}
|
|
261
|
-
process.on("exit", () => shutdown("exit"));
|
|
262
|
-
process.on("uncaughtException", err => {
|
|
263
|
-
console.error(err);
|
|
264
|
-
process.exitCode = 1;
|
|
265
|
-
shutdown("SIGTERM");
|
|
266
|
-
});
|
|
267
|
-
main().catch(err => {
|
|
268
|
-
console.error(err instanceof Error ? err.message : err);
|
|
269
|
-
process.exitCode = 1;
|
|
270
|
-
shutdown("SIGTERM");
|
|
271
|
-
});
|