@decocms/nextjs 7.8.0 → 7.9.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
@@ -131,15 +131,59 @@ relative path:
131
131
  ```ts
132
132
  // src/deco/setup.ts
133
133
  import { createNextSetup } from "@decocms/nextjs/setup";
134
+ import blocks from "deco/blocksManifest.gen"; // .deco/blocksManifest.gen.ts — see below
134
135
  import { sectionImports, sectionMeta, syncComponents, loadingFallbacks } from "deco/sections.gen";
135
136
 
136
137
  export const ensureSetup = createNextSetup({
138
+ blocks,
139
+ blocksDir: false, // the manifest replaces the runtime fs read
137
140
  sections: sectionImports,
138
141
  conventions: { meta: sectionMeta, syncComponents, loadingFallbacks },
139
142
  meta: () => import("deco/meta.gen.json").then((m) => m.default),
140
143
  });
141
144
  ```
142
145
 
146
+ ### Recommended block source: the static-import manifest
147
+
148
+ `generate-blocks-manifest` (from `@decocms/blocks-cli`) emits
149
+ `.deco/blocksManifest.gen.ts` — a module that **statically imports** every
150
+ `.deco/blocks/*.json` file (raw on-disk filename in the specifier; keys are
151
+ the filename minus `.json`, verbatim, exactly like
152
+ `loadDecofileDirectory`). Passing its default export as `blocks` with
153
+ `blocksDir: false` (as in the snippet above) makes the manifest the sole
154
+ block source and the bootstrap pure — no filesystem access.
155
+
156
+ Why this is the recommended wiring over the default `blocksDir` runtime
157
+ read:
158
+
159
+ - **CMS content hot-reloads in `next dev`.** The block JSONs are part of
160
+ Next's module graph, so editing one (Studio daemon write, sync, manual
161
+ edit) invalidates the server module graph, re-evaluates the setup module,
162
+ and rebuilds `createNextSetup`'s memo with the fresh content —
163
+ ~120–165ms per edit, measured on a 500-block site. The runtime fs read is
164
+ invisible to the bundler: edits invalidate nothing and dev serves stale
165
+ content until a restart.
166
+ - **Deploys need no `outputFileTracingIncludes` hack** — the JSON is
167
+ bundled into the build output instead of read from disk at runtime.
168
+
169
+ Trade-offs to know about:
170
+
171
+ - **Content is baked at build time** in production. Studio's
172
+ `POST /.decofile` still overrides the in-memory snapshot at runtime
173
+ (live preview/publish keeps working on a warm instance); the baked
174
+ manifest is the cold-start baseline.
175
+ - **Adding or removing a block *file* requires re-running the generator**
176
+ (content edits to existing files do not). Wire it into the site's
177
+ `generate` chain — see the scripts section below.
178
+
179
+ If you skip the manifest, the default `blocksDir: ".deco/blocks"` runtime
180
+ read still works — just without dev reload, and your deploy must ship the
181
+ directory alongside the server bundle.
182
+
183
+ Keep the manifest out of any client-reachable import graph (import it only
184
+ from the server-side setup module) so 5MB of CMS JSON never lands in a
185
+ client bundle.
186
+
143
187
  `createNextSetup` returns a **memoized** `ensureSetup()` function — a
144
188
  successful bootstrap is cached for the life of the warm serverless
145
189
  instance; a *rejected* bootstrap clears the memo so the next call retries
@@ -172,8 +216,8 @@ Two call sites need `ensureSetup()`:
172
216
 
173
217
  | Option | Purpose |
174
218
  | --- | --- |
175
- | `blocksDir` | Directory of decofile JSON snapshots (`.deco/blocks` by default). Pass `false` to skip filesystem loading entirely (e.g. when all content comes from `blocks`). |
176
- | `blocks` | Extra/override blocks, merged **over** the directory's blocks. |
219
+ | `blocksDir` | Directory of decofile JSON snapshots (`.deco/blocks` by default), read with a plain fs scan at bootstrap. Pass `false` to skip filesystem loading entirely the recommended setting when `blocks` carries the static-import manifest (see above). |
220
+ | `blocks` | Extra/override blocks, merged **over** the directory's blocks. Pass the manifest's default export here (with `blocksDir: false`) to make it the sole block source. |
177
221
  | `sections` | The lazy section registry — `sectionImports` from `generate-sections --registry` (see below). |
178
222
  | `conventions` | `{ meta, syncComponents, loadingFallbacks }` from `sections.gen.ts` — wires the `export const sync/layout/seo/cache/eager/clientOnly` conventions (see below). |
179
223
  | `meta` | Lazy admin meta schema loader: `() => import("deco/meta.gen.json").then(m => m.default)`. Wire this even with a trivial schema — without it, `/deco/meta` (and its `/live/_meta` alias) 503s with `"Schema not initialized"`. |
@@ -184,7 +228,7 @@ Two call sites need `ensureSetup()`:
184
228
 
185
229
  ## 4. `package.json` scripts — non-colliding names
186
230
 
187
- Add two codegen scripts. **Do not name either of these `generate:schema`** —
231
+ Add three codegen scripts. **Do not name any of these `generate:schema`** —
188
232
  FastStore sites already own that script name for their own commerce-schema
189
233
  codegen, and a collision silently shadows one of the two generators
190
234
  depending on script-merge order. Use these names instead:
@@ -193,7 +237,8 @@ depending on script-merge order. Use these names instead:
193
237
  {
194
238
  "scripts": {
195
239
  "generate:deco-meta": "tsx node_modules/@decocms/blocks-cli/scripts/generate-schema.ts",
196
- "generate:deco-sections": "tsx node_modules/@decocms/blocks-cli/scripts/generate-sections.ts --registry"
240
+ "generate:deco-sections": "tsx node_modules/@decocms/blocks-cli/scripts/generate-sections.ts --registry",
241
+ "generate:deco-blocks": "tsx node_modules/@decocms/blocks-cli/scripts/generate-blocks-manifest.ts"
197
242
  }
198
243
  }
199
244
  ```
@@ -215,9 +260,17 @@ site has a reason to put the artifact somewhere else.
215
260
  `sectionMeta`/`syncComponents`/`loadingFallbacks`, not the lazy loader map
216
261
  `setup.ts` needs for its `sections` option. Defaults to
217
262
  `.deco/sections.gen.ts`.
218
-
219
- Run both any time `src/sections/` changes (wire into a `predev`/`prebuild`
220
- step, or a watch script, as your site prefers).
263
+ - `generate:deco-blocks` runs `generate-blocks-manifest.ts`, which emits the
264
+ static-import blocks manifest (see the recommended-block-source section
265
+ above) to `.deco/blocksManifest.gen.ts` by default. Regeneration is
266
+ idempotent — an unchanged block set rewrites nothing, so no-op runs never
267
+ tickle Next's file watcher.
268
+
269
+ Run the first two any time `src/sections/` changes, and
270
+ `generate:deco-blocks` any time a block file is **added or removed** in
271
+ `.deco/blocks/` (content edits to existing files don't need it — the static
272
+ imports pick those up by themselves). Simplest is to wire all three into a
273
+ `predev`/`prebuild` `generate` chain.
221
274
 
222
275
  ## 5. `src/sections/` — the entry-file convention
223
276
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/nextjs",
3
- "version": "7.8.0",
3
+ "version": "7.9.0",
4
4
  "type": "module",
5
5
  "description": "Deco framework binding for Next.js App Router",
6
6
  "repository": {
@@ -25,8 +25,8 @@
25
25
  "lint:unused": "knip"
26
26
  },
27
27
  "dependencies": {
28
- "@decocms/blocks": "7.8.0",
29
- "@decocms/blocks-admin": "7.8.0"
28
+ "@decocms/blocks": "7.9.0",
29
+ "@decocms/blocks-admin": "7.9.0"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "next": ">=15.0.0",
package/src/setup.test.ts CHANGED
@@ -10,10 +10,11 @@
10
10
  import * as fs from "node:fs";
11
11
  import * as os from "node:os";
12
12
  import * as path from "node:path";
13
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
14
13
  import * as cms from "@decocms/blocks/cms";
15
14
  import { listRegisteredSections, loadBlocks, setBlocks } from "@decocms/blocks/cms";
15
+ import * as loadDecofileDirectoryModule from "@decocms/blocks/cms/loadDecofileDirectory";
16
16
  import { getProductionOrigins } from "@decocms/blocks/sdk/normalizeUrls";
17
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
17
18
 
18
19
  // Mirrors routeHandlers.test.ts in this same package: @decocms/blocks-admin
19
20
  // is mocked (rather than exercised for real) because it's a heavier graph
@@ -127,11 +128,27 @@ describe("createNextSetup", () => {
127
128
  });
128
129
  });
129
130
 
131
+ it("manifest wiring (blocks + blocksDir: false) is pure — no filesystem read", async () => {
132
+ // The documented static-import-manifest path: the site passes the
133
+ // manifest's default export as `blocks` and disables the directory
134
+ // scan. Bootstrap must never touch the fs — same namespace-spy
135
+ // technique as the setResolveErrorHandler spies below.
136
+ const loadDirSpy = vi.spyOn(loadDecofileDirectoryModule, "loadDecofileDirectory");
137
+
138
+ const ensureSetup = createNextSetup({
139
+ blocksDir: false,
140
+ blocks: { "pages-Home%20(principal)-287364": { path: "/" } },
141
+ sections: {},
142
+ });
143
+ await ensureSetup();
144
+
145
+ expect(loadDirSpy).not.toHaveBeenCalled();
146
+ expect(loadBlocks()["pages-Home%20(principal)-287364"]).toEqual({ path: "/" });
147
+ loadDirSpy.mockRestore();
148
+ });
149
+
130
150
  it("options.blocks wins over blocksDir on an overlapping key (merge precedence)", async () => {
131
- fs.writeFileSync(
132
- path.join(tmpDir, "shared.json"),
133
- JSON.stringify({ source: "dir" }),
134
- );
151
+ fs.writeFileSync(path.join(tmpDir, "shared.json"), JSON.stringify({ source: "dir" }));
135
152
 
136
153
  const ensureSetup = createNextSetup({
137
154
  blocksDir: tmpDir,
@@ -158,9 +175,7 @@ describe("createNextSetup", () => {
158
175
  });
159
176
  await ensureSetup();
160
177
 
161
- expect(adminMocks.setMetaData).toHaveBeenCalledWith(
162
- await meta.mock.results[0]!.value,
163
- );
178
+ expect(adminMocks.setMetaData).toHaveBeenCalledWith(await meta.mock.results[0]!.value);
164
179
  expect(adminMocks.setRenderShell).toHaveBeenCalledWith(renderShell);
165
180
  expect(adminMocks.setPreviewWrapper).toHaveBeenCalledWith(PreviewWrapper);
166
181
  });
package/src/setup.ts CHANGED
@@ -12,20 +12,30 @@
12
12
  * an admin request actually arrives... and because @decocms/blocks-admin
13
13
  * is a heavier graph than the CMS core.
14
14
  *
15
- * @example site's `src/deco/setup.ts`
15
+ * @example site's `src/deco/setup.ts` — recommended wiring: the static-import
16
+ * blocks manifest (`generate-blocks-manifest`) as the block source, so the
17
+ * bundler's module graph owns CMS content (dev hot-reload on block edits, no
18
+ * `outputFileTracingIncludes` for `.deco/blocks` in deploys).
16
19
  * ```ts
17
20
  * import { createNextSetup } from "@decocms/nextjs/setup";
18
21
  * // Generators default to `.deco/`; `deco/*` is a tsconfig path alias for
19
22
  * // `.deco/*` (see the package README) since `src/deco/setup.ts` isn't
20
23
  * // adjacent to the site-root `.deco/` directory.
24
+ * import blocks from "deco/blocksManifest.gen";
21
25
  * import { sectionImports, sectionMeta, syncComponents } from "deco/sections.gen";
22
26
  *
23
27
  * export const ensureSetup = createNextSetup({
28
+ * blocks,
29
+ * blocksDir: false, // manifest IS the directory — skip the runtime fs read
24
30
  * sections: sectionImports,
25
31
  * conventions: { meta: sectionMeta, syncComponents },
26
32
  * meta: () => import("deco/meta.gen.json").then((m) => m.default),
27
33
  * });
28
34
  * ```
35
+ * With `blocksDir: false` + `blocks`, this bootstrap is pure — no filesystem
36
+ * access. Sites that skip the manifest can instead rely on the default
37
+ * `blocksDir: ".deco/blocks"` runtime read (no dev reload on content edits;
38
+ * deploys must ship the directory alongside the server bundle).
29
39
  */
30
40
  import type { ApplySectionConventionsInput } from "@decocms/blocks/cms";
31
41
  import { applySectionConventions, loadBlocks } from "@decocms/blocks/cms";
@@ -34,13 +44,21 @@ import { createSiteSetup, type SiteSetupOptions } from "@decocms/blocks/setup";
34
44
 
35
45
  export interface NextSetupOptions {
36
46
  /**
37
- * Directory of decofile JSON snapshots, relative to the site root.
38
- * Pass `false` to skip filesystem loading (blocks come from `blocks`).
47
+ * Directory of decofile JSON snapshots, relative to the site root, read
48
+ * at bootstrap time with a plain fs scan. Pass `false` to skip filesystem
49
+ * loading entirely (blocks come from `blocks`) — with `blocks` set this
50
+ * makes the bootstrap pure, which is the recommended manifest wiring (see
51
+ * the module-level example).
39
52
  * @default ".deco/blocks"
40
53
  */
41
54
  blocksDir?: string | false;
42
55
 
43
- /** Extra/override blocks, merged OVER the directory's blocks. */
56
+ /**
57
+ * Extra/override blocks, merged OVER the directory's blocks. Pass the
58
+ * default export of a generated static-import manifest
59
+ * (`generate-blocks-manifest` → `.deco/blocksManifest.gen.ts`) together
60
+ * with `blocksDir: false` to make the manifest the sole block source.
61
+ */
44
62
  blocks?: Record<string, unknown>;
45
63
 
46
64
  /**
@@ -80,6 +98,17 @@ export interface NextSetupOptions {
80
98
  * *rejected* bootstrap is NOT cached — the memo is cleared on failure so
81
99
  * the next call retries from scratch, while the triggering call still
82
100
  * rejects with the original error.
101
+ *
102
+ * "Lifetime of the module" is exactly what makes the static-import manifest
103
+ * hot-reload in `next dev`: the site's setup module imports
104
+ * `blocksManifest.gen.ts`, which statically imports every block JSON, so
105
+ * editing one invalidates the server module graph up through the setup
106
+ * module. Next re-evaluates it, `createNextSetup` runs again with the fresh
107
+ * JSON, and this memo is rebuilt from scratch — that module-graph reset IS
108
+ * the designed dev reload mechanism for CMS content (measured at ~120–165ms
109
+ * per edit on a 500-block site). With the default `blocksDir` fs read
110
+ * instead, nothing imports the JSON, so edits invalidate nothing and dev
111
+ * serves stale content until a full restart.
83
112
  */
84
113
  export function createNextSetup(options: NextSetupOptions): () => Promise<void> {
85
114
  let setupPromise: Promise<void> | null = null;