smoodly 0.0.49 → 0.0.50
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/admin/ops-impl.js +15 -5
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +1 -0
- package/dist/next/page-renderer.js +24 -14
- package/dist/next/reads.d.ts +5 -0
- package/dist/next/reads.js +17 -6
- package/dist/reads.d.ts +5 -0
- package/dist/reads.js +8 -1
- package/dist/revalidate.d.ts +5 -0
- package/dist/revalidate.js +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/admin/ops-impl.js
CHANGED
|
@@ -5,7 +5,7 @@ import { canonicalTags, distinctTags } from "../tags.js";
|
|
|
5
5
|
import { can, isMemberRole, normalizeRole } from "../permissions.js";
|
|
6
6
|
import { ASSET_COLLECTION } from "../media.js";
|
|
7
7
|
import { adminHref, adminSlugTaken, isAdminSlug } from "../admin-path.js";
|
|
8
|
-
import { affectedTargets, collectionTag, entryTag, pageTag, pagesTreeTag, globalTag } from "../revalidate.js";
|
|
8
|
+
import { addressesTag, affectedTargets, collectionTag, entryTag, pageTag, pagesTreeTag, globalTag } from "../revalidate.js";
|
|
9
9
|
import { ensureFixedNodes, fixedNodeOf, fixedNodeSpecs } from "./fixed-nodes.js";
|
|
10
10
|
import { ensureGlobalItems } from "./global-items.js";
|
|
11
11
|
import { SLUG_GRAMMAR_MESSAGE, validateCustomNode, validateFields, validateNode } from "./validate.js";
|
|
@@ -268,10 +268,18 @@ export function createAdminOps(deps) {
|
|
|
268
268
|
for (const id of unique)
|
|
269
269
|
effects.revalidate(pageTag(id));
|
|
270
270
|
};
|
|
271
|
-
/** The tree listing's unit (spec 2026-09-11 §2): every page op that changes what it shows.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
/** The tree listing's unit (spec 2026-09-11 §2): every page op that changes what it shows. The same ops
|
|
272
|
+
* change which addresses resolve, so the addresses tag fires with it (plan 2026-10-09 next cached routes). */
|
|
273
|
+
const expireTree = () => {
|
|
274
|
+
effects?.revalidate?.(pagesTreeTag());
|
|
275
|
+
effects?.revalidate?.(addressesTag());
|
|
276
|
+
};
|
|
277
|
+
/** A collection's list units: every entry write, since a non-versioned collection's published read is its live
|
|
278
|
+
* row. An entry write can give an address a document or take it away, so the addresses tag fires with it. */
|
|
279
|
+
const expireCollection = (name) => {
|
|
280
|
+
effects?.revalidate?.(collectionTag(name));
|
|
281
|
+
effects?.revalidate?.(addressesTag());
|
|
282
|
+
};
|
|
275
283
|
/** A node and every descendant, node first, in listPages() order. */
|
|
276
284
|
const subtreeIds = async (id) => {
|
|
277
285
|
const all = await pages.listPages();
|
|
@@ -1300,6 +1308,7 @@ export function createAdminOps(deps) {
|
|
|
1300
1308
|
return err("validation", `smoodly: "${path}" is the address of "${owner}".`);
|
|
1301
1309
|
}
|
|
1302
1310
|
await pages.addRedirect({ kind: target.kind, id: target.id, locale, path });
|
|
1311
|
+
effects?.revalidate?.(addressesTag()); // a cached 404 at the old address must become the 308
|
|
1303
1312
|
return ok(null);
|
|
1304
1313
|
}),
|
|
1305
1314
|
remove: (locale, path) => guard(async () => {
|
|
@@ -1307,6 +1316,7 @@ export function createAdminOps(deps) {
|
|
|
1307
1316
|
if (unsupported)
|
|
1308
1317
|
return unsupported;
|
|
1309
1318
|
await pages.removeRedirect(locale, path); // the stored address, as list gave it
|
|
1319
|
+
effects?.revalidate?.(addressesTag()); // a cached 308 must become the 404
|
|
1310
1320
|
return ok(null);
|
|
1311
1321
|
}),
|
|
1312
1322
|
},
|
package/dist/next/index.d.ts
CHANGED
|
@@ -6,4 +6,5 @@ export type { SmoodlyQueries, QueryOptions, EntriesQuery, TypedEntryItem, Global
|
|
|
6
6
|
export { previewRoute } from "./preview-route.ts";
|
|
7
7
|
export { previewPill } from "./preview-pill.tsx";
|
|
8
8
|
export { createAdminRoute } from "./admin-route.ts";
|
|
9
|
+
export { buildGate } from "./reads.ts";
|
|
9
10
|
export type { AdminRouteDeps } from "./admin-route.ts";
|
package/dist/next/index.js
CHANGED
|
@@ -8,19 +8,18 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
8
8
|
import { Suspense } from "react";
|
|
9
9
|
import { draftMode } from "next/headers";
|
|
10
10
|
import { notFound, permanentRedirect, unstable_rethrow } from "next/navigation";
|
|
11
|
-
import { connection } from "next/server";
|
|
12
11
|
import { BRIDGE_CSS } from "../marks.js";
|
|
13
12
|
import { CanvasBridgeSlot, CanvasNetSlot } from "./canvas-slot.js";
|
|
14
13
|
import { DRAFT_META_NAME } from "../admin/editor/canvas-diagnosis.js";
|
|
15
14
|
import { fixedPathFor } from "../localize.js";
|
|
16
15
|
import { decodeSegment, joinPath } from "../paths.js";
|
|
17
16
|
import { renderPage } from "../render.js";
|
|
18
|
-
import { globalUnit, hitUnit } from "../reads.js";
|
|
17
|
+
import { globalUnit, hitUnit, missUnit } from "../reads.js";
|
|
19
18
|
import { toEntryItem } from "../read-shapes.js";
|
|
20
19
|
import { pageContextOf } from "../site.js";
|
|
21
20
|
import { metadataFromSeo } from "./meta.js";
|
|
22
21
|
import { previewPill } from "./preview-pill.js";
|
|
23
|
-
import { nextReads } from "./reads.js";
|
|
22
|
+
import { buildGate, nextReads } from "./reads.js";
|
|
24
23
|
/** Logged once per renderer when Next's cacheComponents is on (spec 2026-09-27 cache components §2). */
|
|
25
24
|
export const CACHE_COMPONENTS_WARNING = "smoodly: Next's cacheComponents is on, and Smoodly does not support it — a missing page answers 200 with noindex instead of 404, and a redirect answers 200 instead of 308. Turn cacheComponents off in next.config.ts; the Next.js guide's \"Cache Components\" section says why (https://smoodly.io/docs/frameworks/nextjs).";
|
|
26
25
|
/** The canvas net (spec 2026-09-27 section guard §5): Server Components hand a section that failed to the
|
|
@@ -96,26 +95,37 @@ export function createPageRenderer(options) {
|
|
|
96
95
|
console.warn(CACHE_COMPONENTS_WARNING);
|
|
97
96
|
}
|
|
98
97
|
async function page(pathFor, opts, accept) {
|
|
99
|
-
warnCacheComponents(); // before
|
|
100
|
-
// A CMS route is
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
await
|
|
98
|
+
warnCacheComponents(); // before buildGate(), so a build's prerender prints it too
|
|
99
|
+
// A CMS route renders on its first visit and is cached until a tag expires it (plan 2026-10-09 next
|
|
100
|
+
// cached routes): nothing here may touch a dynamic API at run time. At build, the gate keeps a route
|
|
101
|
+
// without a dynamic segment off the database (DESIGN.md §3).
|
|
102
|
+
await buildGate();
|
|
104
103
|
const locale = opts?.locale ?? site.locales.default;
|
|
105
104
|
const path = pathFor(locale);
|
|
106
105
|
const { isEnabled: draft } = await draftMode();
|
|
107
|
-
|
|
106
|
+
// searchParams is a dynamic API: read inside draft mode only, where the render is dynamic anyway.
|
|
107
|
+
const params = draft ? await opts?.searchParams : undefined;
|
|
108
108
|
// Both parameters exist only inside draft mode: the public site never sees an editor bridge or an old version.
|
|
109
109
|
const editor = draft && params?.["smoodly-editor"] === "1";
|
|
110
110
|
const versionParam = params?.["smoodly-version"];
|
|
111
111
|
const version = draft && typeof versionParam === "string" && versionParam !== "" ? versionParam : undefined;
|
|
112
112
|
const target = await site.locate(locale, path);
|
|
113
|
-
|
|
113
|
+
// No document to tag: a redirect row, a miss or a code-owned page. Next caches the response like a
|
|
114
|
+
// page, so it hears the addresses tag (reads.ts missUnit, decision 4).
|
|
115
|
+
const tagAddress = () => nextReads.run(missUnit(site, locale, path));
|
|
116
|
+
if (target?.redirect) {
|
|
117
|
+
await tagAddress();
|
|
114
118
|
await redirectTo(target, locale, params);
|
|
119
|
+
}
|
|
120
|
+
if (!target)
|
|
121
|
+
await tagAddress();
|
|
115
122
|
// A code-owned address with no record yet is served from code (spec 2026-09-30 tester findings §2).
|
|
116
123
|
const hit = target ? await load(target, locale, path, version) : site.codePage(locale, path);
|
|
117
|
-
if (!hit || !accept(hit))
|
|
124
|
+
if (!hit || !accept(hit)) {
|
|
125
|
+
if (target)
|
|
126
|
+
await tagAddress();
|
|
118
127
|
notFound();
|
|
128
|
+
}
|
|
119
129
|
// The draft marker tells the page editor's canvas diagnosis that Smoodly
|
|
120
130
|
// rendered this response (spec 2026-09-27 shadowed-page detector §3);
|
|
121
131
|
// React hoists the meta into the head. Never outside draft mode.
|
|
@@ -123,7 +133,7 @@ export function createPageRenderer(options) {
|
|
|
123
133
|
}
|
|
124
134
|
async function metadata(pathFor, opts, accept) {
|
|
125
135
|
// generateMetadata runs alongside the page render, so it gates too.
|
|
126
|
-
await
|
|
136
|
+
await buildGate();
|
|
127
137
|
const locale = opts?.locale ?? site.locales.default;
|
|
128
138
|
const path = pathFor(locale);
|
|
129
139
|
const target = await site.locate(locale, path);
|
|
@@ -145,13 +155,13 @@ export function createPageRenderer(options) {
|
|
|
145
155
|
const any = () => true;
|
|
146
156
|
/** The escape hatch may only render ITS OWN registration's record. */
|
|
147
157
|
const only = (Page) => (hit) => hit.kind === "page" && hit.template === Page.schema.name;
|
|
148
|
-
/** Resolved AFTER
|
|
158
|
+
/** Resolved AFTER buildGate(), inside page()/metadata(), so a wiring
|
|
149
159
|
* error surfaces as a rejected promise like every other failure. */
|
|
150
160
|
const fixedPath = (Page) => (locale) => fixedPathFor(Page.schema, locale, site.locales, site.homePageSlug);
|
|
151
161
|
const literal = (segments) => () => joinPath(segments.map(decodeSegment));
|
|
152
162
|
/** A global item for a layout, by schema: the unit `q.getGlobal` runs for the same item and locale. */
|
|
153
163
|
async function getSmoodlyGlobal(item, opts) {
|
|
154
|
-
await
|
|
164
|
+
await buildGate();
|
|
155
165
|
return nextReads.run(globalUnit(site, item, opts?.locale ?? site.locales.default));
|
|
156
166
|
}
|
|
157
167
|
return {
|
package/dist/next/reads.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { ReadAdapter } from "../reads.ts";
|
|
2
|
+
/** Waits for a request only while `next build` prerenders (`NEXT_PHASE`), so a route file without a
|
|
3
|
+
* dynamic segment, and a layout above it, read nothing at build and need no keys. At run time it does
|
|
4
|
+
* nothing: a cached render that called `connection()` would answer a 500. The app's glue calls it before
|
|
5
|
+
* reading the environment (smoodly/server.ts), the renderer before every read. */
|
|
6
|
+
export declare function buildGate(): Promise<void>;
|
|
2
7
|
export declare const nextReads: ReadAdapter;
|
package/dist/next/reads.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
// The read layer's adapter for Next (spec 2026-10-02 astro cloud fit §6): the one place in the Next glue
|
|
2
|
-
// that knows where a read is cached. The gate is `connection()
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// that knows where a read is cached. The gate is `buildGate`: `connection()` while `next build` prerenders,
|
|
3
|
+
// so a route without a dynamic segment stays off the database at build; nothing at run time, because a
|
|
4
|
+
// render Next caches must not touch a dynamic API (it answers a 500, plan 2026-10-09 next cached routes).
|
|
5
|
+
// Inside draft mode a unit reads the live draft and touches no cache; outside it the published read is
|
|
6
|
+
// one `unstable_cache` unit under the unit's key and tags, which the admin's operations expire
|
|
7
|
+
// (revalidate.ts). Next's cache key is the callback's source text plus the key parts, so every unit is
|
|
8
|
+
// cached from the one closure below (a unit's `tagsOf` is ignored: the tags go in before the read): a
|
|
9
|
+
// second call site would split a shared unit.
|
|
7
10
|
import { unstable_cache } from "next/cache";
|
|
8
11
|
import { draftMode } from "next/headers";
|
|
9
12
|
import { connection } from "next/server";
|
|
13
|
+
/** Waits for a request only while `next build` prerenders (`NEXT_PHASE`), so a route file without a
|
|
14
|
+
* dynamic segment, and a layout above it, read nothing at build and need no keys. At run time it does
|
|
15
|
+
* nothing: a cached render that called `connection()` would answer a 500. The app's glue calls it before
|
|
16
|
+
* reading the environment (smoodly/server.ts), the renderer before every read. */
|
|
17
|
+
export async function buildGate() {
|
|
18
|
+
if (process.env.NEXT_PHASE === "phase-production-build")
|
|
19
|
+
await connection();
|
|
20
|
+
}
|
|
10
21
|
export const nextReads = {
|
|
11
|
-
gate: () =>
|
|
22
|
+
gate: () => buildGate(),
|
|
12
23
|
async run(unit) {
|
|
13
24
|
const { isEnabled: draft } = await draftMode();
|
|
14
25
|
if (draft)
|
package/dist/reads.d.ts
CHANGED
|
@@ -57,6 +57,11 @@ export type SmoodlyQueries<C extends readonly CollectionSchema<any, string>[], S
|
|
|
57
57
|
* record, which fires no tag — can never serve the previous occupant (spec 2026-09-29 pre-install fixes
|
|
58
58
|
* §5). `version` is History's stored version and counts inside draft mode only. */
|
|
59
59
|
export declare function hitUnit(site: SmoodlySite, target: PathTarget, locale: string, path: string, version?: string): ReadUnit<SiteHit | null>;
|
|
60
|
+
/** An address with no document to tag — a miss, a redirect row or a code-owned page. The unit reads
|
|
61
|
+
* nothing: it exists so a cached response hears the `addresses` tag, which every address-changing
|
|
62
|
+
* operation fires (revalidate.ts). Next runs it (its route cache stores a 404 like a page); Astro never
|
|
63
|
+
* caches such a response and does not call it. */
|
|
64
|
+
export declare function missUnit(site: SmoodlySite, locale: string, path: string): ReadUnit<null>;
|
|
60
65
|
/** Astro's route read of an address: one lookup outside draft mode — the tags come with the document, so no
|
|
61
66
|
* locate comes first; in draft mode, locate then load as the editing tables need (spec 2026-10-07 delivery
|
|
62
67
|
* model §3). A redirect row answers as a redirect; a code-owned page answers where no record lives. */
|
package/dist/reads.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { referencedCollections } from "./read-shapes.js";
|
|
2
|
-
import { collectionTag, entryTag, globalTag, pageTag, pagesTreeTag } from "./revalidate.js";
|
|
2
|
+
import { addressesTag, collectionTag, entryTag, globalTag, pageTag, pagesTreeTag } from "./revalidate.js";
|
|
3
3
|
/** JSON with object keys sorted at every level and `undefined` dropped, so two spellings of one query share a unit. */
|
|
4
4
|
function stable(value) {
|
|
5
5
|
if (Array.isArray(value))
|
|
@@ -28,6 +28,13 @@ export function hitUnit(site, target, locale, path, version) {
|
|
|
28
28
|
read: (draft) => draft ? site.load(target, locale, path, { draft: true, ...(version !== undefined ? { version } : {}) }) : site.load(target, locale, path),
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
/** An address with no document to tag — a miss, a redirect row or a code-owned page. The unit reads
|
|
32
|
+
* nothing: it exists so a cached response hears the `addresses` tag, which every address-changing
|
|
33
|
+
* operation fires (revalidate.ts). Next runs it (its route cache stores a 404 like a page); Astro never
|
|
34
|
+
* caches such a response and does not call it. */
|
|
35
|
+
export function missUnit(site, locale, path) {
|
|
36
|
+
return { key: ["smoodly-miss", site.cacheKey, locale, path], tags: [addressesTag()], read: async () => null };
|
|
37
|
+
}
|
|
31
38
|
/** Astro's route read of an address: one lookup outside draft mode — the tags come with the document, so no
|
|
32
39
|
* locate comes first; in draft mode, locate then load as the editing tables need (spec 2026-10-07 delivery
|
|
33
40
|
* model §3). A redirect row answers as a redirect; a code-owned page answers where no record lives. */
|
package/dist/revalidate.d.ts
CHANGED
|
@@ -18,6 +18,11 @@ export declare const pagesTreeTag: () => string;
|
|
|
18
18
|
/** A collection's list units: fired by every entry write in the
|
|
19
19
|
* collection, since a non-versioned collection's published read is its live row. */
|
|
20
20
|
export declare const collectionTag: (name: string) => string;
|
|
21
|
+
/** Every address that may begin or stop resolving: a miss, a redirect and a code-owned page cache
|
|
22
|
+
* under it on Next (reads.ts `missUnit`), since Next caches a 404 like a hit (plan 2026-10-09 next
|
|
23
|
+
* cached routes, decision 4). Fired by every page op that fires the tree tag, every entry write and
|
|
24
|
+
* every redirect change (admin/ops-impl.ts). */
|
|
25
|
+
export declare const addressesTag: () => string;
|
|
21
26
|
export declare function affectedTargets(stores: {
|
|
22
27
|
pages: PageStore;
|
|
23
28
|
entries: EntryStore;
|
package/dist/revalidate.js
CHANGED
|
@@ -17,6 +17,11 @@ export const pagesTreeTag = () => "pages:tree";
|
|
|
17
17
|
/** A collection's list units: fired by every entry write in the
|
|
18
18
|
* collection, since a non-versioned collection's published read is its live row. */
|
|
19
19
|
export const collectionTag = (name) => `entries:${name}`;
|
|
20
|
+
/** Every address that may begin or stop resolving: a miss, a redirect and a code-owned page cache
|
|
21
|
+
* under it on Next (reads.ts `missUnit`), since Next caches a 404 like a hit (plan 2026-10-09 next
|
|
22
|
+
* cached routes, decision 4). Fired by every page op that fires the tree tag, every entry write and
|
|
23
|
+
* every redirect change (admin/ops-impl.ts). */
|
|
24
|
+
export const addressesTag = () => "addresses";
|
|
20
25
|
export async function affectedTargets(stores, entryId, options = {}) {
|
|
21
26
|
const maxDepth = options.maxDepth ?? Infinity;
|
|
22
27
|
const seen = new Set([entryId]);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SMOODLY_VERSION = "0.0.
|
|
1
|
+
export declare const SMOODLY_VERSION = "0.0.50";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/write-version.mjs from package.json — do not edit.
|
|
2
|
-
export const SMOODLY_VERSION = "0.0.
|
|
2
|
+
export const SMOODLY_VERSION = "0.0.50";
|
package/package.json
CHANGED