@saacms/host-astro 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/README.md +44 -0
- package/dist/_core-shim.d.ts +20 -0
- package/dist/_core-shim.d.ts.map +1 -0
- package/dist/_core-shim.js +12 -0
- package/dist/host-astro-adapter.d.ts +35 -0
- package/dist/host-astro-adapter.d.ts.map +1 -0
- package/dist/host-astro-adapter.js +58 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +193 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/package.json +44 -0
- package/templates/api-mount.ts.txt +12 -0
- package/templates/page-route.astro.txt +13 -0
- package/templates/preview-route.astro.txt +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @saacms/host-astro
|
|
2
|
+
|
|
3
|
+
The Astro [host adapter](../../CONTEXT.md#architectural-primitives) for saacms. Knows how to:
|
|
4
|
+
|
|
5
|
+
1. Generate `.astro` route files into `src/pages/<page-url>.astro` for Pages and Page templates (per ADR 0003).
|
|
6
|
+
2. Emit the small mount file at `src/pages/api/saacms/[...slug].ts` that binds saacms's runtime handler into Astro's API surface (per ADR 0001).
|
|
7
|
+
3. Generate per-Block preview routes the Puck canvas fetches at edit time (per ADR 0004 Mode 2).
|
|
8
|
+
|
|
9
|
+
This is the **only host adapter shipped in v1 alpha** (per [ADR 0024](../../docs/adr/0024-v1-alpha-scope-astro-cloudflare.md)). Other hosts (`host-nextjs`, `host-sveltekit`, `host-nuxt`) follow in v1.x.
|
|
10
|
+
|
|
11
|
+
## Reading order (relevant ADRs)
|
|
12
|
+
|
|
13
|
+
- [ADR 0001 — Content compiler + serverless runtime](../../docs/adr/0001-content-compiler-delivery.md)
|
|
14
|
+
- [ADR 0003 — saacms owns the host's route filesystem](../../docs/adr/0003-saacms-owns-routing.md)
|
|
15
|
+
- [ADR 0004 — Block authoring: dual mode + preview-fetch](../../docs/adr/0004-block-authoring-dual-mode-with-preview-fetch.md)
|
|
16
|
+
- [ADR 0007 — Render mode is per-Page-template](../../docs/adr/0007-render-mode-per-template.md)
|
|
17
|
+
- [ADR 0009 — Media as Collection kind (repo vs bucket)](../../docs/adr/0009-media-as-collection-kind.md)
|
|
18
|
+
- [ADR 0021 — Rendering and caching across the coupling spectrum](../../docs/adr/0021-rendering-and-caching-across-coupling-spectrum.md)
|
|
19
|
+
- [ADR 0024 — v1 alpha scope: Astro + Cloudflare](../../docs/adr/0024-v1-alpha-scope-astro-cloudflare.md)
|
|
20
|
+
|
|
21
|
+
## Status
|
|
22
|
+
|
|
23
|
+
**Scaffold.** Interface shape and template files are defined; the route emitter is a stub. Real `.astro` source emission lands alongside the v1.0 alpha milestone.
|
|
24
|
+
|
|
25
|
+
## Why Astro, why now
|
|
26
|
+
|
|
27
|
+
Per ADR 0024 — Astro's "lightest framework that works" philosophy aligns with the saacms performance philosophy (ADR 0022 P1). Astro Server Islands give us PPR-equivalent partial rendering without framework-specific magic. Cloudflare Pages + D1 + R2 + Astro is the cleanest single stack to prove the architecture against before fanning out to Next.js / SvelteKit / Nuxt in v1.x.
|
|
28
|
+
|
|
29
|
+
## Templates
|
|
30
|
+
|
|
31
|
+
Three text templates live under `templates/`. They are intentionally **not** TypeScript — they are emitted verbatim by the saacms compile step into the user's project, where their imports refer to user-side files (`../../../../saacms.config.ts`, `../../../saacms/blocks/*.astro`).
|
|
32
|
+
|
|
33
|
+
- `templates/api-mount.ts.txt` — the file `saacms init` writes to `src/pages/api/saacms/[...slug].ts`.
|
|
34
|
+
- `templates/preview-route.astro.txt` — the per-Block preview route template (ADR 0004 Mode 2).
|
|
35
|
+
- `templates/page-route.astro.txt` — the per-Page route template, emitted by the route emitter.
|
|
36
|
+
|
|
37
|
+
## Known reconciliation TODO
|
|
38
|
+
|
|
39
|
+
The local `HostAdapter` interface in `src/types.ts` diverges slightly from the canonical interface exported by `@saacms/core` (`src/host/index.ts`). This is a v1-scaffold artefact — subagents wrote both in parallel without final cross-check. To do before first real implementation pass:
|
|
40
|
+
|
|
41
|
+
- Adopt `core`'s `GenerateRouteContext` / `GeneratedRoute` / `MountTemplate` shape verbatim
|
|
42
|
+
- Drop `_core-shim.ts` (use `@saacms/core` brand types directly)
|
|
43
|
+
- Rename `assetRoot()` → `assetRoot` (readonly property, per core)
|
|
44
|
+
- Rename `previewRouteTemplate(slug)` → fold into `generatePreviewRoute(ctx)` per core's contract
|
|
@@ -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,12 @@
|
|
|
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
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Astro implementation of the HostAdapter interface.
|
|
3
|
+
*
|
|
4
|
+
* Per ADR 0024, Astro is the only supported host in v1 alpha. This adapter:
|
|
5
|
+
* - reports its `assetRoot()` for repo-mode media (per ADR 0009),
|
|
6
|
+
* - hands `saacms init` the contents of the API mount file
|
|
7
|
+
* (per ADR 0001),
|
|
8
|
+
* - emits `.astro` route files into `src/pages/<page-url>.astro`
|
|
9
|
+
* (per ADR 0003 + ADR 0007),
|
|
10
|
+
* - emits per-Block preview routes the Puck canvas fetches
|
|
11
|
+
* (per ADR 0004 Mode 2).
|
|
12
|
+
*
|
|
13
|
+
* `generateRoute` renders the Page's stored Block tree into real Astro markup
|
|
14
|
+
* (per ADR 0003 + ADR 0004 Mode 1 + ADR 0010); `generatePreviewRoute` remains
|
|
15
|
+
* a scaffold stub at this milestone.
|
|
16
|
+
*/
|
|
17
|
+
import type { HostAdapter, PuckLayout } from "./types.ts";
|
|
18
|
+
/**
|
|
19
|
+
* Render a stored saacms draft tree to one well-formed Astro/HTML fragment.
|
|
20
|
+
*
|
|
21
|
+
* Pure: same input → same output, no I/O. `generateRoute` splices the result
|
|
22
|
+
* into the Astro body. Absent/empty layout → `<main></main>` (never the old
|
|
23
|
+
* placeholder, never a throw). Unknown Block types degrade to an HTML comment
|
|
24
|
+
* so a stale tree still emits its siblings (forward-compat, ADR 0004).
|
|
25
|
+
*/
|
|
26
|
+
export declare function renderBlockTree(layout: PuckLayout | undefined): string;
|
|
27
|
+
export interface AstroHostAdapterOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Override the default assets root used for repo-mode media (per ADR 0009).
|
|
30
|
+
* Defaults to "public/" — the conventional Astro static-asset directory.
|
|
31
|
+
*/
|
|
32
|
+
readonly assetRoot?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function createAstroHostAdapter(opts?: AstroHostAdapterOptions): HostAdapter;
|
|
35
|
+
//# sourceMappingURL=host-astro-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-astro-adapter.d.ts","sourceRoot":"","sources":["../src/host-astro-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH,OAAO,KAAK,EAIV,WAAW,EAEX,UAAU,EAEX,MAAM,YAAY,CAAA;AAkJnB;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,MAAM,CAMtE;AAuCD,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,wBAAgB,sBAAsB,CACpC,IAAI,GAAE,uBAA4B,GACjC,WAAW,CAuGb"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Astro implementation of the HostAdapter interface.
|
|
3
|
+
*
|
|
4
|
+
* Per ADR 0024, Astro is the only supported host in v1 alpha. This adapter:
|
|
5
|
+
* - reports its `assetRoot()` for repo-mode media (per ADR 0009),
|
|
6
|
+
* - hands `saacms init` the contents of the API mount file
|
|
7
|
+
* (per ADR 0001),
|
|
8
|
+
* - emits `.astro` route files into `src/pages/<page-url>.astro`
|
|
9
|
+
* (per ADR 0003 + ADR 0007),
|
|
10
|
+
* - emits per-Block preview routes the Puck canvas fetches
|
|
11
|
+
* (per ADR 0004 Mode 2).
|
|
12
|
+
*
|
|
13
|
+
* The route + preview emitters are stubs at this scaffold milestone.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, resolve } from "node:path";
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
// Templates live one level up from src/ in the published package layout.
|
|
20
|
+
const TEMPLATES_DIR = resolve(__dirname, "..", "templates");
|
|
21
|
+
const MOUNT_TEMPLATE = readFileSync(resolve(TEMPLATES_DIR, "api-mount.ts.txt"), "utf8");
|
|
22
|
+
const PREVIEW_TEMPLATE = readFileSync(resolve(TEMPLATES_DIR, "preview-route.astro.txt"), "utf8");
|
|
23
|
+
export function createAstroHostAdapter(opts = {}) {
|
|
24
|
+
const assetRootPath = opts.assetRoot ?? "public/";
|
|
25
|
+
return {
|
|
26
|
+
name: "astro",
|
|
27
|
+
assetRoot() {
|
|
28
|
+
return assetRootPath;
|
|
29
|
+
},
|
|
30
|
+
mountTemplate() {
|
|
31
|
+
return MOUNT_TEMPLATE;
|
|
32
|
+
},
|
|
33
|
+
previewRouteTemplate(blockSlug) {
|
|
34
|
+
// Substitute the ${blockSlug} placeholder in the template literally.
|
|
35
|
+
// The template is intentionally not a JS template literal — it lives in
|
|
36
|
+
// a .txt file so the user's Astro toolchain doesn't try to compile it
|
|
37
|
+
// when emitted into their project.
|
|
38
|
+
return PREVIEW_TEMPLATE.replaceAll("${blockSlug}", blockSlug);
|
|
39
|
+
},
|
|
40
|
+
generateRoute(page, _options) {
|
|
41
|
+
// v1 scaffold: return a stub file at the conventional Astro location.
|
|
42
|
+
// Real emission lands per ADR 0003 + ADR 0007 + ADR 0021.
|
|
43
|
+
return {
|
|
44
|
+
path: `src/pages/${page.url}.astro`,
|
|
45
|
+
source: "// generated by saacms host-astro\n" +
|
|
46
|
+
"<!-- TODO: emit Page tree as .astro source -->\n",
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
generatePreviewRoute(block) {
|
|
50
|
+
// v1 scaffold: emit a placeholder preview route at the conventional
|
|
51
|
+
// location. Real emission substitutes the Block's slug + props schema.
|
|
52
|
+
return {
|
|
53
|
+
path: `src/pages/saacms/preview/${block.slug}.astro`,
|
|
54
|
+
source: PREVIEW_TEMPLATE.replaceAll("${blockSlug}", block.slug),
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @saacms/host-astro — public surface.
|
|
3
|
+
*
|
|
4
|
+
* The Astro host adapter (per ADR 0024 v1 alpha scope). Exports the factory
|
|
5
|
+
* `astroHostAdapter()` that returns a `HostAdapter` for use in
|
|
6
|
+
* `saacms.config.ts`:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { astroHostAdapter } from "@saacms/host-astro"
|
|
10
|
+
* import { defineConfig } from "@saacms/core"
|
|
11
|
+
*
|
|
12
|
+
* export default defineConfig({
|
|
13
|
+
* host: astroHostAdapter(),
|
|
14
|
+
* // ...
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import { type AstroHostAdapterOptions } from "./host-astro-adapter.ts";
|
|
19
|
+
import type { HostAdapter } from "./types.ts";
|
|
20
|
+
export { createAstroHostAdapter } from "./host-astro-adapter.ts";
|
|
21
|
+
export type { AstroHostAdapterOptions } from "./host-astro-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 `createAstroHostAdapter(opts)`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function astroHostAdapter(opts?: AstroHostAdapterOptions): 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,uBAAuB,EAC7B,MAAM,yBAAyB,CAAA;AAChC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAChE,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAA;AACtE,YAAY,EACV,KAAK,EACL,aAAa,EACb,oBAAoB,EACpB,WAAW,EACX,IAAI,GACL,MAAM,YAAY,CAAA;AAEnB;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,CAAC,EAAE,uBAAuB,GAAG,WAAW,CAE5E"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// src/host-astro-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.astro.txt"), "utf8");
|
|
10
|
+
function assertSafeUrl(url) {
|
|
11
|
+
if (url.includes("\\")) {
|
|
12
|
+
throw new Error(`generateRoute: url '${url}' contains a backslash; use forward slashes`);
|
|
13
|
+
}
|
|
14
|
+
if (url.startsWith("/")) {
|
|
15
|
+
throw new Error(`generateRoute: url '${url}' is absolute; pass a path without a leading slash`);
|
|
16
|
+
}
|
|
17
|
+
for (const segment of url.split("/")) {
|
|
18
|
+
if (segment === "..") {
|
|
19
|
+
throw new Error(`generateRoute: url '${url}' contains a '..' segment; refusing to escape src/pages`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
var HTML_ATTR_ESCAPE = {
|
|
24
|
+
"&": "&",
|
|
25
|
+
"<": "<",
|
|
26
|
+
">": ">",
|
|
27
|
+
'"': """,
|
|
28
|
+
"'": "'"
|
|
29
|
+
};
|
|
30
|
+
function escapeAttr(value) {
|
|
31
|
+
return value.replace(/[&<>"']/g, (c) => HTML_ATTR_ESCAPE[c] ?? c);
|
|
32
|
+
}
|
|
33
|
+
function isRecord(v) {
|
|
34
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
35
|
+
}
|
|
36
|
+
var CUSTOM_ELEMENT_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/;
|
|
37
|
+
var SAFE_ATTR_NAME = /^[a-zA-Z_][a-zA-Z0-9_.:-]*$/;
|
|
38
|
+
function isBindValue(v) {
|
|
39
|
+
return isRecord(v) && v.kind === "bind" && typeof v.path === "string";
|
|
40
|
+
}
|
|
41
|
+
function renderNode(node) {
|
|
42
|
+
if (!isRecord(node))
|
|
43
|
+
return "";
|
|
44
|
+
const slug = typeof node.block === "string" ? node.block : "";
|
|
45
|
+
if (!CUSTOM_ELEMENT_NAME.test(slug)) {
|
|
46
|
+
const raw = String(node.block ?? "");
|
|
47
|
+
const safe = raw.replace(/-{2,}/g, "-").replace(/>/g, "").replace(/[\r\n]+/g, " ");
|
|
48
|
+
return `<!-- saacms: unknown block '${safe}' -->`;
|
|
49
|
+
}
|
|
50
|
+
const props = isRecord(node.props) ? node.props : {};
|
|
51
|
+
const attrParts = [];
|
|
52
|
+
const jsonProps = {};
|
|
53
|
+
for (const [key, value] of Object.entries(props)) {
|
|
54
|
+
if (value === null || value === undefined)
|
|
55
|
+
continue;
|
|
56
|
+
if (isBindValue(value)) {
|
|
57
|
+
attrParts.push(`data-saacms-bind="${escapeAttr(value.path)}"`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
61
|
+
if (!SAFE_ATTR_NAME.test(key))
|
|
62
|
+
continue;
|
|
63
|
+
attrParts.push(`${key}="${escapeAttr(String(value))}"`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
jsonProps[key] = value;
|
|
67
|
+
}
|
|
68
|
+
if (Object.keys(jsonProps).length > 0) {
|
|
69
|
+
attrParts.push(`props="${escapeAttr(JSON.stringify(jsonProps))}"`);
|
|
70
|
+
}
|
|
71
|
+
const children = Array.isArray(node.children) ? node.children.map(renderNode).join("") : "";
|
|
72
|
+
const attrs = attrParts.length > 0 ? " " + attrParts.join(" ") : "";
|
|
73
|
+
return `<${slug}${attrs}>${children}</${slug}>`;
|
|
74
|
+
}
|
|
75
|
+
function renderBlockTree(layout) {
|
|
76
|
+
const tree = layout?.tree;
|
|
77
|
+
const nodes = isRecord(tree) && Array.isArray(tree.nodes) ? tree.nodes : [];
|
|
78
|
+
if (nodes.length === 0)
|
|
79
|
+
return "<main></main>";
|
|
80
|
+
return `<main>${nodes.map(renderNode).join("")}</main>`;
|
|
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 createAstroHostAdapter(opts = {}) {
|
|
113
|
+
const assetRootPath = opts.assetRoot ?? "public/";
|
|
114
|
+
return {
|
|
115
|
+
name: "astro",
|
|
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/pages/index.astro" : `src/pages/${page.url}.astro`;
|
|
133
|
+
const frontmatterLines = [
|
|
134
|
+
"// generated by saacms host-astro — do not edit",
|
|
135
|
+
`// saacms-page-id: ${page.id}`,
|
|
136
|
+
`// render-mode: ${effectiveMode}`
|
|
137
|
+
];
|
|
138
|
+
if (effectiveMode === "static") {
|
|
139
|
+
frontmatterLines.push("export const prerender = true");
|
|
140
|
+
} else if (effectiveMode === "isr") {
|
|
141
|
+
frontmatterLines.push("export const prerender = true");
|
|
142
|
+
frontmatterLines.push("export const revalidate = 60");
|
|
143
|
+
} else {
|
|
144
|
+
frontmatterLines.push("export const prerender = false");
|
|
145
|
+
}
|
|
146
|
+
let source;
|
|
147
|
+
if (options.theme) {
|
|
148
|
+
const { tokens, schemeId, darkMode } = options.theme;
|
|
149
|
+
const themeAttr = themeImpl.emitThemeAttribute(schemeId, darkMode);
|
|
150
|
+
const styleBlock = themeImpl.emitThemeStyles(tokens);
|
|
151
|
+
const body = renderBlockTree(page.layout);
|
|
152
|
+
source = `---
|
|
153
|
+
` + frontmatterLines.join(`
|
|
154
|
+
`) + `
|
|
155
|
+
---
|
|
156
|
+
` + `
|
|
157
|
+
` + `<html ${themeAttr}>
|
|
158
|
+
` + `<head>
|
|
159
|
+
` + styleBlock + `
|
|
160
|
+
</head>
|
|
161
|
+
` + `<body>
|
|
162
|
+
` + body + `
|
|
163
|
+
</body>
|
|
164
|
+
` + `</html>
|
|
165
|
+
`;
|
|
166
|
+
} else {
|
|
167
|
+
source = `---
|
|
168
|
+
` + frontmatterLines.join(`
|
|
169
|
+
`) + `
|
|
170
|
+
---
|
|
171
|
+
` + `
|
|
172
|
+
` + renderBlockTree(page.layout) + `
|
|
173
|
+
`;
|
|
174
|
+
}
|
|
175
|
+
return { path: filePath, source };
|
|
176
|
+
},
|
|
177
|
+
generatePreviewRoute(block) {
|
|
178
|
+
return {
|
|
179
|
+
path: `src/pages/saacms/preview/${block.slug}.astro`,
|
|
180
|
+
source: PREVIEW_TEMPLATE.replaceAll("${blockSlug}", block.slug)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/index.ts
|
|
187
|
+
function astroHostAdapter(opts) {
|
|
188
|
+
return createAstroHostAdapter(opts ?? {});
|
|
189
|
+
}
|
|
190
|
+
export {
|
|
191
|
+
createAstroHostAdapter,
|
|
192
|
+
astroHostAdapter
|
|
193
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local type definitions for the host-astro 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-astro 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
|
+
* This shape is DERIVED FROM (verified against, not guessed) what
|
|
17
|
+
* `@saacms/admin`'s `PatternMapper.puckDataToDraft` actually emits and what
|
|
18
|
+
* `puckOnPublishHandler` persists as a Page's stored `layout`:
|
|
19
|
+
*
|
|
20
|
+
* SaacmsDraft = { tree: { root: { props: {…} }, nodes: SaacmsNode[] } }
|
|
21
|
+
* SaacmsNode = { block: string, id: string, props: {…} }
|
|
22
|
+
*
|
|
23
|
+
* host-astro mirrors it structurally — it never imports `@measured/puck` nor
|
|
24
|
+
* `@saacms/admin`, so the host type boundary stays editor-agnostic (ADR 0004).
|
|
25
|
+
* `children` is an additive optional field for nested zones the renderer
|
|
26
|
+
* recurses through; PatternMapper's current output is flat, so it is absent
|
|
27
|
+
* today but the contract is forward-compatible.
|
|
28
|
+
*/
|
|
29
|
+
export interface PuckLayoutNode {
|
|
30
|
+
/** The Block slug, e.g. "saacms-hero" (PatternMapper's `SaacmsNode.block`). */
|
|
31
|
+
readonly block: string;
|
|
32
|
+
/** Stable node id (PatternMapper hoists this out of props). */
|
|
33
|
+
readonly id?: string;
|
|
34
|
+
/** Block instance props (the `id` already stripped by PatternMapper). */
|
|
35
|
+
readonly props?: Readonly<Record<string, unknown>>;
|
|
36
|
+
/** Nested child nodes for zoned Blocks; recursed in tree order. */
|
|
37
|
+
readonly children?: ReadonlyArray<PuckLayoutNode>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Stored Puck Block tree JSON — the saacms draft tree. Opaque to the type
|
|
41
|
+
* system, validated structurally at render time (it originates as core
|
|
42
|
+
* `PageDef.layout?: unknown`). Mirrors the exact shape produced by
|
|
43
|
+
* `PatternMapper.puckDataToDraft`.
|
|
44
|
+
*/
|
|
45
|
+
export interface PuckLayout {
|
|
46
|
+
readonly tree: {
|
|
47
|
+
readonly root?: {
|
|
48
|
+
readonly props?: Readonly<Record<string, unknown>>;
|
|
49
|
+
};
|
|
50
|
+
readonly nodes: ReadonlyArray<PuckLayoutNode>;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The Page projection passed to a host adapter's route emitter.
|
|
55
|
+
*
|
|
56
|
+
* TODO: replace with the canonical Page aggregate from `@saacms/core/types`
|
|
57
|
+
* once it lands.
|
|
58
|
+
*/
|
|
59
|
+
export interface Page {
|
|
60
|
+
readonly id: PageID;
|
|
61
|
+
/** URL path WITHOUT a leading slash, e.g. "about" or "blog/[slug]". */
|
|
62
|
+
readonly url: string;
|
|
63
|
+
/** Render mode (per ADR 0007). v1 ships "static" and "isr". */
|
|
64
|
+
readonly renderMode: "static" | "isr" | "dynamic";
|
|
65
|
+
/** Stored Puck Block tree JSON (the saacms draft tree). Opaque; validated at render. Mirrors core PageDef.layout. */
|
|
66
|
+
readonly layout?: PuckLayout;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The Block projection passed to a host adapter's preview-route emitter.
|
|
70
|
+
*
|
|
71
|
+
* TODO: replace with the canonical Block aggregate from `@saacms/core/types`
|
|
72
|
+
* once it lands.
|
|
73
|
+
*/
|
|
74
|
+
export interface Block {
|
|
75
|
+
readonly slug: BlockSlug;
|
|
76
|
+
/** Per ADR 0004 — "web-component" or "framework-native". */
|
|
77
|
+
readonly authoringMode: "web-component" | "framework-native";
|
|
78
|
+
}
|
|
79
|
+
/** A file the host adapter would write into the user's repo. */
|
|
80
|
+
export interface GeneratedFile {
|
|
81
|
+
/** Path relative to the host project root (e.g. "src/pages/about.astro"). */
|
|
82
|
+
readonly path: string;
|
|
83
|
+
/** Verbatim file contents to write. */
|
|
84
|
+
readonly source: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Per-emit options the route emitter uses to decide what to emit.
|
|
88
|
+
*
|
|
89
|
+
* Currently a placeholder; will grow as the static / isr / dynamic emit
|
|
90
|
+
* branches land (per ADR 0007 + ADR 0021).
|
|
91
|
+
*/
|
|
92
|
+
export interface GenerateRouteOptions {
|
|
93
|
+
/** Force a specific render mode override (used by the test suite). */
|
|
94
|
+
readonly forceRenderMode?: "static" | "isr" | "dynamic";
|
|
95
|
+
/** Optional theme options; when provided, the emitted file wraps content in a full HTML shell. */
|
|
96
|
+
readonly theme?: {
|
|
97
|
+
readonly tokens: Record<string, string>;
|
|
98
|
+
readonly schemeId: string;
|
|
99
|
+
readonly darkMode: boolean;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The host-adapter contract.
|
|
104
|
+
*
|
|
105
|
+
* TODO: import from `@saacms/core` once exported there.
|
|
106
|
+
*/
|
|
107
|
+
export interface HostAdapter {
|
|
108
|
+
/** Stable string identifier for this host (e.g. "astro", "nextjs"). */
|
|
109
|
+
readonly name: string;
|
|
110
|
+
/** Repo-relative directory where repo-mode media is committed (ADR 0009). */
|
|
111
|
+
assetRoot(): string;
|
|
112
|
+
/**
|
|
113
|
+
* The verbatim source of the API mount file `saacms init` writes into the
|
|
114
|
+
* user's project (per ADR 0001).
|
|
115
|
+
*/
|
|
116
|
+
mountTemplate(): string;
|
|
117
|
+
/**
|
|
118
|
+
* The verbatim source of a per-Block preview route, with `${blockSlug}`
|
|
119
|
+
* substituted (per ADR 0004 Mode 2).
|
|
120
|
+
*/
|
|
121
|
+
previewRouteTemplate(blockSlug: string): string;
|
|
122
|
+
/** Emit one route file for a Page (per ADR 0003). */
|
|
123
|
+
generateRoute(page: Page, options: GenerateRouteOptions): GeneratedFile;
|
|
124
|
+
/** Emit one preview route for a Block (per ADR 0004 Mode 2). */
|
|
125
|
+
generatePreviewRoute(block: Block): GeneratedFile;
|
|
126
|
+
/** Theme render contract (ADR 0033) — no-FOUC themed SSR. */
|
|
127
|
+
readonly theme: ThemeRenderContract;
|
|
128
|
+
}
|
|
129
|
+
//# 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;;;;;;;;;;;;;;;GAeG;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;;;;;GAKG;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,6EAA6E;IAC7E,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,uEAAuE;IACvE,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/dist/types.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local type definitions for the host-astro 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-astro needs to fulfill its responsibilities; the
|
|
8
|
+
* canonical core interface will be a strict superset.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saacms/host-astro",
|
|
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
|
+
"astro": "^4.0.0 || ^5.0.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"astro": {
|
|
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,12 @@
|
|
|
1
|
+
// generated by saacms — do not edit
|
|
2
|
+
import { createSaacmsRuntime } from "@saacms/core/runtime"
|
|
3
|
+
import config from "../../../../saacms.config.ts"
|
|
4
|
+
|
|
5
|
+
const handler = createSaacmsRuntime(config)
|
|
6
|
+
export const GET = handler.fetch
|
|
7
|
+
export const POST = handler.fetch
|
|
8
|
+
export const PUT = handler.fetch
|
|
9
|
+
export const PATCH = handler.fetch
|
|
10
|
+
export const DELETE = handler.fetch
|
|
11
|
+
export const OPTIONS = handler.fetch
|
|
12
|
+
export const prerender = false
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
// generated by saacms — do not edit
|
|
3
|
+
// per-Page route emitted by @saacms/host-astro's route emitter.
|
|
4
|
+
//
|
|
5
|
+
// v1 alpha scaffold: this template is a comment-only stub. The real emitter
|
|
6
|
+
// (per ADR 0003 + ADR 0007) will substitute:
|
|
7
|
+
// - the Page's URL pattern + render mode (static | isr) into the file path
|
|
8
|
+
// and any export const prerender / revalidate config,
|
|
9
|
+
// - the Puck Block tree as a sequence of Block imports + element usages,
|
|
10
|
+
// - any binding-expression resolution for Page-template renders (per ADR 0010),
|
|
11
|
+
// - the cache-tag emission for the coupling mode (per ADR 0021).
|
|
12
|
+
---
|
|
13
|
+
<!-- TODO: emit Page tree as .astro source -->
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
// generated by saacms — preview route for ${blockSlug}
|
|
3
|
+
const url = new URL(Astro.request.url)
|
|
4
|
+
const propsJson = url.searchParams.get("props") ?? "{}"
|
|
5
|
+
const props = JSON.parse(propsJson)
|
|
6
|
+
import Block from "../../../saacms/blocks/${blockSlug}.astro"
|
|
7
|
+
---
|
|
8
|
+
<Block {...props} />
|