@nubbin/next 0.1.1 → 0.3.0

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/README.md CHANGED
@@ -25,6 +25,5 @@ await publishRoute(store, "/promotions/summer", hash); // pointer, then revali
25
25
  page in the gap, and the publish appears to have silently not happened. A store rejection
26
26
  propagates without invalidating, so a failed publish never purges a working page.
27
27
 
28
- **Release candidate.** Requires Next 16 or newer.
29
-
30
- <https://effekt.github.io/nubbin/>. MIT.
28
+ Requires Next.js 16 or newer. Read the [Nubbin documentation](https://nubbin.io) for the
29
+ complete Next.js integration reference. MIT.
package/dist/index.d.ts CHANGED
@@ -2,31 +2,48 @@ import { Artifact, FieldHintData, ArtifactStore } from '@nubbin/core';
2
2
  import { Metadata } from 'next';
3
3
 
4
4
  /**
5
- * Maps a compiled artifact's `meta` onto Next's `Metadata`, so the mapping is owned by the
6
- * binding rather than re-decided in every consumer's `generateMetadata` — the same reason
7
- * `holeFetchOptions` owns the hole-lifecycle mapping.
5
+ * Turns a resolved artifact into the `Metadata` Next emits into the document head, so the title
6
+ * and meta tags a page carries come from the document that was published rather than from the
7
+ * code serving it.
8
8
  *
9
- * `compile` has written `meta` into every artifact since the first one, and until this existed
10
- * nothing read it: a published page could not set its own title. The shapes differ in one place
11
- * — a document's `canonical` is a flat field, and Next reads a canonical URL from
12
- * `alternates.canonical` — so that is the only thing this translates rather than copies.
13
- *
14
- * A null artifact returns an empty object rather than a title, because the caller is about to
15
- * render a 404 and the layout's own metadata is the right answer for it.
16
- *
17
- * Optional fields are assigned only when present. `exactOptionalPropertyTypes` is on, and a
18
- * `description: undefined` sent to Next is an instruction to emit an empty tag rather than an
19
- * absence.
9
+ * @param artifact - What `resolveArtifact` answered with. `null` is the unpublished case and
10
+ * belongs here, so a caller resolves once and hands the same value to the page and to
11
+ * `generateMetadata`.
12
+ * @returns `title` always, `description` and `robots` when the document carried them, and
13
+ * `canonical` moved under `alternates`, which is where Next reads one. A field the document
14
+ * omitted is omitted here rather than set to `undefined`. A `null` artifact answers `{}`,
15
+ * leaving the layout's own metadata standing for the 404 the page is about to render.
16
+ * @example
17
+ * ```ts
18
+ * export async function generateMetadata({
19
+ * params,
20
+ * }: { params: Promise<{ slug?: string[] }> }): Promise<Metadata> {
21
+ * const { slug } = await params;
22
+ * return artifactMetadata(await resolveArtifact(store, slug));
23
+ * }
24
+ * ```
20
25
  */
21
26
  declare function artifactMetadata(artifact: Artifact | null): Metadata;
22
27
 
23
28
  /**
24
- * Maps a hole's declared lifecycle onto Next's fetch cache, so the mapping is owned by the
25
- * binding rather than re-decided in every consumer's resolver.
29
+ * Turns a hole's declared lifecycle into the `fetch` options Next reads, for a `resolveHole`
30
+ * fetching the value that fills it.
26
31
  *
27
- * Takes core's `FieldHintData` directly. An earlier plan derived a local `HoleSpec` from
28
- * `ArtifactNode["holes"]` so two packages would not import each other; core exports the type
29
- * by name, so both import it from core and neither derivation is needed.
32
+ * @param spec - The hole's `FieldHintData`, as `compile` copied it from the block's `ui.fields`
33
+ * hint into the artifact node's `holes`. Pass it through rather than choosing an interval at
34
+ * the call site: the published artifact is what says how live the value is.
35
+ * @returns `{ next: { revalidate: spec.revalidate } }`, ready to spread into a `fetch` call.
36
+ * `0` reaches Next as `0` rather than being read as absent, which is Next's instruction not to
37
+ * cache the response at all.
38
+ * @example
39
+ * ```ts
40
+ * import type { HoleResolver } from "@nubbin/react";
41
+ *
42
+ * const resolveHole: HoleResolver = async ({ spec }) => {
43
+ * const response = await fetch(`${origin}/api/price`, holeFetchOptions(spec));
44
+ * return response.json();
45
+ * };
46
+ * ```
30
47
  */
31
48
  declare function holeFetchOptions(spec: FieldHintData): RequestInit & {
32
49
  next: {
@@ -35,31 +52,122 @@ declare function holeFetchOptions(spec: FieldHintData): RequestInit & {
35
52
  };
36
53
 
37
54
  /**
38
- * Pointer first, invalidation second. The reverse order re-caches the outgoing page during
39
- * the gap, and the publish appears to have silently not happened. The store's own publish
40
- * rejects a hash that was never written, so a failed publish never purges a working page.
55
+ * Points a route at an artifact and invalidates that one page, in that order — the body of the
56
+ * `POST /api/nubbin/publish` handler an application exposes, which is where `nubbin publish
57
+ * --origin <url>` sends `{ route, hash }`.
58
+ *
59
+ * `revalidatePath` reaches only the cache of the process that runs it, so this has to run inside
60
+ * the server that serves the page. Moving the pointer from a terminal instead leaves that server
61
+ * answering from its cache until it restarts.
62
+ *
63
+ * @param store - The store serving the application. `publish` is the only method called.
64
+ * @param route - The route to serve, exactly as it was compiled — `resolveArtifact` matches a
65
+ * pointer by its route string, so a route published under a different spelling never resolves.
66
+ * @param hash - The artifact to serve there. Write it first: this moves a pointer and never
67
+ * stores anything, and a pointer at a hash nothing has written is a live 404.
68
+ * @returns Nothing. Publishing the same route and hash twice is a no-op in the store and
69
+ * invalidates the page again, so a retried publish is safe.
70
+ * @throws Whatever `store.publish` raises, before anything is invalidated — a store rejecting an
71
+ * unwritten hash or an unaddressable route leaves the page that is live untouched.
72
+ * @example
73
+ * ```ts
74
+ * export async function POST(request: Request) {
75
+ * const { route, hash } = (await request.json()) as { route: string; hash: string };
76
+ * await publishRoute(store, route, hash);
77
+ * return Response.json({ ok: true, route, hash });
78
+ * }
79
+ * ```
41
80
  */
42
81
  declare function publishRoute(store: ArtifactStore, route: string, hash: string): Promise<void>;
43
82
 
44
83
  /**
45
- * The whole production read path: one pointer read, one artifact read. Null means the caller
46
- * renders a real 404 — an unpublished route has no pointer, which is what makes unpublish a
47
- * server 404 rather than an empty page.
84
+ * Resolves whatever is published at a catch-all route: one pointer read, then one artifact read.
85
+ * The page component and `generateMetadata` both want the same answer, so wrap the call in
86
+ * React's `cache` rather than reading the store twice per request.
87
+ *
88
+ * @param store - The store the route was published through. Only `pointer` and `read` are
89
+ * called, so a read-only implementation serves the whole render path.
90
+ * @param slug - The catch-all param for the request, passed through `routeFromSlug`. `undefined`
91
+ * and the empty array are the root route.
92
+ * @returns The artifact serving that route, or `null` — for a route with no pointer, and for a
93
+ * pointer naming a hash the store no longer holds. Absence is a value rather than a failure:
94
+ * it is what the caller turns into `notFound()`, and it is why unpublishing produces a server
95
+ * 404 instead of an empty page.
96
+ * @throws Whatever the store raises. Both calls answer absence with `null`, so anything thrown
97
+ * from here is storage failing rather than a route that is not published.
98
+ * @example
99
+ * ```tsx
100
+ * export default async function Page({ params }: { params: Promise<{ slug?: string[] }> }) {
101
+ * const artifact = await resolveArtifact(store, (await params).slug);
102
+ * if (!artifact) notFound();
103
+ * return <Renderer artifact={artifact} registry={blockRegistry} resolveHole={resolveHole} />;
104
+ * }
105
+ * ```
48
106
  */
49
107
  declare function resolveArtifact(store: ArtifactStore, slug: readonly string[] | undefined): Promise<Artifact | null>;
50
108
 
51
- /** Catch-all params to the route string artifacts and pointers are keyed by. */
109
+ /**
110
+ * Turns a Next catch-all param into the route string a store keys pointers and artifacts by.
111
+ *
112
+ * @param slug - The `slug` param of a `[...slug]` or `[[...slug]]` segment, as Next resolves it.
113
+ * `undefined` and the empty array both mean the root, which only the optional form matches.
114
+ * @returns `"/"` for an absent or empty slug, and otherwise the segments joined with `/` under a
115
+ * leading slash. Segments pass through as Next handed them over — nothing is encoded, trimmed
116
+ * or lower-cased, so the string matches the route `compile` stamped on the artifact only when
117
+ * the document was published under the same spelling.
118
+ * @example
119
+ * ```ts
120
+ * routeFromSlug(undefined); // "/"
121
+ * routeFromSlug([]); // "/"
122
+ * routeFromSlug(["promotions", "summer"]); // "/promotions/summer"
123
+ * ```
124
+ */
52
125
  declare function routeFromSlug(slug: readonly string[] | undefined): string;
53
126
 
54
127
  /**
55
- * generateStaticParams source. manifest() is an advisory read for exactly this — no request
56
- * ever goes through it. Non-exact pointers are excluded until #5 settles pattern routing.
128
+ * Every published route that can be prebuilt, shaped as the params a catch-all segment's
129
+ * `generateStaticParams` returns.
130
+ *
131
+ * @param store - The store the routes were published through. Only `manifest` is called, and it
132
+ * is read once per build rather than per request.
133
+ * @returns One `{ slug }` per exact pointer, in whatever order the manifest came back in; the
134
+ * root route is `{ slug: [] }`. A `param` or `prefix` pointer — `/guides/[city]`, `/docs/*` —
135
+ * is left out, because its route string is a pattern rather than a path and prebuilding it
136
+ * would name a page that does not exist.
137
+ * @throws Whatever the store raises reading its manifest. An empty manifest is an empty array,
138
+ * which is a build that prebuilds nothing rather than a failure.
139
+ * @example
140
+ * ```ts
141
+ * export const generateStaticParams = () => staticRouteParams(store);
142
+ * ```
57
143
  */
58
144
  declare function staticRouteParams(store: ArtifactStore): Promise<{
59
145
  slug: string[];
60
146
  }[]>;
61
147
 
62
- /** Pointer removed, then that one route invalidated — the next request renders a real 404. */
148
+ /**
149
+ * Takes a route offline and invalidates that one page — the body of the
150
+ * `POST /api/nubbin/unpublish` handler an application exposes, which is where `nubbin unpublish
151
+ * --origin <url>` sends `{ route }`.
152
+ *
153
+ * Like `publishRoute`, it has to run inside the serving process: `revalidatePath` reaches only
154
+ * that process's cache, so a pointer dropped from a terminal leaves the page still being served.
155
+ *
156
+ * @param store - The store serving the application. `unpublish` is the only method called.
157
+ * @param route - The route to stop serving, exactly as it was published.
158
+ * @returns Nothing. The artifact stays readable at its hash — only the pointer goes — so
159
+ * restoring the route is a `publishRoute` at the same hash.
160
+ * @throws Whatever `store.unpublish` raises, before anything is invalidated. Unpublishing a
161
+ * route with no pointer is a no-op rather than a refusal.
162
+ * @example
163
+ * ```ts
164
+ * export async function POST(request: Request) {
165
+ * const { route } = (await request.json()) as { route: string };
166
+ * await unpublishRoute(store, route);
167
+ * return Response.json({ ok: true, route });
168
+ * }
169
+ * ```
170
+ */
63
171
  declare function unpublishRoute(store: ArtifactStore, route: string): Promise<void>;
64
172
 
65
173
  export { artifactMetadata, holeFetchOptions, publishRoute, resolveArtifact, routeFromSlug, staticRouteParams, unpublishRoute };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubbin/next",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "The Next.js binding for Nubbin: resolve a published artifact for a route, prebuild the ones that exist, and publish or unpublish a single route.",
5
5
  "keywords": [
6
6
  "nubbin",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@nubbin/core": "0.1.1"
35
+ "@nubbin/core": "0.3.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "next": ">=16.0.0"