create-ncblock 0.0.15 → 0.0.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ncblock",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "Create a Notion custom view block project.",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/init.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { execSync } from "child_process"
2
- import { existsSync, readdirSync } from "fs"
1
+ import { execSync, spawnSync } from "child_process"
2
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs"
3
3
  import { basename, dirname, resolve } from "path"
4
4
  import {
5
5
  clearScreenDown,
@@ -9,6 +9,7 @@ import {
9
9
  moveCursor,
10
10
  } from "readline"
11
11
  import { fileURLToPath } from "url"
12
+ import { resolveInitArgs } from "./utils/resolveInitArgs"
12
13
  import {
13
14
  getTemplateByName,
14
15
  getTemplates,
@@ -16,6 +17,63 @@ import {
16
17
  type TemplateMetadata,
17
18
  } from "./utils/templates"
18
19
 
20
+ /**
21
+ * Seed `.notion/target.json` with whatever's known at scaffold time. Empty
22
+ * defaults are intentional — subsequent commands (`manifest pull`, `connect`,
23
+ * `deploy`) all merge into this file.
24
+ */
25
+ type InitialTarget = {
26
+ env: string
27
+ block_id: string[]
28
+ data_sources: Record<
29
+ string,
30
+ { data_source_id: string; property_ids_by_key: Record<string, string> }
31
+ >
32
+ }
33
+
34
+ function writeInitialTarget(
35
+ dest: string,
36
+ args: { env?: string; blockId?: string },
37
+ ): void {
38
+ const target: InitialTarget = {
39
+ env: args.env ?? "production",
40
+ block_id: args.blockId ? [args.blockId] : [],
41
+ data_sources: {},
42
+ }
43
+ mkdirSync(resolve(dest, ".notion"), { recursive: true })
44
+ writeFileSync(
45
+ resolve(dest, ".notion/target.json"),
46
+ JSON.stringify(target, null, "\t") + "\n",
47
+ )
48
+ }
49
+
50
+ /**
51
+ * Run the locally-installed `ncblock` CLI from a scaffolded project.
52
+ *
53
+ * We bypass `npx` / `pnpm exec` / `bunx` deliberately: those wrappers can
54
+ * spawn the binary in a way that lets `execSync` return before the child's
55
+ * stdout fully drains, which makes pull-manifest's output land out of order
56
+ * (e.g. after init has already exited). Invoking the .js entrypoint via
57
+ * `node` is synchronous and deterministic.
58
+ *
59
+ * Env is sourced from target.json (written by writeInitialTarget before
60
+ * any ncblock call) — no need to thread ENV through the spawn.
61
+ */
62
+ function runNcblock(dest: string, args: string[]): void {
63
+ const result = spawnSync(
64
+ process.execPath,
65
+ [resolve(dest, "node_modules/ncblock/bin/cli/cli.js"), ...args],
66
+ { cwd: dest, stdio: "inherit" },
67
+ )
68
+ if (result.status !== 0) {
69
+ // Child already printed a user-friendly message; throw so the caller's
70
+ // try/catch can suppress without us having to forward stderr.
71
+ throw new Error(
72
+ `ncblock ${args.join(" ")} exited with code ${result.status}`,
73
+ )
74
+ }
75
+ }
76
+
19
77
  function detectPM(): string {
20
78
  const ua = process.env.npm_config_user_agent ?? ""
21
79
  if (ua.startsWith("pnpm")) {
@@ -61,9 +119,16 @@ function parseArgs(argv: string[]) {
61
119
  args.force = true
62
120
  } else if (arg === "--collection") {
63
121
  args.collection = argv[++i]
122
+ } else if (arg === "--block") {
123
+ args.block = argv[++i]
124
+ } else if (arg === "--local-sdk") {
125
+ args.localSdk = true
64
126
  } else if (!arg.startsWith("-")) {
127
+ // Positional Notion ID has always been captured as a block ID — the
128
+ // variable name (and the deploy hint that uses it) reflect that.
129
+ // Prefer --block when set; otherwise positional is the block.
65
130
  if (NOTION_ID_RE.test(arg)) {
66
- args.blockId = arg
131
+ args.block ??= arg
67
132
  } else {
68
133
  positional.push(arg)
69
134
  }
@@ -499,6 +564,31 @@ async function main() {
499
564
  args.install as boolean | undefined,
500
565
  )
501
566
 
567
+ // Ask for the database URL/ID upfront — alongside the other prompts.
568
+ // Precedence: --collection > --block/positional > prompt. A positional
569
+ // Notion ID is almost always a block inside a collection_view (or the
570
+ // view block itself); `manifest pull`'s resolveId() handles both cases,
571
+ // so we can feed the block ID straight through without re-prompting.
572
+ const blockIdArg = args.block as string | undefined
573
+ const promptedCollection =
574
+ args.collection === undefined && blockIdArg === undefined && installDeps
575
+ ? (await ask("Database URL/ID (enter to skip):", "skip")).trim()
576
+ : undefined
577
+ const collectionOverride =
578
+ (args.collection as string | undefined) ??
579
+ (promptedCollection && promptedCollection !== "skip"
580
+ ? promptedCollection
581
+ : undefined)
582
+
583
+ const resolved = resolveInitArgs({
584
+ collection: collectionOverride,
585
+ block: blockIdArg,
586
+ envBlockId: process.env.BLOCK_ID,
587
+ envName: process.env.ENV,
588
+ installDeps,
589
+ })
590
+ const { blockId, collectionId } = resolved
591
+
502
592
  printSummary({
503
593
  dest,
504
594
  name,
@@ -507,9 +597,29 @@ async function main() {
507
597
  installDeps,
508
598
  })
509
599
 
510
- scaffoldTemplate({ root, templateDir, dest, name, overwrite: destNonEmpty })
600
+ const useLocalSdk = args.localSdk === true
601
+ // Dynamic import: `pack-local-sdk` depends on `build-sdk`, which is not
602
+ // bundled into the published `create-ncblock` package. Resolving it
603
+ // statically here would crash `bun create ncblock`.
604
+ const sdkDep = useLocalSdk
605
+ ? (await import("./utils/pack-local-sdk")).packLocalSdk(dest)
606
+ : undefined
607
+
608
+ scaffoldTemplate({
609
+ root,
610
+ templateDir,
611
+ dest,
612
+ name,
613
+ overwrite: destNonEmpty,
614
+ sdkDep,
615
+ })
511
616
 
512
617
  step(`Scaffolded project in ${c.bold}${dest}${c.reset}`)
618
+ if (useLocalSdk) {
619
+ step(
620
+ `Using locally-packed SDK ${c.dim}(file:${sdkDep?.replace("file:./", "")})${c.reset}`,
621
+ )
622
+ }
513
623
 
514
624
  if (initGit) {
515
625
  execSync("git init", { cwd: dest, stdio: "ignore" })
@@ -518,45 +628,51 @@ async function main() {
518
628
  step("Initialized git repo")
519
629
  }
520
630
 
631
+ // Always seed .notion/target.json so every project starts in a known
632
+ // state. env / block_id are populated from what we have; data_sources
633
+ // gets filled in by `manifest pull` if it runs.
634
+ writeInitialTarget(dest, { env: resolved.env, blockId })
635
+ step(`Seeded .notion/target.json`)
636
+
637
+ let pulled = false
521
638
  if (installDeps) {
639
+ // Close the readline interface before shelling out — otherwise it
640
+ // holds onto stdin/stdout and the inherited stdio in child processes
641
+ // gets buffered, which makes pull-manifest output land out of order.
642
+ closePromptInterface()
643
+
522
644
  console.log("")
523
645
  const installArgs = pm === "pnpm" ? "install --ignore-workspace" : "install"
524
646
  execSync(`${pm} ${installArgs}`, { cwd: dest, stdio: "inherit" })
525
647
  step("Installed dependencies")
526
648
 
527
- // Resolve a block/database ID to populate custom_blocks.json. Prefer
528
- // explicit flags/env over prompting if the user already gave us an
529
- // ID, `ncblock pull manifest` can resolve it (block or database) on
530
- // its own, no prompt needed.
531
- const explicitId =
532
- (args.collection as string | undefined) ??
533
- (args.blockId as string | undefined) ??
534
- process.env.BLOCK_ID
535
- const blockOrCollectionId =
536
- explicitId ??
537
- (await ask("Database URL/ID (enter to skip):", "skip")).trim()
538
- if (blockOrCollectionId && blockOrCollectionId !== "skip") {
649
+ // `manifest pull` resolves a database / data source / collection_view
650
+ // block URL to a manifest entry AND merges the resolved data_source_id
651
+ // into target.json's data_sources. Then `connect` (no flags needed
652
+ // reads everything from target.json) PATCHes the block if one's set.
653
+ if (collectionId) {
539
654
  console.log("")
540
655
  try {
541
- execSync(`npx ncblock pull manifest ${blockOrCollectionId}`, {
542
- cwd: dest,
543
- stdio: "inherit",
544
- })
656
+ runNcblock(dest, ["manifest", "pull", collectionId])
545
657
  step("Updated custom_blocks.json from collection schema")
658
+ pulled = true
659
+ } catch {
660
+ // manifest pull already printed a user-friendly message
661
+ }
662
+ }
663
+
664
+ if (pulled && blockId) {
665
+ console.log("")
666
+ try {
667
+ runNcblock(dest, ["connect"])
668
+ step("Wired block bindings (.notion/target.json + PATCH)")
546
669
  } catch {
547
- // pull manifest already printed a user-friendly message
670
+ // connect already printed a user-friendly message
548
671
  }
549
672
  }
550
673
  }
551
674
 
552
- const blockId = (args.blockId as string | undefined) || process.env.BLOCK_ID
553
- const envName = process.env.ENV
554
- const deployCommand =
555
- blockId && envName
556
- ? `NOTION_KEYRING=0 ntn --env ${envName} custom deploy --block ${blockId} dist/`
557
- : blockId
558
- ? `ntn custom deploy --block ${blockId} dist/`
559
- : undefined
675
+ const deployCommand = blockId ? `npx ncblock deploy dist/` : undefined
560
676
 
561
677
  console.log(`\n${c.bold} Ready!${c.reset} Next steps:\n`)
562
678
  if (dest !== resolve(".")) {
@@ -567,12 +683,29 @@ async function main() {
567
683
  }
568
684
  console.log(` ${pm} run build`)
569
685
  if (deployCommand) {
570
- console.log(` ${deployCommand}\n`)
686
+ console.log(` ${deployCommand}`)
571
687
  } else {
572
688
  console.log(
573
- `\n Then, go back to the instructions in Notion and paste the ${c.bold}ntn deploy${c.reset} command.\n`,
689
+ `\n Then, go back to the instructions in Notion and paste the ${c.bold}ntn deploy${c.reset} command.`,
690
+ )
691
+ }
692
+
693
+ // If install ran but no data source was wired (no collectionId, or a
694
+ // pull/connect failure), surface the manual CLI commands explicitly.
695
+ // Agents read this scrollback — being concrete here keeps them from
696
+ // reinventing the bridge or hardcoding IDs.
697
+ if (installDeps && !pulled) {
698
+ console.log(
699
+ `\n ${c.yellow}No data source connected yet.${c.reset} To wire one up:`,
574
700
  )
701
+ console.log(` npx ncblock manifest pull <database-url-or-id>`)
702
+ if (!blockId) {
703
+ console.log(` npx ncblock connect --block <block-id-or-url>`)
704
+ } else {
705
+ console.log(` npx ncblock connect`)
706
+ }
575
707
  }
708
+ console.log("")
576
709
 
577
710
  closePromptInterface()
578
711
  }
@@ -2,6 +2,14 @@
2
2
 
3
3
  A **Notion custom view** — a sandboxed `<iframe>` rendered inside a Notion block. The iframe is the entire viewport; the only channel to the host is a `postMessage` bridge wrapped by `ncblock`.
4
4
 
5
+ ## Connecting to a Notion database
6
+
7
+ If `custom_blocks.json` or `.notion/target.json` doesn't yet reference a datasource, ask the user if they want to connect a database by url or id. Use the `npx ncblock` cli to connect the database and the project. Discover available commands with:
8
+
9
+ ```bash
10
+ npx ncblock --help
11
+ ```
12
+
5
13
  ## Talking to the host
6
14
 
7
15
  - Always use the React hooks from `ncblock`. Never call `window.parent.postMessage` directly — the SDK owns the protocol.
@@ -44,14 +52,6 @@ Two options:
44
52
  cd custom && pnpm install && pnpm run dev:shell # http://localhost:9875
45
53
  ```
46
54
 
47
- ## Deployment
48
-
49
- Ask the user to create a block via `/custom` and copy the deploy command. It looks like:
50
-
51
- ```
52
- ntn deploy --block <block-id> <dist-path>
53
- ```
54
-
55
55
  ## Where to look next
56
56
 
57
57
  - `node_modules/ncblock/README.md` — landing page with a TOC into the per-category docs below.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Pure resolution of init.ts's data-source / block / env wiring decisions.
3
+ *
4
+ * Pulled out of init.ts so the precedence rules — especially "positional/--block
5
+ * Notion ID should also drive `manifest pull` when no --collection is given" —
6
+ * are unit-testable without spawning a tsx subprocess.
7
+ */
8
+
9
+ export type InitArgsInput = {
10
+ /** Value of --collection, if explicitly passed. */
11
+ collection?: string
12
+ /** Value of --block or a positional Notion ID, if passed. */
13
+ block?: string
14
+ /** `process.env.BLOCK_ID` — used as a last resort for block resolution. */
15
+ envBlockId?: string
16
+ /** `process.env.ENV` — populates `.notion/target.json#env`. */
17
+ envName?: string
18
+ /** Whether `init` will run install + ncblock pull/connect (--install / --no-install). */
19
+ installDeps: boolean
20
+ }
21
+
22
+ export type InitArgsResolution = {
23
+ blockId: string | undefined
24
+ collectionId: string | undefined
25
+ env: string
26
+ /** Will init invoke `ncblock manifest pull`? */
27
+ willPullManifest: boolean
28
+ /** Will init invoke `ncblock connect`? (only when pull will also run) */
29
+ willConnect: boolean
30
+ }
31
+
32
+ /**
33
+ * Decide what init will actually do, given the args. Precedence:
34
+ *
35
+ * 1. `--collection` is the explicit manifest-pull target (highest priority).
36
+ * 2. Otherwise, a positional/`--block` Notion ID doubles as the pull target —
37
+ * `manifest pull`'s resolveId() handles "block inside collection_view" and
38
+ * walks to the parent data source. This is the path users hit with
39
+ * `bun create ncblock <block-id>`.
40
+ * 3. When neither is set, no pull happens.
41
+ *
42
+ * `manifest pull` and `connect` only run when install ran (otherwise the
43
+ * ncblock CLI isn't on disk to call).
44
+ */
45
+ export function resolveInitArgs(input: InitArgsInput): InitArgsResolution {
46
+ const blockId = input.block || input.envBlockId || undefined
47
+ // A block ID doubles as the pull target: `manifest pull` accepts a block
48
+ // (resolveId walks block → parent → data source), and that's the path
49
+ // users hit with `bun create ncblock <block-id>`. Derived from `blockId`
50
+ // rather than `input.block` directly so envBlockId flows through too —
51
+ // no point treating `--block` and `BLOCK_ID=` differently.
52
+ const collectionId = input.collection ?? blockId
53
+ const env = input.envName ?? "production"
54
+
55
+ const willPullManifest = input.installDeps && collectionId !== undefined
56
+ const willConnect = willPullManifest && blockId !== undefined
57
+
58
+ return { blockId, collectionId, env, willPullManifest, willConnect }
59
+ }
package/sdk-version.json CHANGED
@@ -1 +1 @@
1
- {"version":"0.0.13"}
1
+ {"version":"0.0.15"}