create-ncblock 0.0.14 → 0.0.16

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.14",
3
+ "version": "0.0.16",
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,
@@ -16,6 +16,63 @@ import {
16
16
  type TemplateMetadata,
17
17
  } from "./utils/templates"
18
18
 
19
+ /**
20
+ * Seed `.notion/target.json` with whatever's known at scaffold time. Empty
21
+ * defaults are intentional — subsequent commands (`manifest pull`, `connect`,
22
+ * `deploy`) all merge into this file.
23
+ */
24
+ type InitialTarget = {
25
+ env: string
26
+ block_id: string[]
27
+ data_sources: Record<
28
+ string,
29
+ { data_source_id: string; property_ids_by_key: Record<string, string> }
30
+ >
31
+ }
32
+
33
+ function writeInitialTarget(
34
+ dest: string,
35
+ args: { env?: string; blockId?: string },
36
+ ): void {
37
+ const target: InitialTarget = {
38
+ env: args.env ?? "production",
39
+ block_id: args.blockId ? [args.blockId] : [],
40
+ data_sources: {},
41
+ }
42
+ mkdirSync(resolve(dest, ".notion"), { recursive: true })
43
+ writeFileSync(
44
+ resolve(dest, ".notion/target.json"),
45
+ JSON.stringify(target, null, "\t") + "\n",
46
+ )
47
+ }
48
+
49
+ /**
50
+ * Run the locally-installed `ncblock` CLI from a scaffolded project.
51
+ *
52
+ * We bypass `npx` / `pnpm exec` / `bunx` deliberately: those wrappers can
53
+ * spawn the binary in a way that lets `execSync` return before the child's
54
+ * stdout fully drains, which makes pull-manifest's output land out of order
55
+ * (e.g. after init has already exited). Invoking the .js entrypoint via
56
+ * `node` is synchronous and deterministic.
57
+ *
58
+ * Env is sourced from target.json (written by writeInitialTarget before
59
+ * any ncblock call) — no need to thread ENV through the spawn.
60
+ */
61
+ function runNcblock(dest: string, args: string[]): void {
62
+ const result = spawnSync(
63
+ process.execPath,
64
+ [resolve(dest, "node_modules/ncblock/bin/cli/cli.js"), ...args],
65
+ { cwd: dest, stdio: "inherit" },
66
+ )
67
+ if (result.status !== 0) {
68
+ // Child already printed a user-friendly message; throw so the caller's
69
+ // try/catch can suppress without us having to forward stderr.
70
+ throw new Error(
71
+ `ncblock ${args.join(" ")} exited with code ${result.status}`,
72
+ )
73
+ }
74
+ }
75
+
19
76
  function detectPM(): string {
20
77
  const ua = process.env.npm_config_user_agent ?? ""
21
78
  if (ua.startsWith("pnpm")) {
@@ -61,9 +118,16 @@ function parseArgs(argv: string[]) {
61
118
  args.force = true
62
119
  } else if (arg === "--collection") {
63
120
  args.collection = argv[++i]
121
+ } else if (arg === "--block") {
122
+ args.block = argv[++i]
123
+ } else if (arg === "--local-sdk") {
124
+ args.localSdk = true
64
125
  } else if (!arg.startsWith("-")) {
126
+ // Positional Notion ID has always been captured as a block ID — the
127
+ // variable name (and the deploy hint that uses it) reflect that.
128
+ // Prefer --block when set; otherwise positional is the block.
65
129
  if (NOTION_ID_RE.test(arg)) {
66
- args.blockId = arg
130
+ args.block ??= arg
67
131
  } else {
68
132
  positional.push(arg)
69
133
  }
@@ -499,6 +563,19 @@ async function main() {
499
563
  args.install as boolean | undefined,
500
564
  )
501
565
 
566
+ // Ask for the database URL/ID upfront — alongside the other prompts. The
567
+ // prompt itself only fires when we'll actually install (no manifest pull
568
+ // otherwise), but --collection is always honored.
569
+ const collectionAnswer =
570
+ (args.collection as string | undefined) ??
571
+ (installDeps
572
+ ? (await ask("Database URL/ID (enter to skip):", "skip")).trim()
573
+ : "skip")
574
+ const collectionId =
575
+ collectionAnswer && collectionAnswer !== "skip"
576
+ ? collectionAnswer
577
+ : undefined
578
+
502
579
  printSummary({
503
580
  dest,
504
581
  name,
@@ -507,9 +584,29 @@ async function main() {
507
584
  installDeps,
508
585
  })
509
586
 
510
- scaffoldTemplate({ root, templateDir, dest, name, overwrite: destNonEmpty })
587
+ const useLocalSdk = args.localSdk === true
588
+ // Dynamic import: `pack-local-sdk` depends on `build-sdk`, which is not
589
+ // bundled into the published `create-ncblock` package. Resolving it
590
+ // statically here would crash `bun create ncblock`.
591
+ const sdkDep = useLocalSdk
592
+ ? (await import("./utils/pack-local-sdk")).packLocalSdk(dest)
593
+ : undefined
594
+
595
+ scaffoldTemplate({
596
+ root,
597
+ templateDir,
598
+ dest,
599
+ name,
600
+ overwrite: destNonEmpty,
601
+ sdkDep,
602
+ })
511
603
 
512
604
  step(`Scaffolded project in ${c.bold}${dest}${c.reset}`)
605
+ if (useLocalSdk) {
606
+ step(
607
+ `Using locally-packed SDK ${c.dim}(file:${sdkDep?.replace("file:./", "")})${c.reset}`,
608
+ )
609
+ }
513
610
 
514
611
  if (initGit) {
515
612
  execSync("git init", { cwd: dest, stdio: "ignore" })
@@ -518,45 +615,55 @@ async function main() {
518
615
  step("Initialized git repo")
519
616
  }
520
617
 
618
+ const blockId =
619
+ (args.block as string | undefined) || process.env.BLOCK_ID || undefined
620
+ const envName = process.env.ENV
621
+
622
+ // Always seed .notion/target.json so every project starts in a known
623
+ // state. env / block_id are populated from what we have; data_sources
624
+ // gets filled in by `manifest pull` if it runs.
625
+ writeInitialTarget(dest, { env: envName, blockId })
626
+ step(`Seeded .notion/target.json`)
627
+
521
628
  if (installDeps) {
629
+ // Close the readline interface before shelling out — otherwise it
630
+ // holds onto stdin/stdout and the inherited stdio in child processes
631
+ // gets buffered, which makes pull-manifest output land out of order.
632
+ closePromptInterface()
633
+
522
634
  console.log("")
523
635
  const installArgs = pm === "pnpm" ? "install --ignore-workspace" : "install"
524
636
  execSync(`${pm} ${installArgs}`, { cwd: dest, stdio: "inherit" })
525
637
  step("Installed dependencies")
526
638
 
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") {
639
+ // `manifest pull` resolves a database / data source / collection_view
640
+ // block URL to a manifest entry AND merges the resolved data_source_id
641
+ // into target.json's data_sources. Then `connect` (no flags needed
642
+ // reads everything from target.json) PATCHes the block if one's set.
643
+ let pulled = false
644
+ if (collectionId) {
539
645
  console.log("")
540
646
  try {
541
- execSync(`npx ncblock pull manifest ${blockOrCollectionId}`, {
542
- cwd: dest,
543
- stdio: "inherit",
544
- })
647
+ runNcblock(dest, ["manifest", "pull", collectionId])
545
648
  step("Updated custom_blocks.json from collection schema")
649
+ pulled = true
546
650
  } catch {
547
- // pull manifest already printed a user-friendly message
651
+ // manifest pull already printed a user-friendly message
652
+ }
653
+ }
654
+
655
+ if (pulled && blockId) {
656
+ console.log("")
657
+ try {
658
+ runNcblock(dest, ["connect"])
659
+ step("Wired block bindings (.notion/target.json + PATCH)")
660
+ } catch {
661
+ // connect already printed a user-friendly message
548
662
  }
549
663
  }
550
664
  }
551
665
 
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
666
+ const deployCommand = blockId ? `npx ncblock deploy dist/` : undefined
560
667
 
561
668
  console.log(`\n${c.bold} Ready!${c.reset} Next steps:\n`)
562
669
  if (dest !== resolve(".")) {
@@ -32,6 +32,7 @@ Hooks at a glance:
32
32
 
33
33
  - Mount into `<div id="root">` — `useCustomBlockAutoResize` looks for that exact id.
34
34
  - `custom_blocks.json` at the project root defines the data contract. The SDK's Vite plugin serves it in dev; `ntn` bundles it into the deploy.
35
+ - `custom_blocks.json` is the **portable contract** — no IDs. `.notion/target.json` holds the per-deploy IDs (block IDs, data-source IDs, resolved property IDs) and is committed alongside it.
35
36
 
36
37
  ## Previewing during development
37
38
 
@@ -44,13 +45,22 @@ Two options:
44
45
  cd custom && pnpm install && pnpm run dev:shell # http://localhost:9875
45
46
  ```
46
47
 
47
- ## Deployment
48
+ ## Connecting to Notion
48
49
 
49
- Ask the user to create a block via `/custom` and copy the deploy command. It looks like:
50
+ `ncblock` cli provides helpers to wire a scaffolded project to a Notion block and data source. Wirings are saved to `.notion/target.json`. Check the output of the cli for latest syntax instead of copying these commands verbatim.
50
51
 
52
+ ```bash
53
+ # Pull a schema manifest from a Notion database into custom_blocks.json
54
+ npx ncblock manifest pull <db-id-or-url>
55
+
56
+ # Connect a local project to a Notion block and/or data source.
57
+ npx ncblock connect
58
+
59
+ # Deploy a dist/ directory to a block.
60
+ npx ncblock deploy
51
61
  ```
52
- ntn deploy --block <block-id> <dist-path>
53
- ```
62
+
63
+ Templates will often already have some of these set in `.notion/target.json`.
54
64
 
55
65
  ## Where to look next
56
66
 
package/sdk-version.json CHANGED
@@ -1 +1 @@
1
- {"version":"0.0.12"}
1
+ {"version":"0.0.14"}
@@ -7,6 +7,7 @@ import {
7
7
  type NotionPage,
8
8
  type NotionPageId,
9
9
  pages,
10
+ useCurrentUser,
10
11
  useCustomBlockContext,
11
12
  useDataSourceDefinitions,
12
13
  useTheme,
@@ -47,6 +48,7 @@ const metaTextClass =
47
48
 
48
49
  function App() {
49
50
  const ctx = useCustomBlockContext()
51
+ const currentUser = useCurrentUser()
50
52
  const hostThemeValue = useTheme()
51
53
  const dataSources = useDataSourceDefinitions()
52
54
  const [isPaused, setIsPaused] = useState(false)
@@ -78,6 +80,7 @@ function App() {
78
80
  customBlockId: ctx?.customBlockId,
79
81
  parent: ctx?.parent,
80
82
  page: ctx?.page,
83
+ currentUser,
81
84
  dataSources,
82
85
  }
83
86
 
@@ -93,6 +96,9 @@ function App() {
93
96
  ["viewport", `${viewport.innerWidth} \u00d7 ${viewport.innerHeight}`],
94
97
  ["contentHeight", `${viewport.scrollHeight}px`],
95
98
  ["devicePixelRatio", String(viewport.devicePixelRatio)],
99
+ ["currentUser.id", currentUser.id],
100
+ ["currentUser.name", currentUser.name ?? ""],
101
+ ["currentUser.email", currentUser.person.email],
96
102
  ]
97
103
 
98
104
  return (
@@ -856,7 +862,7 @@ function FilterMenu({
856
862
  </IconButton>
857
863
  {open && (
858
864
  <div className="absolute right-0 top-full z-10 mt-1 w-56 rounded-lg border border-(--border) bg-(--card-bg) p-2 shadow-(--toast-shadow)">
859
- <div className="mb-1 px-1 text-[10px] font-semibold uppercase tracking-[0.05em] opacity-40">
865
+ <div className="mb-1 px-1 text-[10px] font-semibold uppercase tracking-wider opacity-40">
860
866
  Direction
861
867
  </div>
862
868
  {directions.map(direction => (
@@ -875,7 +881,7 @@ function FilterMenu({
875
881
  }}
876
882
  />
877
883
  ))}
878
- <div className="mt-2 mb-1 px-1 text-[10px] font-semibold uppercase tracking-[0.05em] opacity-40">
884
+ <div className="mt-2 mb-1 px-1 text-[10px] font-semibold uppercase tracking-wider opacity-40">
879
885
  Types
880
886
  </div>
881
887
  {availableTypes.length === 0 ? (