@notionhq/custom-blocks-dev-shell 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,196 @@
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
+ }