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 +1 -1
- package/scripts/init.ts +164 -31
- package/scripts/scaffold-assets/AGENTS.md +8 -8
- package/scripts/utils/resolveInitArgs.ts +59 -0
- package/sdk-version.json +1 -1
package/package.json
CHANGED
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
670
|
+
// connect already printed a user-friendly message
|
|
548
671
|
}
|
|
549
672
|
}
|
|
550
673
|
}
|
|
551
674
|
|
|
552
|
-
const
|
|
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}
|
|
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
|
|
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.
|
|
1
|
+
{"version":"0.0.15"}
|