create-nextly-app 0.0.2-alpha.72 → 0.0.2-alpha.74

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/dist/index.cjs CHANGED
@@ -1,3 +1,3 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_create_nextly = require("./create-nextly-BGu1tXYs.cjs");
2
+ const require_create_nextly = require("./create-nextly-Decg8dye.cjs");
3
3
  exports.createNextly = require_create_nextly.createNextly;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- export { t as createNextly } from "./create-nextly-CyI8XgVc.mjs";
1
+ export { t as createNextly } from "./create-nextly-ChHdzG83.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextly-app",
3
- "version": "0.0.2-alpha.72",
3
+ "version": "0.0.2-alpha.74",
4
4
  "description": "CLI to scaffold Nextly in your Next.js project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,10 +37,10 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@babel/types": "^7.29.7",
40
- "@nextlyhq/eslint-plugin": "0.0.2-alpha.72",
41
- "@nextlyhq/module-specifiers": "0.0.2-alpha.72",
42
- "@nextlyhq/telemetry": "0.0.2-alpha.72",
43
- "@nextlyhq/tsconfig": "0.0.2-alpha.72",
40
+ "@nextlyhq/eslint-plugin": "0.0.2-alpha.74",
41
+ "@nextlyhq/module-specifiers": "0.0.2-alpha.74",
42
+ "@nextlyhq/telemetry": "0.0.2-alpha.74",
43
+ "@nextlyhq/tsconfig": "0.0.2-alpha.74",
44
44
  "@testing-library/react": "^16.3.0",
45
45
  "@types/fs-extra": "^11.0.4",
46
46
  "@types/node": "^22.20.5",
@@ -7,7 +7,7 @@ description: >-
7
7
  code, and extra fields on users. Also use when asked to add login or
8
8
  sign-up to a Nextly site, so the answer matches what Nextly provides.
9
9
  metadata:
10
- nextly-version: 0.0.2-alpha.72
10
+ nextly-version: 0.0.2-alpha.74
11
11
  ---
12
12
 
13
13
  # Authentication and access in Nextly
@@ -9,7 +9,7 @@ description: >-
9
9
  not appear in the admin, a field change does not reach the database, or a
10
10
  page reading content fails to build.
11
11
  metadata:
12
- nextly-version: 0.0.2-alpha.72
12
+ nextly-version: 0.0.2-alpha.74
13
13
  ---
14
14
 
15
15
  # Define a collection in Nextly
@@ -176,9 +176,15 @@ export default async function PostsPage() {
176
176
  - `status: "published"` keeps drafts off the page. Without it a trusted
177
177
  server read returns drafts too.
178
178
  - `cachedFind` with `nextlyTags("posts")` caches the result and clears it when
179
- a post is saved, so the page updates on publish. It also lets the page build
180
- in an app with `cacheComponents: true`, where an uncached database read
181
- outside `<Suspense>` fails `next build`.
179
+ a post is saved, so the page updates on publish. It also lets the page
180
+ build: a Nextly app runs with `cacheComponents: true`, where an uncached
181
+ database read outside `<Suspense>` fails `next build`. There the read goes
182
+ through the app's own `"use cache"` function, `src/nextly/cached-read.ts`
183
+ (`nextly/cached-read.ts` without a `src/` folder): import it in the module
184
+ that reads, `import "@/nextly/cached-read";` with `src/`, or by a relative
185
+ path without one. A blog ships it; for another app, copy it from the caching
186
+ guide
187
+ (<https://nextlyhq.com/docs/guides/isr-caching#cache-components>).
182
188
  - `keyParts` names the cache entry, and everyone whose read uses the same key
183
189
  gets the same rows. Cache only reads that are the same for every reader:
184
190
  published content of a collection with no read rule, on the collection or
@@ -8,7 +8,7 @@ description: >-
8
8
  Also use when a deployed Nextly app has missing tables, refuses to start,
9
9
  or loses uploaded files.
10
10
  metadata:
11
- nextly-version: 0.0.2-alpha.72
11
+ nextly-version: 0.0.2-alpha.74
12
12
  ---
13
13
 
14
14
  # Deploy a Nextly project
@@ -244,5 +244,7 @@ could then pick their own address.
244
244
  - **Sign-in fails with `CSRF_FAILED` behind a proxy:** add the public origin
245
245
  to `NEXTLY_ALLOWED_ORIGINS`.
246
246
  - **`next build` fails on a page reading content:** in an app with
247
- `cacheComponents: true`, wrap the read in `cachedFind` (the
248
- `nextly-collections` skill) or render it inside `<Suspense>`.
247
+ `cacheComponents: true`, the Nextly default, wrap the read in `cachedFind`
248
+ (the `nextly-collections` skill) or render it inside `<Suspense>`. An app
249
+ made by an earlier release may have it off: check `next.config` first, and
250
+ turn it on as the installation guide describes.
@@ -8,7 +8,7 @@ description: >-
8
8
  Nextly install shows no collections, fails to load nextly.config.ts, or its
9
9
  admin panel misbehaves after setup.
10
10
  metadata:
11
- nextly-version: 0.0.2-alpha.72
11
+ nextly-version: 0.0.2-alpha.74
12
12
  ---
13
13
 
14
14
  # Install Nextly into an existing Next.js app
@@ -23,9 +23,10 @@ Checked against `nextly` 0.0.2-alpha.67 with Next.js 16.4. Run
23
23
 
24
24
  ## Before you start
25
25
 
26
- - Next.js 16.4.1 or later, or 15.5.28 or later on the 15 line, with the App
27
- Router (`app/` or `src/app/`). Earlier versions carry the October 2026
28
- security advisories these releases fix.
26
+ - Next.js `^16.4.1` and React `^19.3.0` (16.4.1 or later within 16.x, 19.3.0
27
+ or later within 19.x), with the App Router (`app/` or `src/app/`). The
28
+ [supported versions](https://nextlyhq.com/docs/getting-started/installation#requirements)
29
+ are kept current there.
29
30
  - Node.js `^22.12.0 || >=24.0.0`. Node 20 and Node 23 are not supported.
30
31
  - A clean git working tree, so every change below is visible in one diff.
31
32
 
@@ -1,6 +1,10 @@
1
+ import { connection } from "next/server";
2
+ import { Suspense } from "react";
1
3
  import { getBrandingCss } from "nextly/config";
4
+ import { checkAdminComponents } from "nextly/runtime";
2
5
 
3
6
  import config from "../../../../nextly.config";
7
+ import { registeredComponentPaths } from "../../../types/generated/plugin-admin-components.generated";
4
8
 
5
9
  const brandingCss = getBrandingCss(config.admin?.branding);
6
10
 
@@ -14,7 +18,25 @@ export default function AdminLayout({
14
18
  {brandingCss && (
15
19
  <style dangerouslySetInnerHTML={{ __html: brandingCss }} />
16
20
  )}
17
- {children}
21
+ <Suspense fallback={null}>
22
+ <ForThisRequest>{children}</ForThisRequest>
23
+ </Suspense>
18
24
  </>
19
25
  );
20
26
  }
27
+
28
+ /**
29
+ * The admin renders for each request, never from a cache another user's
30
+ * request filled. Beneath the layout's Suspense boundary, so under Cache
31
+ * Components the shell above it still prerenders: request work outside a
32
+ * boundary fails the build, in development too, where `checkAdminComponents`
33
+ * boots nextly.
34
+ */
35
+ async function ForThisRequest({ children }: { children: React.ReactNode }) {
36
+ await connection();
37
+ // In development, the generated component map is written again when it
38
+ // falls behind the config, so a plugin added while `next dev` runs draws
39
+ // its components without a command. In production it does nothing.
40
+ await checkAdminComponents({ config, registeredComponentPaths });
41
+ return children;
42
+ }
@@ -47,8 +47,19 @@ body {
47
47
 
48
48
  /* Visible focus ring for keyboard users. `:focus-visible` only fires
49
49
  on keyboard / programmatic focus, so clicking a button doesn't
50
- show the ring but tabbing to it does. */
51
- *:focus-visible {
50
+ show the ring but tabbing to it does.
51
+
52
+ Weighed as two element names, the pseudo-class inside `:where()` counting
53
+ for nothing. That is enough to keep the ring over a reset added later, such
54
+ as `button { outline: none }`, and on this site's pages it draws as
55
+ `*:focus-visible` did, since no other rule here sets these properties on a
56
+ focused element. The admin at /admin renders in this same document and
57
+ reverts a site's rules made of element names only; with the pseudo-class
58
+ counted, this rule would outweigh that and redraw every focused control in
59
+ the admin with this ring and corner. The trade: a class-based reset, such as
60
+ `.btn { outline: none }`, outweighs this ring wherever it is written; give
61
+ such a rule a `:focus-visible` style of its own. */
62
+ html body :where(:focus-visible) {
52
63
  outline: 2px solid currentColor;
53
64
  outline-offset: 2px;
54
65
  border-radius: 2px;
@@ -3,6 +3,11 @@ import "@fontsource-variable/geist";
3
3
  import "@fontsource-variable/geist-mono";
4
4
  import "./globals.css";
5
5
  import { SITE_URL } from "@/lib/site-url";
6
+ // Registers the app's "use cache" read function, and how Nextly starts when a
7
+ // read finds it not started, for every route before any of them reads,
8
+ // during `next build` too: under Cache Components Nextly's cached reads go
9
+ // through that function, and `<SiteCode>` refuses to read without it.
10
+ import "@/nextly/cached-read";
6
11
 
7
12
  /**
8
13
  * The faces are imported as STYLESHEETS from packages in `node_modules`, rather
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Nextly's cached reads under Cache Components.
3
+ *
4
+ * Next.js caches non-fetch reads with `"use cache"` and `cacheTag` once Cache
5
+ * Components is on, and the directive works only in code Next.js compiles, so
6
+ * Nextly reads through this function of the app's own. It receives the read's
7
+ * key, its tags and its lifetime, and runs the reader Nextly registered for
8
+ * that key. The root layout imports this file, so it is registered on every
9
+ * route before anything is read: `<SiteCode>` refuses to read without it.
10
+ *
11
+ * The same import also tells Nextly how to start with the app's config when a
12
+ * read finds it not started, so the root layout's import both registers the
13
+ * cached read and covers a read made before any page starts Nextly, during
14
+ * `next build` too.
15
+ *
16
+ * It works with `cacheComponents: true` and Nextly's cache handlers in
17
+ * `next.config.ts`, as this app ships:
18
+ * https://nextlyhq.com/docs/guides/isr-caching#cache-components
19
+ */
20
+ import { cacheLife, cacheTag } from "next/cache";
21
+ import { getNextly, registerNextlyStart } from "nextly";
22
+ import { registerCachedRead, runRegisteredRead } from "nextly/runtime";
23
+
24
+ import config from "@nextly-config";
25
+
26
+ export async function cachedRead(
27
+ key: string,
28
+ tags: string[],
29
+ revalidate: number | false
30
+ ): Promise<unknown> {
31
+ "use cache";
32
+ cacheTag(...tags);
33
+ // `revalidate: false` is tag-only, as everywhere in Nextly: only a write
34
+ // expires the entry, never time. Next.js's "max" profile revalidates monthly.
35
+ if (revalidate === false) {
36
+ cacheLife({ revalidate: Infinity, expire: Infinity });
37
+ } else {
38
+ // Expired at twice the revalidate, and never within 5 minutes: Next.js
39
+ // leaves an entry that expires sooner out of a static prerender.
40
+ cacheLife({ revalidate, expire: Math.max(revalidate * 2, 300) });
41
+ }
42
+ return runRegisteredRead(key);
43
+ }
44
+
45
+ registerCachedRead(cachedRead, process.env.__NEXT_CACHE_COMPONENTS);
46
+
47
+ // Tells Nextly how this app starts it, for a read that finds it not started,
48
+ // such as the root layout's `<SiteCode>` on `/_not-found` while `next build`
49
+ // prerenders, where nothing else starts it. Nothing starts here: a page that
50
+ // calls `getNextly` first is the start that counts. An app that passes
51
+ // `getNextly` more than its config passes the same options here.
52
+ registerNextlyStart(() => getNextly({ config }));
@@ -1,4 +1,5 @@
1
1
  import Link from "next/link";
2
+ import { Suspense } from "react";
2
3
 
3
4
  import { ThemeToggle } from "@/components/ThemeToggle";
4
5
  import { SITE_URL } from "@/lib/site-url";
@@ -12,6 +13,10 @@ import { SITE_URL } from "@/lib/site-url";
12
13
  * primary button label flips between "Set up admin" (no super-admin
13
14
  * yet) and "Open admin" (super-admin exists).
14
15
  *
16
+ * The app runs with Cache Components on, so the page is prerendered as a
17
+ * static shell and the setup-status read, made per request, sits inside a
18
+ * `<Suspense>` boundary: the shell shows "Open admin" until it resolves.
19
+ *
15
20
  * If the auth API fails for any reason (DB not migrated, network),
16
21
  * the button defaults to "Open admin" / /admin since /admin already
17
22
  * redirects to /admin/setup when no super-admin exists — the link
@@ -37,12 +42,42 @@ async function fetchSetupStatus(): Promise<SetupStatus | null> {
37
42
  }
38
43
  }
39
44
 
40
- export default async function HomePage() {
45
+ /** The primary button, linking to the admin with the label and target it needs. */
46
+ function AdminButton({ label, href }: { label: string; href: string }) {
47
+ return (
48
+ <Link href={href} className="btn-primary">
49
+ <span>{label}</span>
50
+ <svg
51
+ xmlns="http://www.w3.org/2000/svg"
52
+ width="18"
53
+ height="18"
54
+ viewBox="0 0 24 24"
55
+ fill="none"
56
+ stroke="currentColor"
57
+ strokeWidth="2"
58
+ strokeLinecap="round"
59
+ strokeLinejoin="round"
60
+ className="lucide lucide-arrow-right"
61
+ >
62
+ <path d="M5 12h14" />
63
+ <path d="m12 5 7 7-7 7" />
64
+ </svg>
65
+ </Link>
66
+ );
67
+ }
68
+
69
+ /** The button once setup-status answers: "Set up admin" while there is no super-admin. */
70
+ async function SetupAwareAdminButton() {
41
71
  const status = await fetchSetupStatus();
42
72
  const needsSetup = status?.requiresInitialUser ?? false;
43
- const adminLabel = needsSetup ? "Set up admin" : "Open admin";
44
- const adminHref = needsSetup ? "/admin/setup" : "/admin";
73
+ return needsSetup ? (
74
+ <AdminButton label="Set up admin" href="/admin/setup" />
75
+ ) : (
76
+ <AdminButton label="Open admin" href="/admin" />
77
+ );
78
+ }
45
79
 
80
+ export default function HomePage() {
46
81
  return (
47
82
  <main className="relative min-h-screen flex flex-col items-center justify-center p-6 bg-background text-foreground font-display selection:bg-foreground/10">
48
83
  <ThemeToggle />
@@ -85,24 +120,11 @@ export default async function HomePage() {
85
120
  </p>
86
121
 
87
122
  <div className="mt-10 flex flex-wrap items-center justify-center gap-4">
88
- <Link href={adminHref} className="btn-primary">
89
- <span>{adminLabel}</span>
90
- <svg
91
- xmlns="http://www.w3.org/2000/svg"
92
- width="18"
93
- height="18"
94
- viewBox="0 0 24 24"
95
- fill="none"
96
- stroke="currentColor"
97
- strokeWidth="2"
98
- strokeLinecap="round"
99
- strokeLinejoin="round"
100
- className="lucide lucide-arrow-right"
101
- >
102
- <path d="M5 12h14" />
103
- <path d="m12 5 7 7-7 7" />
104
- </svg>
105
- </Link>
123
+ {/* "/admin" redirects to setup while there is no super-admin, so the
124
+ fallback link works before the read answers. */}
125
+ <Suspense fallback={<AdminButton label="Open admin" href="/admin" />}>
126
+ <SetupAwareAdminButton />
127
+ </Suspense>
106
128
  <Link href="https://nextlyhq.com/docs" className="btn-secondary">
107
129
  <span>Read documentation</span>
108
130
  <svg
@@ -4,6 +4,11 @@ import "@fontsource-variable/jetbrains-mono";
4
4
 
5
5
  import "./globals.css";
6
6
  import { SITE_URL } from "@/lib/site-url";
7
+ // Registers the app's "use cache" read function, and how Nextly starts when a
8
+ // read finds it not started, for every route before any of them reads,
9
+ // during `next build` too: under Cache Components Nextly's cached reads go
10
+ // through that function, and `<SiteCode>` refuses to read without it.
11
+ import "@/nextly/cached-read";
7
12
 
8
13
  /**
9
14
  * The faces are imported as STYLESHEETS from packages in `node_modules`, rather
@@ -24,8 +24,9 @@ export async function register(): Promise<void> {
24
24
  const { createRegister } = await import("nextly");
25
25
  const { default: config } = await import("../nextly.config");
26
26
  // Next.js writes its Cache Components setting into this file as it builds
27
- // it. Nextly, loaded from node_modules, cannot read that for itself, and
28
- // warns when Cache Components is on, which it does not support yet.
27
+ // it. Nextly, loaded from node_modules, cannot read that for itself. With
28
+ // it off, Nextly warns at start; with it on, a cached read that finds no
29
+ // "use cache" read function says so.
29
30
  await createRegister(config, {
30
31
  cacheComponents: process.env.__NEXT_CACHE_COMPONENTS,
31
32
  })();
@@ -6,8 +6,14 @@ import type { NextConfig } from "next";
6
6
  // external (Turbopack can't bundle them).
7
7
  const nextConfig: NextConfig = {
8
8
  typescript: { ignoreBuildErrors: true },
9
- // Nextly does not support Cache Components yet; see the installation guide.
10
- cacheComponents: false,
9
+ // Nextly runs with Cache Components and Partial Prefetching on. Its cache
10
+ // handlers make the first read after a write show the new version:
11
+ // https://nextlyhq.com/docs/getting-started/installation#cache-components
12
+ cacheComponents: true,
13
+ partialPrefetching: true,
14
+ cacheHandler: require.resolve("nextly/cache-handler"),
15
+ cacheMaxMemorySize: 0,
16
+ cacheHandlers: { default: require.resolve("nextly/cache-handlers") },
11
17
  // Transpile the aliased plugin source so HMR fires on src/ edits.
12
18
  transpilePackages: ["{{pluginName}}"],
13
19
  serverExternalPackages: [
@@ -0,0 +1,24 @@
1
+ import { connection } from "next/server";
2
+ import { Suspense } from "react";
3
+
4
+ export default function AdminLayout({
5
+ children,
6
+ }: {
7
+ children: React.ReactNode;
8
+ }) {
9
+ return (
10
+ <Suspense fallback={null}>
11
+ <ForThisRequest>{children}</ForThisRequest>
12
+ </Suspense>
13
+ );
14
+ }
15
+
16
+ /**
17
+ * The admin renders for each request, never from a cache another user's
18
+ * request filled. Beneath the layout's Suspense boundary, so under Cache
19
+ * Components the rest of the page still prerenders.
20
+ */
21
+ async function ForThisRequest({ children }: { children: React.ReactNode }) {
22
+ await connection();
23
+ return children;
24
+ }
@@ -1,5 +1,11 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
+ // Registers the dev app's "use cache" read function, and how Nextly starts
4
+ // when a read finds it not started, for every route before any of them
5
+ // reads, during `next build` too: under Cache Components Nextly's cached
6
+ // reads go through that function, and `<SiteCode>` refuses to read without it.
7
+ import "../nextly/cached-read";
8
+
3
9
  export const metadata = {
4
10
  title: "{{pluginName}} — dev playground",
5
11
  };
@@ -8,8 +8,9 @@ export async function register(): Promise<void> {
8
8
  const { createRegister, getService } = await import("nextly");
9
9
  const { default: config } = await import("../nextly.config");
10
10
  // Next.js writes its Cache Components setting into this file as it builds
11
- // it. Nextly, loaded from node_modules, cannot read that for itself, and
12
- // warns when Cache Components is on, which it does not support yet.
11
+ // it. Nextly, loaded from node_modules, cannot read that for itself. With
12
+ // it off, Nextly warns at start; with it on, a cached read that finds no
13
+ // "use cache" read function says so.
13
14
  await createRegister(config, {
14
15
  cacheComponents: process.env.__NEXT_CACHE_COMPONENTS,
15
16
  })();
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Nextly's cached reads under Cache Components, for the plugin's dev app.
3
+ *
4
+ * Next.js caches non-fetch reads with `"use cache"` and `cacheTag` once Cache
5
+ * Components is on, and the directive works only in code Next.js compiles, so
6
+ * Nextly reads through this function of the app's own. It receives the read's
7
+ * key, its tags and its lifetime, and runs the reader Nextly registered for
8
+ * that key. The root layout imports this file, so it is registered on every
9
+ * route before anything is read, as in an app `create-nextly-app` makes.
10
+ *
11
+ * The same import also tells Nextly how to start with the app's config when a
12
+ * read finds it not started, so the root layout's import both registers the
13
+ * cached read and covers a read made before any page starts Nextly, during
14
+ * `next build` too.
15
+ *
16
+ * It works with `cacheComponents: true` and Nextly's cache handlers in
17
+ * `next.config.ts`, as this dev app ships:
18
+ * https://nextlyhq.com/docs/guides/isr-caching#cache-components
19
+ */
20
+ import { cacheLife, cacheTag } from "next/cache";
21
+ import { getNextly, registerNextlyStart } from "nextly";
22
+ import { registerCachedRead, runRegisteredRead } from "nextly/runtime";
23
+
24
+ import config from "../../nextly.config";
25
+
26
+ export async function cachedRead(
27
+ key: string,
28
+ tags: string[],
29
+ revalidate: number | false
30
+ ): Promise<unknown> {
31
+ "use cache";
32
+ cacheTag(...tags);
33
+ // `revalidate: false` is tag-only, as everywhere in Nextly: only a write
34
+ // expires the entry, never time. Next.js's "max" profile revalidates monthly.
35
+ if (revalidate === false) {
36
+ cacheLife({ revalidate: Infinity, expire: Infinity });
37
+ } else {
38
+ // Expired at twice the revalidate, and never within 5 minutes: Next.js
39
+ // leaves an entry that expires sooner out of a static prerender.
40
+ cacheLife({ revalidate, expire: Math.max(revalidate * 2, 300) });
41
+ }
42
+ return runRegisteredRead(key);
43
+ }
44
+
45
+ registerCachedRead(cachedRead, process.env.__NEXT_CACHE_COMPONENTS);
46
+
47
+ // Tells Nextly how this app starts it, for a read that finds it not started,
48
+ // such as the root layout's `<SiteCode>` on `/_not-found` while `next build`
49
+ // prerenders, where nothing else starts it. Nothing starts here: a page that
50
+ // calls `getNextly` first is the start that counts. An app that passes
51
+ // `getNextly` more than its config passes the same options here.
52
+ registerNextlyStart(() => getNextly({ config }));