@decocms/nextjs 7.8.0 → 7.10.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 +60 -7
- package/package.json +3 -3
- package/src/setup.test.ts +23 -8
- package/src/setup.ts +33 -4
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
|
|
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
|
|
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
|
-
|
|
220
|
-
|
|
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.
|
|
3
|
+
"version": "7.10.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.
|
|
29
|
-
"@decocms/blocks-admin": "7.
|
|
28
|
+
"@decocms/blocks": "7.10.0",
|
|
29
|
+
"@decocms/blocks-admin": "7.10.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
|
|
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
|
-
/**
|
|
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;
|