@saacms/host-next 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_core-shim.d.ts +20 -0
- package/dist/_core-shim.d.ts.map +1 -0
- package/dist/host-next-adapter.d.ts +36 -0
- package/dist/host-next-adapter.d.ts.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +198 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +44 -0
- package/templates/api-mount.ts.txt +13 -0
- package/templates/preview-route.tsx.txt +14 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local shim of the brand types this package needs from `@saacms/core`.
|
|
3
|
+
*
|
|
4
|
+
* TODO: delete this file and import from `@saacms/core/types` once that path
|
|
5
|
+
* is wired through and the cross-package build graph is hooked up. Keeping a
|
|
6
|
+
* shim now lets the package typecheck in isolation at the v1 scaffold stage.
|
|
7
|
+
*
|
|
8
|
+
* The brand definitions MUST stay structurally compatible with the canonical
|
|
9
|
+
* ones in `packages/core/src/types/ids.ts` so a future direct import is a
|
|
10
|
+
* drop-in replacement.
|
|
11
|
+
*/
|
|
12
|
+
declare const __brand: unique symbol;
|
|
13
|
+
export type PageID = string & {
|
|
14
|
+
readonly [__brand]: "PageID";
|
|
15
|
+
};
|
|
16
|
+
export type BlockSlug = string & {
|
|
17
|
+
readonly [__brand]: "BlockSlug";
|
|
18
|
+
};
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=_core-shim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_core-shim.d.ts","sourceRoot":"","sources":["../src/_core-shim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,CAAC,MAAM,OAAO,EAAE,OAAO,MAAM,CAAA;AAEpC,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAA;CAAE,CAAA;AAC9D,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,WAAW,CAAA;CAAE,CAAA"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js (App Router) implementation of the HostAdapter interface.
|
|
3
|
+
*
|
|
4
|
+
* The Next.js analogue of host-astro (per ADR 0024 — host adapters share one
|
|
5
|
+
* contract). This adapter:
|
|
6
|
+
* - reports its `assetRoot()` for repo-mode media (per ADR 0009),
|
|
7
|
+
* - hands `saacms init` the contents of the API mount file
|
|
8
|
+
* (per ADR 0001) — a Next.js catch-all route handler,
|
|
9
|
+
* - emits `page.tsx` route files into `src/app/<page-url>/page.tsx`
|
|
10
|
+
* (per ADR 0003 + ADR 0007),
|
|
11
|
+
* - emits per-Block preview routes the Puck canvas fetches
|
|
12
|
+
* (per ADR 0004 Mode 2).
|
|
13
|
+
*
|
|
14
|
+
* The route + preview emitters are scaffold-grade: they emit a placeholder
|
|
15
|
+
* React component until the Puck Block tree renderer lands (per ADR 0010).
|
|
16
|
+
*/
|
|
17
|
+
import type { HostAdapter, PuckLayout } from "./types.ts";
|
|
18
|
+
/**
|
|
19
|
+
* Render a stored saacms draft tree to one well-formed JSX fragment for a
|
|
20
|
+
* Next.js App Router RSC page.tsx.
|
|
21
|
+
*
|
|
22
|
+
* Pure: same input → same output, no I/O. `generateRoute` splices the result
|
|
23
|
+
* into the component body. Absent/empty layout → `<main></main>` (never the
|
|
24
|
+
* old placeholder, never a throw). Unknown Block types degrade to a JSX comment
|
|
25
|
+
* so a stale tree still emits its siblings (forward-compat, ADR 0004).
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderBlockTree(layout: PuckLayout | undefined): string;
|
|
28
|
+
export interface NextHostAdapterOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Override the default assets root used for repo-mode media (per ADR 0009).
|
|
31
|
+
* Defaults to "public/" — the conventional Next.js static-asset directory.
|
|
32
|
+
*/
|
|
33
|
+
readonly assetRoot?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function createNextHostAdapter(opts?: NextHostAdapterOptions): HostAdapter;
|
|
36
|
+
//# sourceMappingURL=host-next-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-next-adapter.d.ts","sourceRoot":"","sources":["../src/host-next-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH,OAAO,KAAK,EAIV,WAAW,EAEX,UAAU,EAEX,MAAM,YAAY,CAAA;AA4GnB;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,MAAM,CAMtE;AA2DD,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,wBAAgB,qBAAqB,CACnC,IAAI,GAAE,sBAA2B,GAChC,WAAW,CAsHb"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @saacms/host-next — public surface.
|
|
3
|
+
*
|
|
4
|
+
* The Next.js (App Router) host adapter. Exports the factory
|
|
5
|
+
* `nextHostAdapter()` that returns a `HostAdapter` for use in
|
|
6
|
+
* `saacms.config.ts`:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { nextHostAdapter } from "@saacms/host-next"
|
|
10
|
+
* import { defineConfig } from "@saacms/core"
|
|
11
|
+
*
|
|
12
|
+
* export default defineConfig({
|
|
13
|
+
* host: nextHostAdapter(),
|
|
14
|
+
* // ...
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import { type NextHostAdapterOptions } from "./host-next-adapter.ts";
|
|
19
|
+
import type { HostAdapter } from "./types.ts";
|
|
20
|
+
export { createNextHostAdapter } from "./host-next-adapter.ts";
|
|
21
|
+
export type { NextHostAdapterOptions } from "./host-next-adapter.ts";
|
|
22
|
+
export type { Block, GeneratedFile, GenerateRouteOptions, HostAdapter, Page, } from "./types.ts";
|
|
23
|
+
/**
|
|
24
|
+
* Convenience alias — the canonical name used by user code in
|
|
25
|
+
* `saacms.config.ts`. Equivalent to `createNextHostAdapter(opts)`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function nextHostAdapter(opts?: NextHostAdapterOptions): HostAdapter;
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AACpE,YAAY,EACV,KAAK,EACL,aAAa,EACb,oBAAoB,EACpB,WAAW,EACX,IAAI,GACL,MAAM,YAAY,CAAA;AAEnB;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,CAAC,EAAE,sBAAsB,GAAG,WAAW,CAE1E"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// src/host-next-adapter.ts
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { SCHEME_COOKIE, DARK_MODE_COOKIE } from "@saacms/core";
|
|
6
|
+
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
var TEMPLATES_DIR = resolve(__dirname2, "..", "templates");
|
|
8
|
+
var MOUNT_TEMPLATE = readFileSync(resolve(TEMPLATES_DIR, "api-mount.ts.txt"), "utf8");
|
|
9
|
+
var PREVIEW_TEMPLATE = readFileSync(resolve(TEMPLATES_DIR, "preview-route.tsx.txt"), "utf8");
|
|
10
|
+
var HTML_ATTR_ESCAPE = {
|
|
11
|
+
"&": "&",
|
|
12
|
+
"<": "<",
|
|
13
|
+
">": ">",
|
|
14
|
+
'"': """,
|
|
15
|
+
"'": "'"
|
|
16
|
+
};
|
|
17
|
+
function escapeAttr(value) {
|
|
18
|
+
return value.replace(/[&<>"']/g, (c) => HTML_ATTR_ESCAPE[c] ?? c);
|
|
19
|
+
}
|
|
20
|
+
function isRecord(v) {
|
|
21
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
22
|
+
}
|
|
23
|
+
var CUSTOM_ELEMENT_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/;
|
|
24
|
+
var SAFE_ATTR_NAME = /^[a-zA-Z_][a-zA-Z0-9_.:-]*$/;
|
|
25
|
+
function isBindValue(v) {
|
|
26
|
+
return isRecord(v) && v.kind === "bind" && typeof v.path === "string";
|
|
27
|
+
}
|
|
28
|
+
function renderNode(node) {
|
|
29
|
+
if (!isRecord(node))
|
|
30
|
+
return "";
|
|
31
|
+
const slug = typeof node.block === "string" ? node.block : "";
|
|
32
|
+
if (!CUSTOM_ELEMENT_NAME.test(slug)) {
|
|
33
|
+
const raw = String(node.block ?? "");
|
|
34
|
+
const safe = raw.replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ");
|
|
35
|
+
return `{/* saacms: unknown block '${safe}' */}`;
|
|
36
|
+
}
|
|
37
|
+
const props = isRecord(node.props) ? node.props : {};
|
|
38
|
+
const attrParts = [];
|
|
39
|
+
const jsonProps = {};
|
|
40
|
+
for (const [key, value] of Object.entries(props)) {
|
|
41
|
+
if (value === null || value === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
if (isBindValue(value)) {
|
|
44
|
+
attrParts.push(`data-saacms-bind="${escapeAttr(value.path)}"`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
48
|
+
if (!SAFE_ATTR_NAME.test(key))
|
|
49
|
+
continue;
|
|
50
|
+
attrParts.push(`${key}="${escapeAttr(String(value))}"`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
jsonProps[key] = value;
|
|
54
|
+
}
|
|
55
|
+
if (Object.keys(jsonProps).length > 0) {
|
|
56
|
+
attrParts.push(`props="${escapeAttr(JSON.stringify(jsonProps))}"`);
|
|
57
|
+
}
|
|
58
|
+
const children = Array.isArray(node.children) ? node.children.map(renderNode).join("") : "";
|
|
59
|
+
const attrs = attrParts.length > 0 ? " " + attrParts.join(" ") : "";
|
|
60
|
+
return `<${slug}${attrs}>${children}</${slug}>`;
|
|
61
|
+
}
|
|
62
|
+
function renderBlockTree(layout) {
|
|
63
|
+
const tree = layout?.tree;
|
|
64
|
+
const nodes = isRecord(tree) && Array.isArray(tree.nodes) ? tree.nodes : [];
|
|
65
|
+
if (nodes.length === 0)
|
|
66
|
+
return "<main></main>";
|
|
67
|
+
return `<main>${nodes.map(renderNode).join("")}</main>`;
|
|
68
|
+
}
|
|
69
|
+
function assertSafeUrl(url) {
|
|
70
|
+
if (url.includes("\\")) {
|
|
71
|
+
throw new Error(`generateRoute: url '${url}' contains a backslash; use forward slashes`);
|
|
72
|
+
}
|
|
73
|
+
if (url.startsWith("/")) {
|
|
74
|
+
throw new Error(`generateRoute: url '${url}' is absolute; pass a path without a leading slash`);
|
|
75
|
+
}
|
|
76
|
+
for (const segment of url.split("/")) {
|
|
77
|
+
if (segment === "..") {
|
|
78
|
+
throw new Error(`generateRoute: url '${url}' contains a '..' segment; refusing to escape src/app`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function parseCookies(header) {
|
|
83
|
+
if (header.length === 0)
|
|
84
|
+
return {};
|
|
85
|
+
return Object.fromEntries(header.split(";").map((pair) => {
|
|
86
|
+
const eqIdx = pair.indexOf("=");
|
|
87
|
+
if (eqIdx === -1)
|
|
88
|
+
return [pair.trim(), ""];
|
|
89
|
+
return [pair.slice(0, eqIdx).trim(), pair.slice(eqIdx + 1).trim()];
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
var themeImpl = {
|
|
93
|
+
resolveActiveScheme(req) {
|
|
94
|
+
const cookies = parseCookies(req.headers.get("cookie") ?? "");
|
|
95
|
+
return {
|
|
96
|
+
schemeId: cookies[SCHEME_COOKIE] ?? null,
|
|
97
|
+
darkMode: cookies[DARK_MODE_COOKIE] === "1"
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
emitThemeStyles(tokens) {
|
|
101
|
+
const props = Object.entries(tokens).map(([k, v]) => ` --${k}: ${v};`).join(`
|
|
102
|
+
`);
|
|
103
|
+
return `<style>:root {
|
|
104
|
+
${props}
|
|
105
|
+
}</style>`;
|
|
106
|
+
},
|
|
107
|
+
emitThemeAttribute(schemeId, darkMode) {
|
|
108
|
+
const dark = darkMode ? ' class="dark"' : "";
|
|
109
|
+
return `data-theme="${schemeId}"${dark}`;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
function createNextHostAdapter(opts = {}) {
|
|
113
|
+
const assetRootPath = opts.assetRoot ?? "public/";
|
|
114
|
+
return {
|
|
115
|
+
name: "next",
|
|
116
|
+
theme: themeImpl,
|
|
117
|
+
assetRoot() {
|
|
118
|
+
return assetRootPath;
|
|
119
|
+
},
|
|
120
|
+
mountTemplate() {
|
|
121
|
+
return MOUNT_TEMPLATE;
|
|
122
|
+
},
|
|
123
|
+
previewRouteTemplate(blockSlug) {
|
|
124
|
+
return PREVIEW_TEMPLATE.replaceAll("${blockSlug}", blockSlug);
|
|
125
|
+
},
|
|
126
|
+
generateRoute(page, options) {
|
|
127
|
+
assertSafeUrl(page.url);
|
|
128
|
+
const effectiveMode = options.forceRenderMode ?? page.renderMode;
|
|
129
|
+
if (effectiveMode !== "static" && effectiveMode !== "isr" && effectiveMode !== "dynamic") {
|
|
130
|
+
throw new Error(`generateRoute: unrecognised render mode '${String(effectiveMode)}'`);
|
|
131
|
+
}
|
|
132
|
+
const filePath = page.url === "" ? "src/app/page.tsx" : `src/app/${page.url}/page.tsx`;
|
|
133
|
+
const headerLines = [
|
|
134
|
+
"// generated by saacms — do not edit",
|
|
135
|
+
`// saacms-page-id: ${page.id}`,
|
|
136
|
+
`// render-mode: ${effectiveMode}`
|
|
137
|
+
];
|
|
138
|
+
if (effectiveMode === "static") {
|
|
139
|
+
headerLines.push(`export const dynamic = "force-static"`);
|
|
140
|
+
} else if (effectiveMode === "isr") {
|
|
141
|
+
headerLines.push("export const revalidate = 60");
|
|
142
|
+
} else {
|
|
143
|
+
headerLines.push(`export const dynamic = "force-dynamic"`);
|
|
144
|
+
}
|
|
145
|
+
const body = renderBlockTree(page.layout);
|
|
146
|
+
let source;
|
|
147
|
+
if (options.theme) {
|
|
148
|
+
const { tokens, schemeId, darkMode } = options.theme;
|
|
149
|
+
const cssContent = `:root {
|
|
150
|
+
` + Object.entries(tokens).map(([k, v]) => ` --${k}: ${v};`).join(`
|
|
151
|
+
`) + `
|
|
152
|
+
}`;
|
|
153
|
+
const darkAttr = darkMode ? ` className="dark"` : "";
|
|
154
|
+
source = headerLines.join(`
|
|
155
|
+
`) + `
|
|
156
|
+
` + `
|
|
157
|
+
` + `export default function Page() {
|
|
158
|
+
` + ` return (
|
|
159
|
+
` + ` <html data-theme="${schemeId}"${darkAttr}>
|
|
160
|
+
` + ` <head>
|
|
161
|
+
` + " <style>{`" + cssContent + "`}</style>\n" + ` </head>
|
|
162
|
+
` + ` <body>
|
|
163
|
+
` + " " + body + `
|
|
164
|
+
` + ` </body>
|
|
165
|
+
` + ` </html>
|
|
166
|
+
` + ` )
|
|
167
|
+
` + `}
|
|
168
|
+
`;
|
|
169
|
+
} else {
|
|
170
|
+
source = headerLines.join(`
|
|
171
|
+
`) + `
|
|
172
|
+
` + `
|
|
173
|
+
` + `export default function Page() {
|
|
174
|
+
` + ` return (
|
|
175
|
+
` + " " + body + `
|
|
176
|
+
` + ` )
|
|
177
|
+
` + `}
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
return { path: filePath, source };
|
|
181
|
+
},
|
|
182
|
+
generatePreviewRoute(block) {
|
|
183
|
+
return {
|
|
184
|
+
path: `src/app/saacms/preview/${block.slug}/page.tsx`,
|
|
185
|
+
source: PREVIEW_TEMPLATE.replaceAll("${blockSlug}", block.slug)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/index.ts
|
|
192
|
+
function nextHostAdapter(opts) {
|
|
193
|
+
return createNextHostAdapter(opts ?? {});
|
|
194
|
+
}
|
|
195
|
+
export {
|
|
196
|
+
nextHostAdapter,
|
|
197
|
+
createNextHostAdapter
|
|
198
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local type definitions for the host-next adapter.
|
|
3
|
+
*
|
|
4
|
+
* TODO: once `@saacms/core` exports a canonical `HostAdapter` interface
|
|
5
|
+
* (per ADR 0020 — Hosting bounded context, Host adapter aggregate root), import
|
|
6
|
+
* it from there and delete the local copy. The shape declared here is the
|
|
7
|
+
* minimum surface area host-next needs to fulfill its responsibilities; the
|
|
8
|
+
* canonical core interface will be a strict superset.
|
|
9
|
+
*/
|
|
10
|
+
import type { BlockSlug, PageID } from "./_core-shim.ts";
|
|
11
|
+
import type { ThemeRenderContract } from "@saacms/core";
|
|
12
|
+
export type { ThemeRenderContract };
|
|
13
|
+
/**
|
|
14
|
+
* One Block instance in a stored saacms draft tree.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors the shape produced by `PatternMapper.puckDataToDraft` in
|
|
17
|
+
* `@saacms/admin`. host-next never imports `@measured/puck` or
|
|
18
|
+
* `@saacms/admin` — the host type boundary stays editor-agnostic (ADR 0004).
|
|
19
|
+
* `children` is additive-optional for nested zones.
|
|
20
|
+
*/
|
|
21
|
+
export interface PuckLayoutNode {
|
|
22
|
+
/** The Block slug, e.g. "saacms-hero" (PatternMapper's `SaacmsNode.block`). */
|
|
23
|
+
readonly block: string;
|
|
24
|
+
/** Stable node id (PatternMapper hoists this out of props). */
|
|
25
|
+
readonly id?: string;
|
|
26
|
+
/** Block instance props (the `id` already stripped by PatternMapper). */
|
|
27
|
+
readonly props?: Readonly<Record<string, unknown>>;
|
|
28
|
+
/** Nested child nodes for zoned Blocks; recursed in tree order. */
|
|
29
|
+
readonly children?: ReadonlyArray<PuckLayoutNode>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Stored Puck Block tree JSON — the saacms draft tree. Opaque to the type
|
|
33
|
+
* system, validated structurally at render time. Mirrors the exact shape
|
|
34
|
+
* produced by `PatternMapper.puckDataToDraft`.
|
|
35
|
+
*/
|
|
36
|
+
export interface PuckLayout {
|
|
37
|
+
readonly tree: {
|
|
38
|
+
readonly root?: {
|
|
39
|
+
readonly props?: Readonly<Record<string, unknown>>;
|
|
40
|
+
};
|
|
41
|
+
readonly nodes: ReadonlyArray<PuckLayoutNode>;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The Page projection passed to a host adapter's route emitter.
|
|
46
|
+
*
|
|
47
|
+
* TODO: replace with the canonical Page aggregate from `@saacms/core/types`
|
|
48
|
+
* once it lands.
|
|
49
|
+
*/
|
|
50
|
+
export interface Page {
|
|
51
|
+
readonly id: PageID;
|
|
52
|
+
/** URL path WITHOUT a leading slash, e.g. "about" or "blog/[slug]". */
|
|
53
|
+
readonly url: string;
|
|
54
|
+
/** Render mode (per ADR 0007). v1 ships "static" and "isr". */
|
|
55
|
+
readonly renderMode: "static" | "isr" | "dynamic";
|
|
56
|
+
/** Stored Puck Block tree JSON (the saacms draft tree). Opaque; validated at render. Mirrors core PageDef.layout. */
|
|
57
|
+
readonly layout?: PuckLayout;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The Block projection passed to a host adapter's preview-route emitter.
|
|
61
|
+
*
|
|
62
|
+
* TODO: replace with the canonical Block aggregate from `@saacms/core/types`
|
|
63
|
+
* once it lands.
|
|
64
|
+
*/
|
|
65
|
+
export interface Block {
|
|
66
|
+
readonly slug: BlockSlug;
|
|
67
|
+
/** Per ADR 0004 — "web-component" or "framework-native". */
|
|
68
|
+
readonly authoringMode: "web-component" | "framework-native";
|
|
69
|
+
}
|
|
70
|
+
/** A file the host adapter would write into the user's repo. */
|
|
71
|
+
export interface GeneratedFile {
|
|
72
|
+
/** Path relative to the host project root (e.g. "src/app/about/page.tsx"). */
|
|
73
|
+
readonly path: string;
|
|
74
|
+
/** Verbatim file contents to write. */
|
|
75
|
+
readonly source: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Per-emit options the route emitter uses to decide what to emit.
|
|
79
|
+
*
|
|
80
|
+
* Currently a placeholder; will grow as the static / isr / dynamic emit
|
|
81
|
+
* branches land (per ADR 0007 + ADR 0021).
|
|
82
|
+
*/
|
|
83
|
+
export interface GenerateRouteOptions {
|
|
84
|
+
/** Force a specific render mode override (used by the test suite). */
|
|
85
|
+
readonly forceRenderMode?: "static" | "isr" | "dynamic";
|
|
86
|
+
/** Optional theme options; when provided, the emitted file wraps content in a full HTML shell. */
|
|
87
|
+
readonly theme?: {
|
|
88
|
+
readonly tokens: Record<string, string>;
|
|
89
|
+
readonly schemeId: string;
|
|
90
|
+
readonly darkMode: boolean;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The host-adapter contract.
|
|
95
|
+
*
|
|
96
|
+
* TODO: import from `@saacms/core` once exported there.
|
|
97
|
+
*/
|
|
98
|
+
export interface HostAdapter {
|
|
99
|
+
/** Stable string identifier for this host (e.g. "astro", "next"). */
|
|
100
|
+
readonly name: string;
|
|
101
|
+
/** Repo-relative directory where repo-mode media is committed (ADR 0009). */
|
|
102
|
+
assetRoot(): string;
|
|
103
|
+
/**
|
|
104
|
+
* The verbatim source of the API mount file `saacms init` writes into the
|
|
105
|
+
* user's project (per ADR 0001).
|
|
106
|
+
*/
|
|
107
|
+
mountTemplate(): string;
|
|
108
|
+
/**
|
|
109
|
+
* The verbatim source of a per-Block preview route, with `${blockSlug}`
|
|
110
|
+
* substituted (per ADR 0004 Mode 2).
|
|
111
|
+
*/
|
|
112
|
+
previewRouteTemplate(blockSlug: string): string;
|
|
113
|
+
/** Emit one route file for a Page (per ADR 0003). */
|
|
114
|
+
generateRoute(page: Page, options: GenerateRouteOptions): GeneratedFile;
|
|
115
|
+
/** Emit one preview route for a Block (per ADR 0004 Mode 2). */
|
|
116
|
+
generatePreviewRoute(block: Block): GeneratedFile;
|
|
117
|
+
/** Theme render contract (ADR 0033) — no-FOUC themed SSR. */
|
|
118
|
+
readonly theme: ThemeRenderContract;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AACvD,YAAY,EAAE,mBAAmB,EAAE,CAAA;AAEnC;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;IACpB,yEAAyE;IACzE,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAClD,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,cAAc,CAAC,CAAA;CAClD;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE;QACb,QAAQ,CAAC,IAAI,CAAC,EAAE;YAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;SAAE,CAAA;QACtE,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,cAAc,CAAC,CAAA;KAC9C,CAAA;CACF;AAED;;;;;GAKG;AACH,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,QAAQ,GAAG,KAAK,GAAG,SAAS,CAAA;IACjD,qHAAqH;IACrH,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAA;CAC7B;AAED;;;;;GAKG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,eAAe,GAAG,kBAAkB,CAAA;CAC7D;AAED,gEAAgE;AAChE,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,uCAAuC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,sEAAsE;IACtE,QAAQ,CAAC,eAAe,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,SAAS,CAAA;IACvD,kGAAkG;IAClG,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;QACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;KAC3B,CAAA;CACF;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB,6EAA6E;IAC7E,SAAS,IAAI,MAAM,CAAA;IAEnB;;;OAGG;IACH,aAAa,IAAI,MAAM,CAAA;IAEvB;;;OAGG;IACH,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAA;IAE/C,qDAAqD;IACrD,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAAA;IAEvE,gEAAgE;IAChE,oBAAoB,CAAC,KAAK,EAAE,KAAK,GAAG,aAAa,CAAA;IAEjD,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAA;CACpC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saacms/host-next",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"import": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./templates/*": "./templates/*"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"templates",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc --build",
|
|
20
|
+
"typecheck": "tsc --build --noEmit",
|
|
21
|
+
"prepack": "cp package.json package.json.pack-bak && bun run ../../scripts/prepack-pkg.ts",
|
|
22
|
+
"postpack": "mv package.json.pack-bak package.json"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@saacms/core": "workspace:*"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"next": "^14.0.0 || ^15.0.0 || ^16.0.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"next": {
|
|
35
|
+
"optional": false
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/bun": "latest",
|
|
40
|
+
"typescript": "^5.7.0"
|
|
41
|
+
},
|
|
42
|
+
"main": "./dist/index.js",
|
|
43
|
+
"types": "./dist/index.d.ts"
|
|
44
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// generated by saacms — do not edit
|
|
2
|
+
import { createSaacmsRuntime } from "@saacms/core"
|
|
3
|
+
import config from "../../../../saacms.config"
|
|
4
|
+
|
|
5
|
+
const app = createSaacmsRuntime(config)
|
|
6
|
+
const handler = (req: Request) => app.fetch(req)
|
|
7
|
+
|
|
8
|
+
export const GET = handler
|
|
9
|
+
export const POST = handler
|
|
10
|
+
export const PUT = handler
|
|
11
|
+
export const PATCH = handler
|
|
12
|
+
export const DELETE = handler
|
|
13
|
+
export const runtime = "edge"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// generated by saacms — preview route for ${blockSlug}
|
|
2
|
+
import Block from "../../../../saacms/blocks/${blockSlug}"
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic"
|
|
5
|
+
|
|
6
|
+
export default async function PreviewPage({
|
|
7
|
+
searchParams,
|
|
8
|
+
}: {
|
|
9
|
+
searchParams: Promise<{ props?: string }>
|
|
10
|
+
}) {
|
|
11
|
+
const { props: propsJson } = await searchParams
|
|
12
|
+
const props = JSON.parse(propsJson ?? "{}")
|
|
13
|
+
return <Block {...props} />
|
|
14
|
+
}
|