@rsc-kit/press 0.0.0-stage → 0.30.43
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 +117 -2
- package/dist/admin/actions.d.ts +30 -0
- package/dist/admin/actions.js +171 -0
- package/dist/admin/actions.js.map +1 -0
- package/dist/admin/index.d.ts +5 -0
- package/dist/admin/index.js +2 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/panel.d.ts +96 -0
- package/dist/admin/panel.js +354 -0
- package/dist/admin/panel.js.map +1 -0
- package/dist/admin/registry.d.ts +58 -0
- package/dist/admin/registry.js +90 -0
- package/dist/admin/registry.js.map +1 -0
- package/dist/admin/theme.d.ts +251 -0
- package/dist/admin/theme.js +2 -0
- package/dist/admin/theme.js.map +1 -0
- package/dist/admin/url.d.ts +24 -0
- package/dist/admin/url.js +45 -0
- package/dist/admin/url.js.map +1 -0
- package/dist/blocks.d.ts +46 -0
- package/dist/blocks.js +33 -0
- package/dist/blocks.js.map +1 -0
- package/dist/forms/actions.d.ts +7 -0
- package/dist/forms/actions.js +26 -0
- package/dist/forms/actions.js.map +1 -0
- package/dist/forms/form.d.ts +42 -0
- package/dist/forms/form.js +44 -0
- package/dist/forms/form.js.map +1 -0
- package/dist/forms/honeypot.d.ts +2 -0
- package/dist/forms/honeypot.js +3 -0
- package/dist/forms/honeypot.js.map +1 -0
- package/dist/forms/index.d.ts +3 -0
- package/dist/forms/index.js +2 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/forms/registry.d.ts +10 -0
- package/dist/forms/registry.js +17 -0
- package/dist/forms/registry.js.map +1 -0
- package/dist/forms/theme.d.ts +29 -0
- package/dist/forms/theme.js +2 -0
- package/dist/forms/theme.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/installable.d.ts +173 -0
- package/dist/installable.js +490 -0
- package/dist/installable.js.map +1 -0
- package/dist/memory.d.ts +7 -0
- package/dist/memory.js +50 -0
- package/dist/memory.js.map +1 -0
- package/dist/pages/index.d.ts +2 -0
- package/dist/pages/index.js +2 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/pages.d.ts +78 -0
- package/dist/pages/pages.js +183 -0
- package/dist/pages/pages.js.map +1 -0
- package/dist/plugin.d.ts +51 -0
- package/dist/plugin.js +20 -0
- package/dist/plugin.js.map +1 -0
- package/dist/resource.d.ts +320 -0
- package/dist/resource.js +415 -0
- package/dist/resource.js.map +1 -0
- package/dist/shadcn/breadcrumbs.d.ts +6 -0
- package/dist/shadcn/breadcrumbs.js +21 -0
- package/dist/shadcn/breadcrumbs.js.map +1 -0
- package/dist/shadcn/components/ui/alert-dialog.d.ts +18 -0
- package/dist/shadcn/components/ui/alert-dialog.js +44 -0
- package/dist/shadcn/components/ui/alert-dialog.js.map +1 -0
- package/dist/shadcn/components/ui/avatar.d.ts +11 -0
- package/dist/shadcn/components/ui/avatar.js +25 -0
- package/dist/shadcn/components/ui/avatar.js.map +1 -0
- package/dist/shadcn/components/ui/badge.d.ts +7 -0
- package/dist/shadcn/components/ui/badge.js +34 -0
- package/dist/shadcn/components/ui/badge.js.map +1 -0
- package/dist/shadcn/components/ui/breadcrumb.d.ts +10 -0
- package/dist/shadcn/components/ui/breadcrumb.js +38 -0
- package/dist/shadcn/components/ui/breadcrumb.js.map +1 -0
- package/dist/shadcn/components/ui/button.d.ts +8 -0
- package/dist/shadcn/components/ui/button.js +35 -0
- package/dist/shadcn/components/ui/button.js.map +1 -0
- package/dist/shadcn/components/ui/card.d.ts +11 -0
- package/dist/shadcn/components/ui/card.js +26 -0
- package/dist/shadcn/components/ui/card.js.map +1 -0
- package/dist/shadcn/components/ui/checkbox.d.ts +3 -0
- package/dist/shadcn/components/ui/checkbox.js +10 -0
- package/dist/shadcn/components/ui/checkbox.js.map +1 -0
- package/dist/shadcn/components/ui/dropdown-menu.d.ts +29 -0
- package/dist/shadcn/components/ui/dropdown-menu.js +53 -0
- package/dist/shadcn/components/ui/dropdown-menu.js.map +1 -0
- package/dist/shadcn/components/ui/field.d.ts +24 -0
- package/dist/shadcn/components/ui/field.js +69 -0
- package/dist/shadcn/components/ui/field.js.map +1 -0
- package/dist/shadcn/components/ui/input.d.ts +3 -0
- package/dist/shadcn/components/ui/input.js +9 -0
- package/dist/shadcn/components/ui/input.js.map +1 -0
- package/dist/shadcn/components/ui/label.d.ts +3 -0
- package/dist/shadcn/components/ui/label.js +9 -0
- package/dist/shadcn/components/ui/label.js.map +1 -0
- package/dist/shadcn/components/ui/select.d.ts +15 -0
- package/dist/shadcn/components/ui/select.js +36 -0
- package/dist/shadcn/components/ui/select.js.map +1 -0
- package/dist/shadcn/components/ui/separator.d.ts +3 -0
- package/dist/shadcn/components/ui/separator.js +9 -0
- package/dist/shadcn/components/ui/separator.js.map +1 -0
- package/dist/shadcn/components/ui/sheet.d.ts +14 -0
- package/dist/shadcn/components/ui/sheet.js +39 -0
- package/dist/shadcn/components/ui/sheet.js.map +1 -0
- package/dist/shadcn/components/ui/sidebar.d.ts +63 -0
- package/dist/shadcn/components/ui/sidebar.js +257 -0
- package/dist/shadcn/components/ui/sidebar.js.map +1 -0
- package/dist/shadcn/components/ui/skeleton.d.ts +2 -0
- package/dist/shadcn/components/ui/skeleton.js +7 -0
- package/dist/shadcn/components/ui/skeleton.js.map +1 -0
- package/dist/shadcn/components/ui/sonner.d.ts +3 -0
- package/dist/shadcn/components/ui/sonner.js +38 -0
- package/dist/shadcn/components/ui/sonner.js.map +1 -0
- package/dist/shadcn/components/ui/switch.d.ts +5 -0
- package/dist/shadcn/components/ui/switch.js +9 -0
- package/dist/shadcn/components/ui/switch.js.map +1 -0
- package/dist/shadcn/components/ui/table.d.ts +10 -0
- package/dist/shadcn/components/ui/table.js +30 -0
- package/dist/shadcn/components/ui/table.js.map +1 -0
- package/dist/shadcn/components/ui/textarea.d.ts +3 -0
- package/dist/shadcn/components/ui/textarea.js +8 -0
- package/dist/shadcn/components/ui/textarea.js.map +1 -0
- package/dist/shadcn/components/ui/tooltip.d.ts +6 -0
- package/dist/shadcn/components/ui/tooltip.js +18 -0
- package/dist/shadcn/components/ui/tooltip.js.map +1 -0
- package/dist/shadcn/data-table.d.ts +12 -0
- package/dist/shadcn/data-table.js +195 -0
- package/dist/shadcn/data-table.js.map +1 -0
- package/dist/shadcn/fields.d.ts +20 -0
- package/dist/shadcn/fields.js +179 -0
- package/dist/shadcn/fields.js.map +1 -0
- package/dist/shadcn/flash-toasts.d.ts +2 -0
- package/dist/shadcn/flash-toasts.js +11 -0
- package/dist/shadcn/flash-toasts.js.map +1 -0
- package/dist/shadcn/hooks/use-mobile.d.ts +1 -0
- package/dist/shadcn/hooks/use-mobile.js +16 -0
- package/dist/shadcn/hooks/use-mobile.js.map +1 -0
- package/dist/shadcn/index.d.ts +10 -0
- package/dist/shadcn/index.js +31 -0
- package/dist/shadcn/index.js.map +1 -0
- package/dist/shadcn/mode-toggle.d.ts +7 -0
- package/dist/shadcn/mode-toggle.js +52 -0
- package/dist/shadcn/mode-toggle.js.map +1 -0
- package/dist/shadcn/nav.d.ts +5 -0
- package/dist/shadcn/nav.js +19 -0
- package/dist/shadcn/nav.js.map +1 -0
- package/dist/shadcn/pages.d.ts +15 -0
- package/dist/shadcn/pages.js +59 -0
- package/dist/shadcn/pages.js.map +1 -0
- package/dist/shadcn/plugin-forms.d.ts +16 -0
- package/dist/shadcn/plugin-forms.js +25 -0
- package/dist/shadcn/plugin-forms.js.map +1 -0
- package/dist/shadcn/public-form.d.ts +8 -0
- package/dist/shadcn/public-form.js +26 -0
- package/dist/shadcn/public-form.js.map +1 -0
- package/dist/shadcn/resource-form.d.ts +19 -0
- package/dist/shadcn/resource-form.js +30 -0
- package/dist/shadcn/resource-form.js.map +1 -0
- package/dist/shadcn/styles.css +12 -0
- package/dist/shadcn/tenant-switcher.d.ts +3 -0
- package/dist/shadcn/tenant-switcher.js +17 -0
- package/dist/shadcn/tenant-switcher.js.map +1 -0
- package/dist/shadcn/theme-script.d.ts +7 -0
- package/dist/shadcn/theme-script.js +10 -0
- package/dist/shadcn/theme-script.js.map +1 -0
- package/dist/shadcn/user-menu.d.ts +3 -0
- package/dist/shadcn/user-menu.js +23 -0
- package/dist/shadcn/user-menu.js.map +1 -0
- package/dist/signing.d.ts +43 -0
- package/dist/signing.js +82 -0
- package/dist/signing.js.map +1 -0
- package/dist/tenant.d.ts +12 -0
- package/dist/tenant.js +29 -0
- package/dist/tenant.js.map +1 -0
- package/dist/uploads.d.ts +32 -0
- package/dist/uploads.js +28 -0
- package/dist/uploads.js.map +1 -0
- package/package.json +74 -4
package/dist/memory.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rows held in memory, searched, filtered, sorted and paged the way a database
|
|
3
|
+
* adapter would. For a prototype, a demo or a test: the rows last as long as
|
|
4
|
+
* the process.
|
|
5
|
+
*/
|
|
6
|
+
export function memorySource(rows, key, make) {
|
|
7
|
+
let next = rows.length + 1;
|
|
8
|
+
const get = (row, name) => row[name];
|
|
9
|
+
const at = (id) => rows.findIndex((row) => String(get(row, key)) === id);
|
|
10
|
+
return {
|
|
11
|
+
async list({ q, sort, dir, page, perPage, filters, searchable }) {
|
|
12
|
+
let found = rows.filter((row) => Object.entries(filters).every(([name, value]) => String(get(row, name)) === value));
|
|
13
|
+
if (q) {
|
|
14
|
+
const needle = q.toLowerCase();
|
|
15
|
+
found = found.filter((row) => searchable.some((name) => String(get(row, name) ?? '').toLowerCase().includes(needle)));
|
|
16
|
+
}
|
|
17
|
+
if (sort) {
|
|
18
|
+
found = [...found].sort((a, b) => {
|
|
19
|
+
const [x, y] = [get(a, sort), get(b, sort)];
|
|
20
|
+
const order = typeof x === 'number' && typeof y === 'number' ? x - y : String(x ?? '').localeCompare(String(y ?? ''));
|
|
21
|
+
return dir === 'desc' ? -order : order;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (perPage === null)
|
|
25
|
+
return { rows: found, total: found.length };
|
|
26
|
+
return { rows: found.slice((page - 1) * perPage, page * perPage), total: found.length };
|
|
27
|
+
},
|
|
28
|
+
async find(id) {
|
|
29
|
+
return rows[at(id)] ?? null;
|
|
30
|
+
},
|
|
31
|
+
async create(values) {
|
|
32
|
+
const row = make(next++, values);
|
|
33
|
+
rows.push(row);
|
|
34
|
+
return row;
|
|
35
|
+
},
|
|
36
|
+
async update(id, values) {
|
|
37
|
+
const index = at(id);
|
|
38
|
+
if (index === -1)
|
|
39
|
+
throw new Error(`[admin] no row ${id} to update`);
|
|
40
|
+
rows[index] = { ...rows[index], ...values };
|
|
41
|
+
return rows[index];
|
|
42
|
+
},
|
|
43
|
+
async delete(id) {
|
|
44
|
+
const index = at(id);
|
|
45
|
+
if (index !== -1)
|
|
46
|
+
rows.splice(index, 1);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DataSource } from './resource.js'\n\n/**\n * Rows held in memory, searched, filtered, sorted and paged the way a database\n * adapter would. For a prototype, a demo or a test: the rows last as long as\n * the process.\n */\nexport function memorySource<Row extends object, Values>(\n rows: Row[],\n key: keyof Row & string,\n make: (id: number, values: Values) => Row,\n): DataSource<Row, Values> {\n let next = rows.length + 1\n const get = (row: Row, name: string) => (row as Record<string, unknown>)[name]\n const at = (id: string) => rows.findIndex((row) => String(get(row, key)) === id)\n\n return {\n async list({ q, sort, dir, page, perPage, filters, searchable }) {\n let found = rows.filter((row) => Object.entries(filters).every(([name, value]) => String(get(row, name)) === value))\n\n if (q) {\n const needle = q.toLowerCase()\n\n found = found.filter((row) => searchable.some((name) => String(get(row, name) ?? '').toLowerCase().includes(needle)))\n }\n\n if (sort) {\n found = [...found].sort((a, b) => {\n const [x, y] = [get(a, sort), get(b, sort)]\n const order = typeof x === 'number' && typeof y === 'number' ? x - y : String(x ?? '').localeCompare(String(y ?? ''))\n\n return dir === 'desc' ? -order : order\n })\n }\n\n if (perPage === null) return { rows: found, total: found.length }\n\n return { rows: found.slice((page - 1) * perPage, page * perPage), total: found.length }\n },\n async find(id) {\n return rows[at(id)] ?? null\n },\n async create(values) {\n const row = make(next++, values)\n\n rows.push(row)\n\n return row\n },\n async update(id, values) {\n const index = at(id)\n\n if (index === -1) throw new Error(`[admin] no row ${id} to update`)\n\n rows[index] = { ...rows[index]!, ...values }\n\n return rows[index]!\n },\n async delete(id) {\n const index = at(id)\n\n if (index !== -1) rows.splice(index, 1)\n },\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pages/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA","sourcesContent":["export { definePages } from './pages.js'\nexport type { PageRow, PageValues, Pages, PagesDefinition } from './pages.js'\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
+
import type { GenerateMetadata } from '@rsc-kit/core/metadata';
|
|
3
|
+
import type { StandardSchemaV1 } from '@rsc-kit/core/standardSchema';
|
|
4
|
+
import type { Block, BlockValue } from '../blocks.js';
|
|
5
|
+
import type { DataSource } from '../resource.js';
|
|
6
|
+
/** A page as it is stored. */
|
|
7
|
+
export interface PageRow {
|
|
8
|
+
id: string | number;
|
|
9
|
+
title: string;
|
|
10
|
+
/** Its address without the leading slash: about, pricing/teams. */
|
|
11
|
+
slug: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
published: boolean;
|
|
14
|
+
blocks: BlockValue[];
|
|
15
|
+
}
|
|
16
|
+
/** What an editor saves. */
|
|
17
|
+
export type PageValues = Omit<PageRow, 'id'>;
|
|
18
|
+
export interface PagesDefinition {
|
|
19
|
+
/** The resource's name in the panel, and its url segment there. pages by default. */
|
|
20
|
+
name?: string;
|
|
21
|
+
/** The kinds of block a page is built from, in the order an editor is offered them. */
|
|
22
|
+
blocks: Block<any>[];
|
|
23
|
+
/**
|
|
24
|
+
* Where pages are kept. Its list() is also how a page is found by its
|
|
25
|
+
* address: it is asked for filters: { slug }, and must honour it.
|
|
26
|
+
*/
|
|
27
|
+
source: DataSource<PageRow, PageValues>;
|
|
28
|
+
/** The slug served at the catch-all's root - the site's home page. home by default. */
|
|
29
|
+
home?: string;
|
|
30
|
+
/** The sidebar's icon in the panel. */
|
|
31
|
+
icon?: ComponentType;
|
|
32
|
+
/** What shows while a page is looked up. Nothing by default. */
|
|
33
|
+
fallback?: ReactNode;
|
|
34
|
+
/**
|
|
35
|
+
* Seconds a page, once looked up, is served without asking the source
|
|
36
|
+
* again: 60 by default, false to ask on every request. A save in the panel
|
|
37
|
+
* is live on the next visit either way; this bounds a change made around
|
|
38
|
+
* the panel - straight in the database - and one made on another instance
|
|
39
|
+
* when versions are not shared.
|
|
40
|
+
*/
|
|
41
|
+
revalidate?: number | false;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Pages an editor builds in the panel from the app's blocks - a landing
|
|
45
|
+
* page, an about page, a pricing page - served by one catch-all route:
|
|
46
|
+
*
|
|
47
|
+
* // src/app/[...path]/page.tsx
|
|
48
|
+
* export default site.Page
|
|
49
|
+
* export const generateMetadata = site.generateMetadata
|
|
50
|
+
*
|
|
51
|
+
* // src/app/[...path]/middleware.ts
|
|
52
|
+
* export default site.middleware
|
|
53
|
+
*
|
|
54
|
+
* // the panel
|
|
55
|
+
* resources: [site.resource, ...]
|
|
56
|
+
*
|
|
57
|
+
* A page is looked up once and kept for `revalidate` seconds (60), in the
|
|
58
|
+
* store installCacheStore() names - memory unless the app says otherwise. A
|
|
59
|
+
* save in the panel is live on the next visit all the same; nothing is
|
|
60
|
+
* rebuilt.
|
|
61
|
+
*/
|
|
62
|
+
export declare function definePages(definition: PagesDefinition): {
|
|
63
|
+
resource: import("../resource.js").Resource<PageRow, StandardSchemaV1<Record<string, unknown>, PageValues> & {
|
|
64
|
+
'~standard': {
|
|
65
|
+
jsonSchema: {
|
|
66
|
+
input: () => unknown;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
}>;
|
|
70
|
+
Page: ({ params }: {
|
|
71
|
+
params: Promise<Record<string, string | undefined>>;
|
|
72
|
+
}) => import("react").JSX.Element;
|
|
73
|
+
generateMetadata: GenerateMetadata<Record<string, string | undefined>>;
|
|
74
|
+
middleware: (params: Record<string, string | undefined>) => Promise<void>;
|
|
75
|
+
find: (path: string) => Promise<PageRow | null>;
|
|
76
|
+
blocks: Block<any>[];
|
|
77
|
+
};
|
|
78
|
+
export type Pages = ReturnType<typeof definePages>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense } from 'react';
|
|
3
|
+
import { cache } from '@rsc-kit/core/cache';
|
|
4
|
+
import { fieldErrors } from '@rsc-kit/core/action';
|
|
5
|
+
import { changed } from '@rsc-kit/core/changed';
|
|
6
|
+
import { notFound } from '@rsc-kit/core/not-found';
|
|
7
|
+
import { Blocks } from '../blocks.js';
|
|
8
|
+
import { defineResource } from '../resource.js';
|
|
9
|
+
const SLUG = /^[a-z0-9-]+(\/[a-z0-9-]+)*$/;
|
|
10
|
+
const issue = (field, message) => ({ message, path: [field] });
|
|
11
|
+
/**
|
|
12
|
+
* A page's schema, written out rather than taken from a validation library,
|
|
13
|
+
* which press does not depend on. It says what it is as JSON Schema too, so
|
|
14
|
+
* the form gets the same browser checks a zod schema's would.
|
|
15
|
+
*/
|
|
16
|
+
const pageSchema = {
|
|
17
|
+
'~standard': {
|
|
18
|
+
version: 1,
|
|
19
|
+
vendor: '@rsc-kit/press',
|
|
20
|
+
validate(input) {
|
|
21
|
+
const value = (input ?? {});
|
|
22
|
+
const issues = [];
|
|
23
|
+
const title = typeof value.title === 'string' ? value.title.trim() : '';
|
|
24
|
+
const slug = typeof value.slug === 'string' ? value.slug : '';
|
|
25
|
+
const description = typeof value.description === 'string' ? value.description : undefined;
|
|
26
|
+
if (title === '')
|
|
27
|
+
issues.push(issue('title', 'A page needs a title.'));
|
|
28
|
+
else if (title.length > 200)
|
|
29
|
+
issues.push(issue('title', 'Keep the title under 200 characters.'));
|
|
30
|
+
if (!SLUG.test(slug))
|
|
31
|
+
issues.push(issue('slug', 'Lowercase letters, digits and dashes, with / between parts: pricing/teams.'));
|
|
32
|
+
if (description !== undefined && description.length > 300)
|
|
33
|
+
issues.push(issue('description', 'Keep the description under 300 characters.'));
|
|
34
|
+
if (!Array.isArray(value.blocks))
|
|
35
|
+
issues.push(issue('blocks', 'A page is a list of blocks.'));
|
|
36
|
+
if (issues.length > 0)
|
|
37
|
+
return { issues };
|
|
38
|
+
return {
|
|
39
|
+
value: {
|
|
40
|
+
title,
|
|
41
|
+
slug,
|
|
42
|
+
...(description ? { description } : {}),
|
|
43
|
+
published: value.published === true,
|
|
44
|
+
blocks: value.blocks,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
jsonSchema: {
|
|
49
|
+
input: () => ({
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
title: { type: 'string', minLength: 1, maxLength: 200 },
|
|
53
|
+
slug: { type: 'string', pattern: SLUG.source },
|
|
54
|
+
description: { type: 'string', maxLength: 300 },
|
|
55
|
+
published: { type: 'boolean' },
|
|
56
|
+
blocks: { type: 'array' },
|
|
57
|
+
},
|
|
58
|
+
required: ['title', 'slug', 'published', 'blocks'],
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Pages an editor builds in the panel from the app's blocks - a landing
|
|
65
|
+
* page, an about page, a pricing page - served by one catch-all route:
|
|
66
|
+
*
|
|
67
|
+
* // src/app/[...path]/page.tsx
|
|
68
|
+
* export default site.Page
|
|
69
|
+
* export const generateMetadata = site.generateMetadata
|
|
70
|
+
*
|
|
71
|
+
* // src/app/[...path]/middleware.ts
|
|
72
|
+
* export default site.middleware
|
|
73
|
+
*
|
|
74
|
+
* // the panel
|
|
75
|
+
* resources: [site.resource, ...]
|
|
76
|
+
*
|
|
77
|
+
* A page is looked up once and kept for `revalidate` seconds (60), in the
|
|
78
|
+
* store installCacheStore() names - memory unless the app says otherwise. A
|
|
79
|
+
* save in the panel is live on the next visit all the same; nothing is
|
|
80
|
+
* rebuilt.
|
|
81
|
+
*/
|
|
82
|
+
export function definePages(definition) {
|
|
83
|
+
const home = definition.home ?? 'home';
|
|
84
|
+
const blocks = definition.blocks;
|
|
85
|
+
const source = definition.source;
|
|
86
|
+
const everywhere = { tenant: null };
|
|
87
|
+
if (!SLUG.test(home))
|
|
88
|
+
throw new Error(`[press] home is a slug: ${JSON.stringify(home)}`);
|
|
89
|
+
for (const [index, block] of blocks.entries()) {
|
|
90
|
+
if (blocks.findIndex((candidate) => candidate.name === block.name) !== index)
|
|
91
|
+
throw new Error(`[press] two blocks are named ${block.name}`);
|
|
92
|
+
}
|
|
93
|
+
const bySlug = async (slug) => (await source.list({ dir: 'asc', page: 1, perPage: 1, filters: { slug }, searchable: [] }, everywhere)).rows.find((row) => row.slug === slug) ?? null;
|
|
94
|
+
// An address is one page's. Checked here, so a memory or rpc source need
|
|
95
|
+
// not, and refused under the field like the schema's own refusals.
|
|
96
|
+
const unique = async (slug, id) => {
|
|
97
|
+
const taken = await bySlug(slug);
|
|
98
|
+
if (taken && String(taken.id) !== id)
|
|
99
|
+
fieldErrors({ slug: `${taken.title} is already at /${slug}.` });
|
|
100
|
+
};
|
|
101
|
+
const name = definition.name ?? 'pages';
|
|
102
|
+
// One name for every page: a save can move a page's address or unpublish
|
|
103
|
+
// it, so it is not only its own lookup that goes stale.
|
|
104
|
+
const tag = `press:${name}`;
|
|
105
|
+
/** Run a write, then say pages changed, so the next lookup asks the source. */
|
|
106
|
+
const saving = async (write) => {
|
|
107
|
+
const done = await write;
|
|
108
|
+
await changed(tag);
|
|
109
|
+
return done;
|
|
110
|
+
};
|
|
111
|
+
const guarded = {
|
|
112
|
+
list: (query, context) => source.list(query, context),
|
|
113
|
+
find: (id, context) => source.find(id, context),
|
|
114
|
+
create: async (values, context) => {
|
|
115
|
+
await unique(values.slug, null);
|
|
116
|
+
return saving(source.create(values, context));
|
|
117
|
+
},
|
|
118
|
+
update: async (id, values, context) => {
|
|
119
|
+
await unique(values.slug, id);
|
|
120
|
+
return saving(source.update(id, values, context));
|
|
121
|
+
},
|
|
122
|
+
delete: (id, context) => saving(source.delete(id, context)),
|
|
123
|
+
};
|
|
124
|
+
const resource = defineResource()({
|
|
125
|
+
name,
|
|
126
|
+
key: 'id',
|
|
127
|
+
title: 'title',
|
|
128
|
+
...(definition.icon ? { icon: definition.icon } : {}),
|
|
129
|
+
description: 'Built from blocks, and live at their address once published.',
|
|
130
|
+
source: guarded,
|
|
131
|
+
table: (col) => [col.text('title').searchable().sortable(), col.text('slug').searchable().sortable(), col.boolean('published').sortable()],
|
|
132
|
+
form: pageSchema,
|
|
133
|
+
fields: {
|
|
134
|
+
slug: { description: `Its address, without the leading slash. ${home} is the home page.` },
|
|
135
|
+
description: { type: 'textarea', description: 'For search engines and link previews.' },
|
|
136
|
+
blocks: { label: 'Content', blocks },
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const lookup = async (path) => {
|
|
140
|
+
// The home page lives at the root, not also at /home.
|
|
141
|
+
if (path === home)
|
|
142
|
+
return null;
|
|
143
|
+
const page = await bySlug(path === '' ? home : path);
|
|
144
|
+
return page?.published ? page : null;
|
|
145
|
+
};
|
|
146
|
+
const revalidate = definition.revalidate ?? 60;
|
|
147
|
+
if (revalidate !== false && !(revalidate > 0))
|
|
148
|
+
throw new Error(`[press] revalidate is seconds above 0, or false: ${revalidate}`);
|
|
149
|
+
/**
|
|
150
|
+
* The page at a catch-all's path, published, or null. Once per request
|
|
151
|
+
* however many ask, and kept between requests for `revalidate` seconds or
|
|
152
|
+
* until a page is saved - so a visit does not ask the source at all.
|
|
153
|
+
*/
|
|
154
|
+
const find = revalidate === false ? cache(lookup) : cache(lookup, { key: `press.pages.${name}`, revalidate, tags: [tag] });
|
|
155
|
+
const pathOf = async (params) => String(Object.values(await params)[0] ?? '');
|
|
156
|
+
async function Content({ params }) {
|
|
157
|
+
const page = await find(await pathOf(params));
|
|
158
|
+
if (!page)
|
|
159
|
+
notFound();
|
|
160
|
+
return _jsx(Blocks, { blocks: blocks, value: page.blocks });
|
|
161
|
+
}
|
|
162
|
+
/** The catch-all route's page. Its param may have any name. */
|
|
163
|
+
function Page({ params }) {
|
|
164
|
+
return (_jsx(Suspense, { fallback: definition.fallback ?? null, children: _jsx(Content, { params: params }) }));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The same lookup before anything is sent, so an address with no page is
|
|
168
|
+
* a real 404. The page's own notFound() is under the shell, which a stored
|
|
169
|
+
* route sends with a 200 before the lookup has answered.
|
|
170
|
+
*/
|
|
171
|
+
async function middleware(params) {
|
|
172
|
+
if (!(await find(String(Object.values(params ?? {})[0] ?? ''))))
|
|
173
|
+
notFound();
|
|
174
|
+
}
|
|
175
|
+
const generateMetadata = async ({ params }) => {
|
|
176
|
+
const page = await find(await pathOf(params));
|
|
177
|
+
if (!page)
|
|
178
|
+
return {};
|
|
179
|
+
return { title: page.title, ...(page.description ? { description: page.description } : {}) };
|
|
180
|
+
};
|
|
181
|
+
return { resource, Page, generateMetadata, middleware, find, blocks };
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=pages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../../src/pages/pages.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Suspense } from 'react'\nimport { cache } from '@rsc-kit/core/cache'\nimport type { ComponentType, ReactNode } from 'react'\nimport { fieldErrors } from '@rsc-kit/core/action'\nimport { changed } from '@rsc-kit/core/changed'\nimport type { GenerateMetadata, Metadata } from '@rsc-kit/core/metadata'\nimport { notFound } from '@rsc-kit/core/not-found'\nimport type { StandardSchemaV1 } from '@rsc-kit/core/standardSchema'\nimport { Blocks } from '../blocks.js'\nimport type { Block, BlockValue } from '../blocks.js'\nimport { defineResource } from '../resource.js'\nimport type { DataSource } from '../resource.js'\n\n/** A page as it is stored. */\nexport interface PageRow {\n id: string | number\n title: string\n /** Its address without the leading slash: about, pricing/teams. */\n slug: string\n description?: string\n published: boolean\n blocks: BlockValue[]\n}\n\n/** What an editor saves. */\nexport type PageValues = Omit<PageRow, 'id'>\n\nexport interface PagesDefinition {\n /** The resource's name in the panel, and its url segment there. pages by default. */\n name?: string\n /** The kinds of block a page is built from, in the order an editor is offered them. */\n blocks: Block<any>[]\n /**\n * Where pages are kept. Its list() is also how a page is found by its\n * address: it is asked for filters: { slug }, and must honour it.\n */\n source: DataSource<PageRow, PageValues>\n /** The slug served at the catch-all's root - the site's home page. home by default. */\n home?: string\n /** The sidebar's icon in the panel. */\n icon?: ComponentType\n /** What shows while a page is looked up. Nothing by default. */\n fallback?: ReactNode\n /**\n * Seconds a page, once looked up, is served without asking the source\n * again: 60 by default, false to ask on every request. A save in the panel\n * is live on the next visit either way; this bounds a change made around\n * the panel - straight in the database - and one made on another instance\n * when versions are not shared.\n */\n revalidate?: number | false\n}\n\nconst SLUG = /^[a-z0-9-]+(\\/[a-z0-9-]+)*$/\n\ntype Issue = { message: string; path: string[] }\n\nconst issue = (field: string, message: string): Issue => ({ message, path: [field] })\n\n/**\n * A page's schema, written out rather than taken from a validation library,\n * which press does not depend on. It says what it is as JSON Schema too, so\n * the form gets the same browser checks a zod schema's would.\n */\nconst pageSchema: StandardSchemaV1<Record<string, unknown>, PageValues> & {\n '~standard': { jsonSchema: { input: () => unknown } }\n} = {\n '~standard': {\n version: 1,\n vendor: '@rsc-kit/press',\n validate(input) {\n const value = (input ?? {}) as Record<string, unknown>\n const issues: Issue[] = []\n const title = typeof value.title === 'string' ? value.title.trim() : ''\n const slug = typeof value.slug === 'string' ? value.slug : ''\n const description = typeof value.description === 'string' ? value.description : undefined\n\n if (title === '') issues.push(issue('title', 'A page needs a title.'))\n else if (title.length > 200) issues.push(issue('title', 'Keep the title under 200 characters.'))\n if (!SLUG.test(slug)) issues.push(issue('slug', 'Lowercase letters, digits and dashes, with / between parts: pricing/teams.'))\n if (description !== undefined && description.length > 300) issues.push(issue('description', 'Keep the description under 300 characters.'))\n if (!Array.isArray(value.blocks)) issues.push(issue('blocks', 'A page is a list of blocks.'))\n\n if (issues.length > 0) return { issues }\n\n return {\n value: {\n title,\n slug,\n ...(description ? { description } : {}),\n published: value.published === true,\n blocks: value.blocks as BlockValue[],\n },\n }\n },\n jsonSchema: {\n input: () => ({\n type: 'object',\n properties: {\n title: { type: 'string', minLength: 1, maxLength: 200 },\n slug: { type: 'string', pattern: SLUG.source },\n description: { type: 'string', maxLength: 300 },\n published: { type: 'boolean' },\n blocks: { type: 'array' },\n },\n required: ['title', 'slug', 'published', 'blocks'],\n }),\n },\n },\n}\n\n/**\n * Pages an editor builds in the panel from the app's blocks - a landing\n * page, an about page, a pricing page - served by one catch-all route:\n *\n * // src/app/[...path]/page.tsx\n * export default site.Page\n * export const generateMetadata = site.generateMetadata\n *\n * // src/app/[...path]/middleware.ts\n * export default site.middleware\n *\n * // the panel\n * resources: [site.resource, ...]\n *\n * A page is looked up once and kept for `revalidate` seconds (60), in the\n * store installCacheStore() names - memory unless the app says otherwise. A\n * save in the panel is live on the next visit all the same; nothing is\n * rebuilt.\n */\nexport function definePages(definition: PagesDefinition) {\n const home = definition.home ?? 'home'\n const blocks = definition.blocks\n const source = definition.source\n const everywhere = { tenant: null }\n\n if (!SLUG.test(home)) throw new Error(`[press] home is a slug: ${JSON.stringify(home)}`)\n\n for (const [index, block] of blocks.entries()) {\n if (blocks.findIndex((candidate) => candidate.name === block.name) !== index) throw new Error(`[press] two blocks are named ${block.name}`)\n }\n\n const bySlug = async (slug: string): Promise<PageRow | null> =>\n (await source.list({ dir: 'asc', page: 1, perPage: 1, filters: { slug }, searchable: [] }, everywhere)).rows.find((row) => row.slug === slug) ?? null\n\n // An address is one page's. Checked here, so a memory or rpc source need\n // not, and refused under the field like the schema's own refusals.\n const unique = async (slug: string, id: string | null) => {\n const taken = await bySlug(slug)\n\n if (taken && String(taken.id) !== id) fieldErrors({ slug: `${taken.title} is already at /${slug}.` })\n }\n\n const name = definition.name ?? 'pages'\n // One name for every page: a save can move a page's address or unpublish\n // it, so it is not only its own lookup that goes stale.\n const tag = `press:${name}`\n\n /** Run a write, then say pages changed, so the next lookup asks the source. */\n const saving = async <T,>(write: Promise<T>): Promise<T> => {\n const done = await write\n\n await changed(tag)\n\n return done\n }\n\n const guarded: DataSource<PageRow, PageValues> = {\n list: (query, context) => source.list(query, context),\n find: (id, context) => source.find(id, context),\n create: async (values, context) => {\n await unique(values.slug, null)\n\n return saving(source.create(values, context))\n },\n update: async (id, values, context) => {\n await unique(values.slug, id)\n\n return saving(source.update(id, values, context))\n },\n delete: (id, context) => saving(source.delete(id, context)),\n }\n\n const resource = defineResource<PageRow>()({\n name,\n key: 'id',\n title: 'title',\n ...(definition.icon ? { icon: definition.icon } : {}),\n description: 'Built from blocks, and live at their address once published.',\n source: guarded,\n table: (col) => [col.text('title').searchable().sortable(), col.text('slug').searchable().sortable(), col.boolean('published').sortable()],\n form: pageSchema,\n fields: {\n slug: { description: `Its address, without the leading slash. ${home} is the home page.` },\n description: { type: 'textarea', description: 'For search engines and link previews.' },\n blocks: { label: 'Content', blocks },\n },\n })\n\n const lookup = async (path: string): Promise<PageRow | null> => {\n // The home page lives at the root, not also at /home.\n if (path === home) return null\n\n const page = await bySlug(path === '' ? home : path)\n\n return page?.published ? page : null\n }\n\n const revalidate = definition.revalidate ?? 60\n\n if (revalidate !== false && !(revalidate > 0)) throw new Error(`[press] revalidate is seconds above 0, or false: ${revalidate}`)\n\n /**\n * The page at a catch-all's path, published, or null. Once per request\n * however many ask, and kept between requests for `revalidate` seconds or\n * until a page is saved - so a visit does not ask the source at all.\n */\n const find = revalidate === false ? cache(lookup) : cache(lookup, { key: `press.pages.${name}`, revalidate, tags: [tag] })\n\n const pathOf = async (params: Promise<Record<string, string | undefined>>) => String(Object.values(await params)[0] ?? '')\n\n async function Content({ params }: { params: Promise<Record<string, string | undefined>> }) {\n const page = await find(await pathOf(params))\n\n if (!page) notFound()\n\n return <Blocks blocks={blocks} value={page.blocks} />\n }\n\n /** The catch-all route's page. Its param may have any name. */\n function Page({ params }: { params: Promise<Record<string, string | undefined>> }) {\n return (\n <Suspense fallback={definition.fallback ?? null}>\n <Content params={params} />\n </Suspense>\n )\n }\n\n /**\n * The same lookup before anything is sent, so an address with no page is\n * a real 404. The page's own notFound() is under the shell, which a stored\n * route sends with a 200 before the lookup has answered.\n */\n async function middleware(params: Record<string, string | undefined>) {\n if (!(await find(String(Object.values(params ?? {})[0] ?? '')))) notFound()\n }\n\n const generateMetadata: GenerateMetadata<Record<string, string | undefined>> = async ({ params }) => {\n const page = await find(await pathOf(params))\n\n if (!page) return {}\n\n return { title: page.title, ...(page.description ? { description: page.description } : {}) } satisfies Metadata\n }\n\n return { resource, Page, generateMetadata, middleware, find, blocks }\n}\n\nexport type Pages = ReturnType<typeof definePages>\n"]}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { Resource, Tenant } from './resource.js';
|
|
3
|
+
/** What happened to a record, told to every plugin after it happened. */
|
|
4
|
+
export interface RecordChange {
|
|
5
|
+
/** The panel's path: '/admin'. */
|
|
6
|
+
panel: string;
|
|
7
|
+
resource: string;
|
|
8
|
+
id: string;
|
|
9
|
+
change: 'created' | 'updated' | 'deleted';
|
|
10
|
+
/** What was saved, as the schema parsed it. Absent for a delete. */
|
|
11
|
+
values?: Record<string, unknown>;
|
|
12
|
+
/** Whose record it was, in a panel with tenants. */
|
|
13
|
+
tenant: Tenant | null;
|
|
14
|
+
}
|
|
15
|
+
export interface Widget {
|
|
16
|
+
title: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
component: ComponentType<{
|
|
19
|
+
tenant: Tenant | null;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A plugin adds to a panel; it never changes the panel's own code. Everything
|
|
24
|
+
* here is what an app could write itself - a plugin is the same thing in a
|
|
25
|
+
* package, so it can be installed, versioned and sold.
|
|
26
|
+
*/
|
|
27
|
+
export interface PanelPlugin {
|
|
28
|
+
/** Unique within a panel; a package's name is a good one. */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Resources the panel shows as if the app had defined them. */
|
|
31
|
+
resources?: Resource[];
|
|
32
|
+
/**
|
|
33
|
+
* Server components rendered on the dashboard, each in a card, after the
|
|
34
|
+
* counts. A widget is told the tenant, so it shows that tenant's numbers.
|
|
35
|
+
*/
|
|
36
|
+
widgets?: Widget[];
|
|
37
|
+
/**
|
|
38
|
+
* Called after a save or a delete has succeeded. A plugin that throws is
|
|
39
|
+
* logged and does not undo the change or fail the request: the record is
|
|
40
|
+
* already written, and the person saving it is not the plugin's audience.
|
|
41
|
+
*/
|
|
42
|
+
onChange?: (change: RecordChange) => void | Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Typed, and nothing more: a plugin is a plain object. Export a function that
|
|
46
|
+
* takes the plugin's options and returns one, so an app writes
|
|
47
|
+
* plugins: [auditLog({ keep: 500 })].
|
|
48
|
+
*/
|
|
49
|
+
export declare function definePlugin<Plugin extends PanelPlugin>(plugin: Plugin): Plugin;
|
|
50
|
+
/** Every plugin's onChange, in the order the plugins were given. */
|
|
51
|
+
export declare function tell(plugins: PanelPlugin[], change: RecordChange): Promise<void>;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed, and nothing more: a plugin is a plain object. Export a function that
|
|
3
|
+
* takes the plugin's options and returns one, so an app writes
|
|
4
|
+
* plugins: [auditLog({ keep: 500 })].
|
|
5
|
+
*/
|
|
6
|
+
export function definePlugin(plugin) {
|
|
7
|
+
return plugin;
|
|
8
|
+
}
|
|
9
|
+
/** Every plugin's onChange, in the order the plugins were given. */
|
|
10
|
+
export async function tell(plugins, change) {
|
|
11
|
+
for (const plugin of plugins) {
|
|
12
|
+
try {
|
|
13
|
+
await plugin.onChange?.(change);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
console.error(`[admin] the plugin ${plugin.name} failed on ${change.change} of ${change.resource} ${change.id}`, error);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AA6CA;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAA6B,MAAc;IACrE,OAAO,MAAM,CAAA;AACf,CAAC;AAED,oEAAoE;AACpE,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,OAAsB,EAAE,MAAoB;IACrE,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAA;QACjC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,sBAAsB,MAAM,CAAC,IAAI,cAAc,MAAM,CAAC,MAAM,OAAO,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,CAAA;QACzH,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["import type { ComponentType } from 'react'\nimport type { Resource, Tenant } from './resource.js'\n\n/** What happened to a record, told to every plugin after it happened. */\nexport interface RecordChange {\n /** The panel's path: '/admin'. */\n panel: string\n resource: string\n id: string\n change: 'created' | 'updated' | 'deleted'\n /** What was saved, as the schema parsed it. Absent for a delete. */\n values?: Record<string, unknown>\n /** Whose record it was, in a panel with tenants. */\n tenant: Tenant | null\n}\n\nexport interface Widget {\n title: string\n description?: string\n component: ComponentType<{ tenant: Tenant | null }>\n}\n\n/**\n * A plugin adds to a panel; it never changes the panel's own code. Everything\n * here is what an app could write itself - a plugin is the same thing in a\n * package, so it can be installed, versioned and sold.\n */\nexport interface PanelPlugin {\n /** Unique within a panel; a package's name is a good one. */\n name: string\n /** Resources the panel shows as if the app had defined them. */\n resources?: Resource[]\n /**\n * Server components rendered on the dashboard, each in a card, after the\n * counts. A widget is told the tenant, so it shows that tenant's numbers.\n */\n widgets?: Widget[]\n /**\n * Called after a save or a delete has succeeded. A plugin that throws is\n * logged and does not undo the change or fail the request: the record is\n * already written, and the person saving it is not the plugin's audience.\n */\n onChange?: (change: RecordChange) => void | Promise<void>\n}\n\n/**\n * Typed, and nothing more: a plugin is a plain object. Export a function that\n * takes the plugin's options and returns one, so an app writes\n * plugins: [auditLog({ keep: 500 })].\n */\nexport function definePlugin<Plugin extends PanelPlugin>(plugin: Plugin): Plugin {\n return plugin\n}\n\n/** Every plugin's onChange, in the order the plugins were given. */\nexport async function tell(plugins: PanelPlugin[], change: RecordChange): Promise<void> {\n for (const plugin of plugins) {\n try {\n await plugin.onChange?.(change)\n } catch (error) {\n console.error(`[admin] the plugin ${plugin.name} failed on ${change.change} of ${change.resource} ${change.id}`, error)\n }\n }\n}\n"]}
|