smoodly 0.0.51 → 0.0.53
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 +14 -13
- package/dist/admin/auth.d.ts +3 -11
- package/dist/admin/auth.js +1 -30
- package/dist/admin/client-ops.d.ts +5 -4
- package/dist/admin/client-ops.js +15 -12
- package/dist/admin/delivery-mode.d.ts +1 -1
- package/dist/admin/delivery-mode.js +2 -2
- package/dist/admin/drift.d.ts +0 -2
- package/dist/admin/drift.js +1 -1
- package/dist/admin/editor/EditorView.d.ts +0 -3
- package/dist/admin/editor/EditorView.js +11 -16
- package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
- package/dist/admin/editor/FloatingToolbar.js +1 -1
- package/dist/admin/editor/SeoSettings.d.ts +0 -2
- package/dist/admin/editor/SeoSettings.js +2 -2
- package/dist/admin/editor/canvas-address.d.ts +8 -0
- package/dist/admin/editor/canvas-address.js +18 -2
- package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
- package/dist/admin/editor/canvas-diagnosis.js +3 -0
- package/dist/admin/editor/inline-dom.d.ts +0 -4
- package/dist/admin/editor/inline-dom.js +1 -1
- package/dist/admin/editor/preview-grant.d.ts +6 -0
- package/dist/admin/editor/preview-grant.js +25 -0
- package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
- package/dist/admin/files.generated.js +1 -1
- package/dist/admin/fixed-nodes.d.ts +0 -6
- package/dist/admin/fixed-nodes.js +1 -81
- package/dist/admin/forms/CropEditor.d.ts +4 -2
- package/dist/admin/forms/CropEditor.js +58 -5
- package/dist/admin/forms/ImagePreview.d.ts +3 -1
- package/dist/admin/forms/ImagePreview.js +9 -3
- package/dist/admin/forms/MediaGrid.js +2 -1
- package/dist/admin/forms/MediaPicker.js +9 -6
- package/dist/admin/forms/MediaWidget.js +11 -9
- package/dist/admin/forms/assets-context.d.ts +7 -3
- package/dist/admin/forms/assets-context.js +5 -3
- package/dist/admin/forms/crop-editor.d.ts +24 -2
- package/dist/admin/forms/crop-editor.js +36 -2
- package/dist/admin/forms/date-picker.d.ts +0 -1
- package/dist/admin/forms/date-picker.js +1 -1
- package/dist/admin/forms/tabs.d.ts +0 -1
- package/dist/admin/forms/tabs.js +1 -1
- package/dist/admin/forms/upload.d.ts +18 -11
- package/dist/admin/forms/upload.js +62 -24
- package/dist/admin/global-items.d.ts +0 -3
- package/dist/admin/global-items.js +0 -54
- package/dist/admin/handler.d.ts +22 -28
- package/dist/admin/handler.js +146 -47
- package/dist/admin/health.d.ts +0 -4
- package/dist/admin/index.d.ts +6 -9
- package/dist/admin/index.js +3 -6
- package/dist/admin/ops.d.ts +40 -32
- package/dist/admin/page-data.d.ts +14 -6
- package/dist/admin/page-data.js +1 -1
- package/dist/admin/session-flow.d.ts +65 -0
- package/dist/admin/session-flow.js +121 -0
- package/dist/admin/shell/AdminApp.d.ts +8 -3
- package/dist/admin/shell/AdminApp.js +37 -58
- package/dist/admin/shell/ContentHealthPage.js +1 -3
- package/dist/admin/shell/GlobalFields.d.ts +0 -3
- package/dist/admin/shell/GlobalFields.js +1 -1
- package/dist/admin/shell/IconRail.js +10 -8
- package/dist/admin/shell/MediaScreen.js +9 -6
- package/dist/admin/shell/MembersPage.d.ts +1 -2
- package/dist/admin/shell/MembersPage.js +19 -90
- package/dist/admin/shell/SettingsView.d.ts +1 -2
- package/dist/admin/shell/SettingsView.js +2 -2
- package/dist/admin/shell/SignedOutView.d.ts +5 -0
- package/dist/admin/shell/SignedOutView.js +9 -0
- package/dist/admin/shell/console-links.d.ts +5 -0
- package/dist/admin/shell/console-links.js +11 -0
- package/dist/admin/shell/members-list.d.ts +1 -14
- package/dist/admin/shell/members-list.js +2 -23
- package/dist/admin/shell/pages-tree.d.ts +0 -2
- package/dist/admin/shell/pages-tree.js +1 -1
- package/dist/admin/shell/presence-socket.d.ts +10 -0
- package/dist/admin/shell/presence-socket.js +191 -0
- package/dist/admin/shell/presence.d.ts +6 -18
- package/dist/admin/shell/presence.js +4 -11
- package/dist/admin/shell/session.d.ts +19 -12
- package/dist/admin/shell/session.js +114 -47
- package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
- package/dist/admin/ui/LocaleSwitcher.js +2 -2
- package/dist/admin/ui/primitives.d.ts +4 -14
- package/dist/admin/ui/primitives.js +7 -9
- package/dist/admin/ui/theme.js +13 -2
- package/dist/admin/validate.js +10 -1
- package/dist/api-client.d.ts +26 -0
- package/dist/api-client.js +107 -0
- package/dist/assemble.d.ts +4 -41
- package/dist/assemble.js +5 -303
- package/dist/asset-store.d.ts +28 -30
- package/dist/asset-store.js +26 -121
- package/dist/astro/cache.d.ts +1 -1
- package/dist/astro/components/Head.astro +4 -1
- package/dist/astro/components/Image.astro +12 -17
- package/dist/astro/components/Video.astro +5 -2
- package/dist/astro/head.d.ts +3 -0
- package/dist/astro/head.js +24 -0
- package/dist/astro/index.js +61 -27
- package/dist/astro/locale-paths.d.ts +5 -2
- package/dist/astro/locale-paths.js +10 -2
- package/dist/astro/routes/admin.js +9 -7
- package/dist/astro/runtime.d.ts +3 -2
- package/dist/astro/runtime.js +5 -4
- package/dist/astro/server.d.ts +7 -11
- package/dist/astro/server.js +22 -28
- package/dist/astro/shell.d.ts +3 -3
- package/dist/astro/shell.js +1 -1
- package/dist/astro/static-media-node.d.ts +4 -0
- package/dist/astro/static-media-node.js +25 -0
- package/dist/astro/static-media.d.ts +17 -0
- package/dist/astro/static-media.js +55 -0
- package/dist/astro/static.d.ts +5 -10
- package/dist/astro/static.js +13 -29
- package/dist/canvas-marker.d.ts +14 -0
- package/dist/canvas-marker.js +36 -0
- package/dist/cli/api.d.ts +14 -0
- package/dist/cli/api.js +48 -0
- package/dist/cli/app-env.d.ts +2 -0
- package/dist/cli/app-env.js +20 -0
- package/dist/cli/export.d.ts +16 -2
- package/dist/cli/export.js +52 -35
- package/dist/cli/import.d.ts +7 -11
- package/dist/cli/import.js +50 -54
- package/dist/cli/load-config.d.ts +0 -1
- package/dist/cli/load-config.js +1 -1
- package/dist/cli/main.d.ts +4 -12
- package/dist/cli/main.js +26 -72
- package/dist/cli/push.d.ts +6 -0
- package/dist/cli/push.js +31 -0
- package/dist/collections.d.ts +0 -1
- package/dist/collections.js +1 -1
- package/dist/config.d.ts +20 -5
- package/dist/config.js +18 -1
- package/dist/content/content-api.d.ts +23 -23
- package/dist/content/content-api.js +48 -56
- package/dist/content/mime.d.ts +1 -0
- package/dist/content/mime.js +6 -0
- package/dist/css.js +1 -1
- package/dist/deploy-store.d.ts +19 -0
- package/dist/entry-store.d.ts +8 -94
- package/dist/entry-store.js +9 -557
- package/dist/env.d.ts +27 -20
- package/dist/env.js +38 -50
- package/dist/fields.d.ts +11 -3
- package/dist/fields.js +33 -2
- package/dist/global-id.d.ts +2 -0
- package/dist/global-id.js +4 -2
- package/dist/image/SmoodlyImage.d.ts +9 -5
- package/dist/image/SmoodlyImage.js +29 -15
- package/dist/image/SmoodlyVideo.d.ts +1 -1
- package/dist/image/SmoodlyVideo.js +3 -2
- package/dist/image/index.d.ts +2 -3
- package/dist/image/index.js +0 -2
- package/dist/image/markup.d.ts +53 -0
- package/dist/image/markup.js +111 -0
- package/dist/index.d.ts +13 -23
- package/dist/index.js +7 -20
- package/dist/media-cut.d.ts +44 -0
- package/dist/media-cut.js +70 -0
- package/dist/media.d.ts +58 -4
- package/dist/media.js +126 -16
- package/dist/member-store.d.ts +2 -50
- package/dist/member-store.js +0 -56
- package/dist/next/admin-route.d.ts +1 -1
- package/dist/next/admin-route.js +5 -3
- package/dist/next/meta.js +2 -3
- package/dist/next/page-renderer.d.ts +2 -2
- package/dist/next/page-renderer.js +22 -11
- package/dist/next/queries.d.ts +2 -2
- package/dist/path-index.d.ts +0 -24
- package/dist/path-index.js +0 -98
- package/dist/paths.d.ts +1 -1
- package/dist/paths.js +1 -1
- package/dist/push.d.ts +30 -0
- package/dist/push.js +52 -0
- package/dist/read-counter.d.ts +2 -0
- package/dist/read-counter.js +6 -0
- package/dist/reads.d.ts +7 -7
- package/dist/reads.js +3 -0
- package/dist/registry-json.d.ts +11 -0
- package/dist/registry-json.js +81 -0
- package/dist/render.d.ts +0 -1
- package/dist/render.js +1 -1
- package/dist/resolve.d.ts +12 -6
- package/dist/resolve.js +11 -8
- package/dist/revalidate.d.ts +8 -1
- package/dist/revalidate.js +8 -1
- package/dist/richtext/index.d.ts +1 -1
- package/dist/section-wrapper.d.ts +1 -1
- package/dist/section-wrapper.js +0 -9
- package/dist/signed-message.d.ts +26 -0
- package/dist/signed-message.js +103 -0
- package/dist/site.d.ts +59 -18
- package/dist/site.js +21 -252
- package/dist/space-transfer.d.ts +6 -3
- package/dist/space-transfer.js +18 -6
- package/dist/store.d.ts +2 -80
- package/dist/store.js +3 -489
- package/dist/toolset.d.ts +2 -1
- package/dist/toolset.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -15
- package/dist/admin/auth-config.d.ts +0 -6
- package/dist/admin/browser/fetch-op.d.ts +0 -10
- package/dist/admin/browser/fetch-op.js +0 -44
- package/dist/admin/browser/main.d.ts +0 -1
- package/dist/admin/browser/main.js +0 -23
- package/dist/admin/ops-impl.d.ts +0 -51
- package/dist/admin/ops-impl.js +0 -1403
- package/dist/admin/shell/InviteModal.d.ts +0 -6
- package/dist/admin/shell/InviteModal.js +0 -28
- package/dist/admin/shell/LoginView.d.ts +0 -5
- package/dist/admin/shell/LoginView.js +0 -80
- package/dist/admin/shell/SchemaNotice.d.ts +0 -4
- package/dist/admin/shell/SchemaNotice.js +0 -4
- package/dist/admin/shell/SetPasswordView.d.ts +0 -7
- package/dist/admin/shell/SetPasswordView.js +0 -41
- package/dist/admin/shell/presence-realtime.d.ts +0 -3
- package/dist/admin/shell/presence-realtime.js +0 -32
- package/dist/admin/supabase-auth.d.ts +0 -7
- package/dist/admin/supabase-auth.js +0 -30
- package/dist/astro/deploy-deps.d.ts +0 -10
- package/dist/astro/deploy-deps.js +0 -8
- package/dist/astro/image.d.ts +0 -27
- package/dist/astro/image.js +0 -29
- package/dist/bind.d.ts +0 -19
- package/dist/bind.js +0 -34
- package/dist/cli/add-admin.d.ts +0 -11
- package/dist/cli/add-admin.js +0 -25
- package/dist/cli/load-collections.d.ts +0 -2
- package/dist/cli/load-collections.js +0 -27
- package/dist/cli/migrate.d.ts +0 -43
- package/dist/cli/migrate.js +0 -138
- package/dist/cli/rebuild.d.ts +0 -23
- package/dist/cli/rebuild.js +0 -22
- package/dist/cli/space-io.d.ts +0 -41
- package/dist/cli/space-io.js +0 -84
- package/dist/client.d.ts +0 -11
- package/dist/client.js +0 -54
- package/dist/current-space.d.ts +0 -4
- package/dist/current-space.js +0 -9
- package/dist/document-stamp.d.ts +0 -7
- package/dist/document-stamp.js +0 -48
- package/dist/image/loaders.d.ts +0 -15
- package/dist/image/loaders.js +0 -5
- package/dist/image/next-config.d.ts +0 -18
- package/dist/image/next-config.js +0 -32
- package/dist/image/remote-pattern.d.ts +0 -1
- package/dist/image/remote-pattern.js +0 -5
- package/dist/migrations.d.ts +0 -37
- package/dist/migrations.js +0 -73
- package/dist/project.d.ts +0 -88
- package/dist/project.js +0 -231
- package/dist/published.d.ts +0 -142
- package/dist/published.js +0 -164
- package/dist/ref-index.d.ts +0 -19
- package/dist/ref-index.js +0 -0
- package/dist/schema-read.d.ts +0 -23
- package/dist/schema-read.js +0 -50
- package/dist/schema-version.d.ts +0 -19
- package/dist/schema-version.js +0 -34
- package/dist/sql-space.d.ts +0 -44
- package/dist/sql-space.js +0 -289
- package/dist/sql.d.ts +0 -15
- package/dist/sql.js +0 -556
- package/dist/supabase-asset-store.d.ts +0 -21
- package/dist/supabase-asset-store.js +0 -170
- package/dist/supabase-entry-store.d.ts +0 -92
- package/dist/supabase-entry-store.js +0 -824
- package/dist/supabase-member-store.d.ts +0 -21
- package/dist/supabase-member-store.js +0 -96
- package/dist/supabase-path-index.d.ts +0 -30
- package/dist/supabase-path-index.js +0 -138
- package/dist/supabase-published-store.d.ts +0 -56
- package/dist/supabase-published-store.js +0 -314
- package/dist/supabase-store.d.ts +0 -103
- package/dist/supabase-store.js +0 -678
- package/migrations/20260926120000_smoodly_v1.sql +0 -535
- package/migrations/20260926130000_smoodly_v2.sql +0 -26
- package/migrations/20260926140000_smoodly_v3.sql +0 -38
- package/migrations/20260926150000_smoodly_v4.sql +0 -41
- package/migrations/20260929120000_smoodly_v5.sql +0 -56
- package/migrations/20260929135523_smoodly_v6.sql +0 -200
- package/migrations/20260930040956_smoodly_v7.sql +0 -84
- package/migrations/20261007120000_smoodly_v8.sql +0 -154
- package/migrations/20261007200000_smoodly_v9.sql +0 -30
- package/migrations/20261008120000_smoodly_v10.sql +0 -47
- package/migrations/20261009120000_smoodly_v11.sql +0 -49
- /package/dist/{admin/auth-config.js → deploy-store.js} +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const STATIC_MEDIA_DIR = "/_smoodly/media";
|
|
2
|
+
const hex = (bytes) => [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3
|
+
/** `/_smoodly/media/<sha256(sourceUrl + format), first 16 hex>.<ext>`: the same address and format, the same file. */
|
|
4
|
+
export async function staticMediaPath(sourceUrl, format, ext) {
|
|
5
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${sourceUrl}\n${format}`));
|
|
6
|
+
return `${STATIC_MEDIA_DIR}/${hex(digest).slice(0, 16)}.${ext}`;
|
|
7
|
+
}
|
|
8
|
+
/** An original's extension from its address (`…/a1.jpg` → `jpg`), else `bin`. */
|
|
9
|
+
const extOf = (url) => /\.([a-z0-9]{1,8})$/i.exec(new URL(url).pathname)?.[1]?.toLowerCase() ?? "bin";
|
|
10
|
+
const candidates = (srcset) => srcset.split(", ").map((c) => { const [url, width] = c.trim().split(/\s+/); return { url: url, ...(width ? { width } : {}) }; });
|
|
11
|
+
/** The markup with every media address replaced by a file of the output, each file handed to `register`. */
|
|
12
|
+
export async function rewriteForStatic(m, register) {
|
|
13
|
+
const file = async (url, format) => {
|
|
14
|
+
const path = await staticMediaPath(url, format, format === "original" ? extOf(url) : format);
|
|
15
|
+
register({ url, format, path });
|
|
16
|
+
return path;
|
|
17
|
+
};
|
|
18
|
+
const inFormat = async (list, format) => (await Promise.all(list.map(async (c) => `${await file(c.url, format)}${c.width ? ` ${c.width}` : ""}`))).join(", ");
|
|
19
|
+
// A cut lists widths; an original is one address with no descriptor.
|
|
20
|
+
const cutOf = (srcset) => (srcset && candidates(srcset).every((c) => c.width) ? candidates(srcset) : null);
|
|
21
|
+
const sources = [];
|
|
22
|
+
const both = async (base, list) => {
|
|
23
|
+
sources.push({ ...base, type: "image/avif", srcset: await inFormat(list, "avif") });
|
|
24
|
+
sources.push({ ...base, type: "image/webp", srcset: await inFormat(list, "webp") });
|
|
25
|
+
};
|
|
26
|
+
for (const { srcset, type: _type, ...base } of m.sources) {
|
|
27
|
+
const list = cutOf(srcset);
|
|
28
|
+
if (list)
|
|
29
|
+
await both(base, list);
|
|
30
|
+
else
|
|
31
|
+
sources.push({ ...base, srcset: await inFormat(candidates(srcset), "original") });
|
|
32
|
+
}
|
|
33
|
+
const main = cutOf(m.img.srcset);
|
|
34
|
+
const { srcset: _srcset, sizes, ...img } = m.img;
|
|
35
|
+
if (main) {
|
|
36
|
+
await both({ ...(sizes ? { sizes } : {}), width: img.width, height: img.height }, main);
|
|
37
|
+
return { ...m, sources, img: { ...img, src: await file(m.img.src, "webp") } };
|
|
38
|
+
}
|
|
39
|
+
return { ...m, sources, img: { ...img, ...(sizes ? { sizes } : {}), src: await file(m.img.src, "original") } };
|
|
40
|
+
}
|
|
41
|
+
/** Where a prerendered page hands its files: installed by the integration for one static build. */
|
|
42
|
+
const COLLECTOR = Symbol.for("smoodly.staticMedia");
|
|
43
|
+
/** The build's collector (index.ts, astro:build:start): every file the pages register, once per path. */
|
|
44
|
+
export function collectStaticMedia() {
|
|
45
|
+
const entries = new Map();
|
|
46
|
+
globalThis[COLLECTOR] = (entry) => void entries.set(entry.path, entry);
|
|
47
|
+
return entries;
|
|
48
|
+
}
|
|
49
|
+
/** What Image.astro calls in a prerendered page of static output: the rewrite, each file handed to the build. */
|
|
50
|
+
export async function staticImage(m) {
|
|
51
|
+
const register = globalThis[COLLECTOR];
|
|
52
|
+
if (!register)
|
|
53
|
+
throw new Error("smoodly: a static page rendered outside the integration's `astro build` — no build collects its images.");
|
|
54
|
+
return rewriteForStatic(m, register);
|
|
55
|
+
}
|
package/dist/astro/static.d.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import type { ResolvedConfig } from "../config.ts";
|
|
2
|
-
import type {
|
|
3
|
-
import type { PageStore } from "../store.ts";
|
|
2
|
+
import type { DeliveryReads } from "../site.ts";
|
|
4
3
|
import { type LocaleRouting } from "./locale-paths.ts";
|
|
5
4
|
type Href = (path: string, locale: string) => string;
|
|
6
|
-
/** Every published page and entry address of every locale, as Astro's getStaticPaths entries
|
|
7
|
-
* the delivery tree (a node with no path, hidden or under a code address, is not a page); entries from the paths
|
|
8
|
-
* index's live rows, kept to the entries with a published delivery row — the Supabase delivery store lists entries
|
|
9
|
-
* with their document only, no path (0.0.48 built no entry page for it), and the index is what visitors reach. */
|
|
5
|
+
/** Every published page and entry address of every locale, as Astro's getStaticPaths entries, each once. */
|
|
10
6
|
export declare function staticPaths(deps: {
|
|
11
|
-
|
|
12
|
-
pages: PageStore;
|
|
7
|
+
delivery: DeliveryReads;
|
|
13
8
|
config: ResolvedConfig;
|
|
14
9
|
href: Href;
|
|
15
10
|
}): Promise<{
|
|
@@ -18,9 +13,9 @@ export declare function staticPaths(deps: {
|
|
|
18
13
|
};
|
|
19
14
|
}[]>;
|
|
20
15
|
/** The redirect rows as Astro's `redirects` config: old address → the target's current address, 308 (spec
|
|
21
|
-
* 2026-09-26 redirects §7 at the host).
|
|
16
|
+
* 2026-09-26 redirects §7 at the host). The service leaves out a target with no live path in the locale. */
|
|
22
17
|
export declare function redirectEntries(deps: {
|
|
23
|
-
|
|
18
|
+
delivery: DeliveryReads;
|
|
24
19
|
config: ResolvedConfig;
|
|
25
20
|
href: Href;
|
|
26
21
|
}): Promise<Record<string, {
|
package/dist/astro/static.js
CHANGED
|
@@ -4,32 +4,19 @@ const param = (site) => {
|
|
|
4
4
|
const trimmed = site.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
5
5
|
return trimmed === "" ? undefined : trimmed;
|
|
6
6
|
};
|
|
7
|
-
/** Every published page and entry address of every locale, as Astro's getStaticPaths entries
|
|
8
|
-
* the delivery tree (a node with no path, hidden or under a code address, is not a page); entries from the paths
|
|
9
|
-
* index's live rows, kept to the entries with a published delivery row — the Supabase delivery store lists entries
|
|
10
|
-
* with their document only, no path (0.0.48 built no entry page for it), and the index is what visitors reach. */
|
|
7
|
+
/** Every published page and entry address of every locale, as Astro's getStaticPaths entries, each once. */
|
|
11
8
|
export async function staticPaths(deps) {
|
|
12
9
|
const seen = new Set();
|
|
13
10
|
const out = [];
|
|
14
|
-
const add = (site) => {
|
|
15
|
-
const p = param(site);
|
|
16
|
-
if (seen.has(p))
|
|
17
|
-
return;
|
|
18
|
-
seen.add(p);
|
|
19
|
-
out.push({ params: { path: p } });
|
|
20
|
-
};
|
|
21
|
-
const collections = (deps.config.registry.collections ?? []).map((c) => c.name);
|
|
22
11
|
for (const locale of deps.config.locales.supported) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (row.kind === "entry" && published.has(row.id))
|
|
32
|
-
add(deps.href(row.path, locale));
|
|
12
|
+
const { pages, entries } = await deps.delivery.addresses(locale);
|
|
13
|
+
for (const path of [...pages, ...entries]) {
|
|
14
|
+
const p = param(deps.href(path, locale));
|
|
15
|
+
if (seen.has(p))
|
|
16
|
+
continue;
|
|
17
|
+
seen.add(p);
|
|
18
|
+
out.push({ params: { path: p } });
|
|
19
|
+
}
|
|
33
20
|
}
|
|
34
21
|
return out;
|
|
35
22
|
}
|
|
@@ -37,16 +24,13 @@ export async function staticPaths(deps) {
|
|
|
37
24
|
* some hosts. A stored path with any of them would redirect more than itself, so it is a literal address or nothing. */
|
|
38
25
|
const literalAddress = (path) => !/[[\]*:]/.test(path);
|
|
39
26
|
/** The redirect rows as Astro's `redirects` config: old address → the target's current address, 308 (spec
|
|
40
|
-
* 2026-09-26 redirects §7 at the host).
|
|
27
|
+
* 2026-09-26 redirects §7 at the host). The service leaves out a target with no live path in the locale. */
|
|
41
28
|
export async function redirectEntries(deps) {
|
|
42
29
|
const out = {};
|
|
43
30
|
for (const locale of deps.config.locales.supported) {
|
|
44
|
-
for (const row of await deps.
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
const current = (await deps.pages.targetPaths({ kind: row.kind, id: row.id }))[locale];
|
|
48
|
-
if (current && literalAddress(current))
|
|
49
|
-
out[deps.href(row.path, locale)] = { status: 308, destination: deps.href(current, locale) };
|
|
31
|
+
for (const row of (await deps.delivery.addresses(locale)).redirects) {
|
|
32
|
+
if (literalAddress(row.from) && literalAddress(row.to))
|
|
33
|
+
out[deps.href(row.from, locale)] = { status: 308, destination: deps.href(row.to, locale) };
|
|
50
34
|
}
|
|
51
35
|
}
|
|
52
36
|
return out;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type CanvasFlags = {
|
|
2
|
+
editor: true;
|
|
3
|
+
} | {
|
|
4
|
+
version: string;
|
|
5
|
+
};
|
|
6
|
+
/** `path` with the marker as its last segment, before any query or hash. */
|
|
7
|
+
export declare function markCanvasPath(path: string, flags: CanvasFlags): string;
|
|
8
|
+
export type CanvasMarker = {
|
|
9
|
+
segments: string[];
|
|
10
|
+
editor: boolean;
|
|
11
|
+
version?: string;
|
|
12
|
+
};
|
|
13
|
+
/** The marker taken off a catch-all's segments, which a host may hand over encoded or not. */
|
|
14
|
+
export declare function readCanvasMarker(segments: string[]): CanvasMarker;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The canvas marker: where the editor's canvas and History's frame say what they want of a draft render when the
|
|
2
|
+
// host strips the query. Vercel keeps only a prerendered route's own params in the query, draft mode included, so
|
|
3
|
+
// on Next the flags ride the path's last segment: `~editor` (mount the editor bridge) or `~version~<id>` (render a
|
|
4
|
+
// stored version). The admin writes it (admin/editor/canvas-address.ts), the Next renderer reads it inside draft
|
|
5
|
+
// mode only; outside draft mode a marked address is an ordinary miss. A slug never starts with `~`.
|
|
6
|
+
const EDITOR = "~editor";
|
|
7
|
+
const VERSION = "~version~";
|
|
8
|
+
/** `path` with the marker as its last segment, before any query or hash. */
|
|
9
|
+
export function markCanvasPath(path, flags) {
|
|
10
|
+
const cut = path.search(/[?#]/);
|
|
11
|
+
const [pathname, rest] = cut === -1 ? [path, ""] : [path.slice(0, cut), path.slice(cut)];
|
|
12
|
+
const segment = "editor" in flags ? EDITOR : VERSION + encodeURIComponent(flags.version);
|
|
13
|
+
return `${pathname.replace(/\/+$/, "")}/${segment}${rest}`;
|
|
14
|
+
}
|
|
15
|
+
/** The marker taken off a catch-all's segments, which a host may hand over encoded or not. */
|
|
16
|
+
export function readCanvasMarker(segments) {
|
|
17
|
+
const last = segments.length > 0 ? safeDecode(segments[segments.length - 1]) : "";
|
|
18
|
+
const rest = segments.slice(0, -1);
|
|
19
|
+
if (last === EDITOR)
|
|
20
|
+
return { segments: rest, editor: true };
|
|
21
|
+
if (last.startsWith(VERSION) && last.length > VERSION.length) {
|
|
22
|
+
const raw = segments[segments.length - 1];
|
|
23
|
+
// The id was encoded once when the marker was written; a host that decoded the segment already handed it over plain.
|
|
24
|
+
const id = raw.startsWith(VERSION) ? safeDecode(raw.slice(VERSION.length)) : last.slice(VERSION.length);
|
|
25
|
+
return { segments: rest, editor: false, version: id };
|
|
26
|
+
}
|
|
27
|
+
return { segments, editor: false };
|
|
28
|
+
}
|
|
29
|
+
function safeDecode(segment) {
|
|
30
|
+
try {
|
|
31
|
+
return decodeURIComponent(segment);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return segment;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type CliApi = {
|
|
2
|
+
get<T>(path: string): Promise<T>;
|
|
3
|
+
post<T>(path: string, body: unknown): Promise<T>;
|
|
4
|
+
/** The bytes, or null when the API answers 404. */
|
|
5
|
+
getBytes(path: string): Promise<Uint8Array | null>;
|
|
6
|
+
postBytes<T>(path: string, bytes: Uint8Array, mime: string): Promise<T>;
|
|
7
|
+
};
|
|
8
|
+
/** The API refused: its status, its code and its message, as the content API words them. */
|
|
9
|
+
export declare class CliApiError extends Error {
|
|
10
|
+
readonly status: number;
|
|
11
|
+
readonly code: string;
|
|
12
|
+
constructor(status: number, code: string, message: string);
|
|
13
|
+
}
|
|
14
|
+
export declare function connectApi(cwd: string, env?: Record<string, string | undefined>, doFetch?: typeof fetch): CliApi;
|
package/dist/cli/api.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The CLI's door to Smoodly's API (plan 2026-10-14 the service holds the stores): the site's env — SMOODLY_API_URL,
|
|
2
|
+
// SMOODLY_SPACE_ID, SMOODLY_SECRET_KEY, from the app's env files — and the content API's envelope `{ ok, data }`.
|
|
3
|
+
import { apiEnv } from "../env.js";
|
|
4
|
+
import { loadAppEnv } from "./app-env.js";
|
|
5
|
+
/** The API refused: its status, its code and its message, as the content API words them. */
|
|
6
|
+
export class CliApiError extends Error {
|
|
7
|
+
constructor(status, code, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.name = "CliApiError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function connectApi(cwd, env = process.env, doFetch = fetch) {
|
|
15
|
+
const read = loadAppEnv(cwd, env);
|
|
16
|
+
if (read.length === 0 && !env.SMOODLY_API_URL && !env.SMOODLY_SECRET_KEY) {
|
|
17
|
+
throw new Error(`smoodly: no .env.local or .env in ${cwd} — run this in your app's folder.`);
|
|
18
|
+
}
|
|
19
|
+
const { apiUrl, spaceId, key } = apiEnv(env);
|
|
20
|
+
const base = `${apiUrl}/v1/spaces/${encodeURIComponent(spaceId)}`;
|
|
21
|
+
async function call(path, init = {}) {
|
|
22
|
+
try {
|
|
23
|
+
return await doFetch(`${base}${path}`, { ...init, headers: { ...init.headers, authorization: `Bearer ${key}` } });
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
throw new Error(`smoodly: can't reach SMOODLY_API_URL (${apiUrl}): ${e instanceof Error ? e.message : String(e)}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function data(res) {
|
|
30
|
+
const body = (await res.json().catch(() => null));
|
|
31
|
+
if (body?.ok === true)
|
|
32
|
+
return body.data;
|
|
33
|
+
throw new CliApiError(res.status, typeof body?.code === "string" ? body.code : "internal", typeof body?.message === "string" ? body.message : `smoodly: SMOODLY_API_URL (${apiUrl}) answered ${res.status} with no message.`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
get: async (path) => data(await call(path)),
|
|
37
|
+
post: async (path, body) => data(await call(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })),
|
|
38
|
+
async getBytes(path) {
|
|
39
|
+
const res = await call(path);
|
|
40
|
+
if (res.status === 404)
|
|
41
|
+
return null;
|
|
42
|
+
if (!res.ok)
|
|
43
|
+
return data(res);
|
|
44
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
45
|
+
},
|
|
46
|
+
postBytes: async (path, bytes, mime) => data(await call(path, { method: "POST", headers: { "content-type": mime }, body: bytes })),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The app's env files as the CLI reads them (spec 2026-09-28 export/import §5): `.env.local`, then `.env`, as Next
|
|
2
|
+
// reads them; a variable already set wins.
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { parseEnv } from "node:util";
|
|
6
|
+
/** `.env.local`, then `.env`, as Next reads them; a variable already set wins (spec §5). Returns the files it read. */
|
|
7
|
+
export function loadAppEnv(cwd, env = process.env) {
|
|
8
|
+
const read = [];
|
|
9
|
+
for (const name of [".env.local", ".env"]) {
|
|
10
|
+
const file = join(cwd, name);
|
|
11
|
+
if (!existsSync(file))
|
|
12
|
+
continue;
|
|
13
|
+
read.push(name);
|
|
14
|
+
for (const [key, value] of Object.entries(parseEnv(readFileSync(file, "utf8")))) {
|
|
15
|
+
if (env[key] === undefined)
|
|
16
|
+
env[key] = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return read;
|
|
20
|
+
}
|
package/dist/cli/export.d.ts
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ContentTable } from "../space-transfer.ts";
|
|
2
|
+
import { type ExportMember, type Row } from "../space-transfer.ts";
|
|
3
|
+
import { type CliApi } from "./api.ts";
|
|
2
4
|
export type ExportArgs = {
|
|
3
5
|
out: string | null;
|
|
4
6
|
};
|
|
5
7
|
export type ExportDeps = {
|
|
6
8
|
cwd?: string;
|
|
7
|
-
|
|
9
|
+
api?: CliApi;
|
|
8
10
|
now?: () => Date;
|
|
11
|
+
/** Where SMOODLY_SPACE_ID is read, after connectApi loaded the app's env files into it; default process.env. */
|
|
12
|
+
env?: Record<string, string | undefined>;
|
|
13
|
+
};
|
|
14
|
+
/** What a transfer starts from (the service's GET /transfer). */
|
|
15
|
+
export type TransferHead = {
|
|
16
|
+
schemaVersion: number;
|
|
17
|
+
members: ExportMember[];
|
|
9
18
|
};
|
|
10
19
|
export declare const defaultExportDir: (now: Date) => string;
|
|
11
20
|
export declare const folderExists: (dir: string) => string;
|
|
12
21
|
export declare const CHANGED_DURING_EXPORT = "Content changed during export \u2014 run it again.";
|
|
13
22
|
export declare const missingFile: (path: string) => string;
|
|
23
|
+
/** The transfer's head, or null after printing the API's refusal of a database without the schema. */
|
|
24
|
+
export declare function transferHead(api: CliApi, out: (line: string) => void): Promise<TransferHead | null>;
|
|
25
|
+
/** Every page of one table, as the service answers it: `keys` reads only the key columns. */
|
|
26
|
+
export declare function readTable(api: CliApi, table: ContentTable, keys?: boolean): Promise<Row[]>;
|
|
27
|
+
export declare const mediaUrl: (path: string) => string;
|
|
14
28
|
export declare function runExport(args: ExportArgs, out: (line: string) => void, deps?: ExportDeps): Promise<number>;
|
package/dist/cli/export.js
CHANGED
|
@@ -1,21 +1,50 @@
|
|
|
1
1
|
// packages/smoodly/src/cli/export.ts
|
|
2
|
-
// `smoodly export` (spec 2026-09-28 export/import §8
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
2
|
+
// `smoodly export` (spec 2026-09-28 export/import §8; over the API since plan
|
|
3
|
+
// 2026-10-14 the service holds the stores): the key's space — every content
|
|
4
|
+
// row, history included, and the media files — into a folder. The service
|
|
5
|
+
// pipes the rows without their space; the consistency check runs here, over
|
|
6
|
+
// the whole export. Reads only; the folder appears whole or not at all
|
|
7
|
+
// (written as <dir>.partial, renamed last). The message strings are what the
|
|
8
|
+
// guide quotes.
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
10
|
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
9
11
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
10
|
-
import {
|
|
11
|
-
import { CONTENT_TABLES } from "../
|
|
12
|
-
import { FORMAT, brokenReferences, countRows, describeExport, emptyTables,
|
|
13
|
-
import {
|
|
14
|
-
import { readCurrentSpace } from "../current-space.js";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { CONTENT_TABLES } from "../space-transfer.js";
|
|
14
|
+
import { FORMAT, brokenReferences, countRows, describeExport, emptyTables, sortRows, } from "../space-transfer.js";
|
|
15
|
+
import { CliApiError, connectApi } from "./api.js";
|
|
15
16
|
export const defaultExportDir = (now) => `smoodly-export-${now.toISOString().slice(0, 10)}`;
|
|
16
17
|
export const folderExists = (dir) => `Refusing: ${dir} already exists. Remove it, or pass --out <dir>.`;
|
|
17
18
|
export const CHANGED_DURING_EXPORT = "Content changed during export — run it again.";
|
|
18
19
|
export const missingFile = (path) => `Warning: ${path} is missing from the media bucket (already broken on the site); its asset row is exported without a file.`;
|
|
20
|
+
/** The installed package's version for the manifest: `../../package.json` from src/cli/ and dist/cli/ alike. */
|
|
21
|
+
function packageVersion() {
|
|
22
|
+
return JSON.parse(readFileSync(fileURLToPath(new URL("../../package.json", import.meta.url).href), "utf8")).version;
|
|
23
|
+
}
|
|
24
|
+
/** The transfer's head, or null after printing the API's refusal of a database without the schema. */
|
|
25
|
+
export async function transferHead(api, out) {
|
|
26
|
+
try {
|
|
27
|
+
return await api.get("/transfer");
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
if (e instanceof CliApiError && e.code === "validation") {
|
|
31
|
+
out(e.message);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Every page of one table, as the service answers it: `keys` reads only the key columns. */
|
|
38
|
+
export async function readTable(api, table, keys = false) {
|
|
39
|
+
const rows = [];
|
|
40
|
+
for (let offset = 0; offset !== null;) {
|
|
41
|
+
const page = await api.get(`/transfer/rows/${table}?offset=${offset}${keys ? "&keys=1" : ""}`);
|
|
42
|
+
rows.push(...page.rows);
|
|
43
|
+
offset = page.next;
|
|
44
|
+
}
|
|
45
|
+
return rows;
|
|
46
|
+
}
|
|
47
|
+
export const mediaUrl = (path) => `/transfer/media?path=${encodeURIComponent(path)}`;
|
|
19
48
|
export async function runExport(args, out, deps = {}) {
|
|
20
49
|
const cwd = deps.cwd ?? process.cwd();
|
|
21
50
|
const now = (deps.now ?? (() => new Date()))();
|
|
@@ -27,22 +56,16 @@ export async function runExport(args, out, deps = {}) {
|
|
|
27
56
|
out(folderExists(shown));
|
|
28
57
|
return 1;
|
|
29
58
|
}
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const space = await readCurrentSpace(db);
|
|
37
|
-
if (schemaBehind(schema.version)) {
|
|
38
|
-
out(schemaBehindMessage(schema.version));
|
|
59
|
+
const env = deps.env ?? process.env;
|
|
60
|
+
const api = deps.api ?? connectApi(cwd, env);
|
|
61
|
+
// Read after connectApi: it loads the app's .env files. The space's public id; the service knows its database's.
|
|
62
|
+
const space = env.SMOODLY_SPACE_ID ?? "";
|
|
63
|
+
const head = await transferHead(api, out);
|
|
64
|
+
if (!head)
|
|
39
65
|
return 1;
|
|
40
|
-
}
|
|
41
66
|
const rows = emptyTables();
|
|
42
|
-
for (const table of CONTENT_TABLES)
|
|
43
|
-
|
|
44
|
-
rows[table] = sortRows(table, read.map((row) => fromSpace(table, row, space)));
|
|
45
|
-
}
|
|
67
|
+
for (const table of CONTENT_TABLES)
|
|
68
|
+
rows[table] = sortRows(table, await readTable(api, table));
|
|
46
69
|
const broken = brokenReferences(rows);
|
|
47
70
|
if (broken.length > 0) {
|
|
48
71
|
out(CHANGED_DURING_EXPORT);
|
|
@@ -50,9 +73,6 @@ export async function runExport(args, out, deps = {}) {
|
|
|
50
73
|
out(` ${line}`);
|
|
51
74
|
return 1;
|
|
52
75
|
}
|
|
53
|
-
const members = await db.from("members").select("email, role").eq("space_id", space).order("email");
|
|
54
|
-
if (members.error)
|
|
55
|
-
throw new Error(`smoodly: reading members: ${members.error.message}`);
|
|
56
76
|
const partial = `${dir}.partial`;
|
|
57
77
|
await rm(partial, { recursive: true, force: true });
|
|
58
78
|
await mkdir(join(partial, "rows"), { recursive: true });
|
|
@@ -62,15 +82,12 @@ export async function runExport(args, out, deps = {}) {
|
|
|
62
82
|
let bytes = 0;
|
|
63
83
|
for (const asset of rows.assets) {
|
|
64
84
|
const path = String(asset.path);
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
67
|
-
if (!isNotFound(got.error))
|
|
68
|
-
throw new Error(`smoodly: downloading ${path}: ${got.error.message}`);
|
|
85
|
+
const data = await api.getBytes(mediaUrl(path));
|
|
86
|
+
if (!data) {
|
|
69
87
|
missing.push(path);
|
|
70
88
|
out(missingFile(path));
|
|
71
89
|
continue;
|
|
72
90
|
}
|
|
73
|
-
const data = new Uint8Array(await got.data.arrayBuffer());
|
|
74
91
|
const file = join(partial, "media", path);
|
|
75
92
|
await mkdir(dirname(file), { recursive: true });
|
|
76
93
|
await writeFile(file, data);
|
|
@@ -82,13 +99,13 @@ export async function runExport(args, out, deps = {}) {
|
|
|
82
99
|
}
|
|
83
100
|
const manifest = {
|
|
84
101
|
format: FORMAT,
|
|
85
|
-
schemaVersion:
|
|
102
|
+
schemaVersion: head.schemaVersion,
|
|
86
103
|
smoodlyVersion: packageVersion(),
|
|
87
104
|
exportedAt: now.toISOString(),
|
|
88
105
|
sourceSpace: space,
|
|
89
106
|
counts: countRows(rows),
|
|
90
107
|
media: { files, bytes, missing },
|
|
91
|
-
members:
|
|
108
|
+
members: head.members,
|
|
92
109
|
};
|
|
93
110
|
await writeFile(join(partial, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
94
111
|
await rename(partial, dir);
|
package/dist/cli/import.d.ts
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
import { type ContentTable } from "../
|
|
2
|
-
import { type
|
|
3
|
-
import type { ResolvedConfig } from "../config.ts";
|
|
1
|
+
import { type ContentTable } from "../space-transfer.ts";
|
|
2
|
+
import { type CliApi } from "./api.ts";
|
|
4
3
|
export type ImportArgs = {
|
|
5
4
|
dir: string;
|
|
6
5
|
};
|
|
7
6
|
export type ImportDeps = {
|
|
8
7
|
cwd?: string;
|
|
9
|
-
|
|
10
|
-
/** The app's config, which the closing rebuild assembles with; default: loaded from the folder (load-config.ts). */
|
|
11
|
-
config?: ResolvedConfig;
|
|
8
|
+
api?: CliApi;
|
|
12
9
|
batchSize?: number;
|
|
13
10
|
/** Test seam: runs before each batch; a throw stops the import there. */
|
|
14
11
|
beforeBatch?: (table: ContentTable, index: number) => void;
|
|
12
|
+
/** Where SMOODLY_SPACE_ID is read, after connectApi loaded the app's env files into it; default process.env. */
|
|
13
|
+
env?: Record<string, string | undefined>;
|
|
15
14
|
};
|
|
16
|
-
export declare const BATCH_ROWS = 500;
|
|
17
|
-
export declare const notAnExport: (dir: string) => string;
|
|
18
|
-
export declare const formatMismatch: (format: unknown) => string;
|
|
19
15
|
export declare const RUN_AGAIN = "Run the same command again to continue: rows and files that landed are skipped.";
|
|
20
|
-
export declare const MEMBERS_HEADING = "Members in the source \u2014
|
|
21
|
-
export declare const nothingNew: (
|
|
16
|
+
export declare const MEMBERS_HEADING = "Members in the source \u2014 add them to the space's members in the project at smoodly.io:";
|
|
17
|
+
export declare const nothingNew: () => string;
|
|
22
18
|
export declare const CACHE_NOTE = "Restart or redeploy the app: it may have cached pages from while the space was empty.";
|
|
23
19
|
export declare function runImport(args: ImportArgs, out: (line: string) => void, deps?: ImportDeps): Promise<number>;
|