@tinacms/astro 0.0.1 → 0.3.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.
Files changed (75) hide show
  1. package/README.md +50 -20
  2. package/dist/bridge.d.ts +7 -1
  3. package/dist/bridge.js +1 -0
  4. package/dist/data.d.ts +19 -0
  5. package/dist/data.js +64 -0
  6. package/dist/data.test-d.d.ts +1 -0
  7. package/dist/experimental.d.ts +10 -0
  8. package/dist/experimental.js +87 -0
  9. package/dist/index.d.ts +36 -1
  10. package/dist/index.js +92 -2
  11. package/dist/integration.d.ts +5 -0
  12. package/dist/integration.js +69 -0
  13. package/dist/internal/admin-origin.d.ts +6 -0
  14. package/dist/internal/escape.d.ts +8 -0
  15. package/dist/internal/forms-store.d.ts +12 -0
  16. package/dist/internal/request-context.d.ts +16 -0
  17. package/dist/is-edit-mode.d.ts +32 -0
  18. package/dist/is-edit-mode.js +37 -0
  19. package/dist/island-route.d.ts +18 -0
  20. package/dist/island-route.js +87 -0
  21. package/dist/middleware.d.ts +3 -0
  22. package/dist/middleware.js +123 -0
  23. package/dist/sanitize.d.ts +12 -1
  24. package/dist/sanitize.js +6 -10
  25. package/dist/tina-field.d.ts +1 -1
  26. package/dist/tina-field.js +1 -0
  27. package/dist/types.d.ts +92 -1
  28. package/dist/types.js +0 -1
  29. package/dist/vite.d.ts +21 -0
  30. package/dist/vite.js +24 -0
  31. package/package.json +88 -17
  32. package/src/CodeBlockNode.astro +28 -0
  33. package/src/Container.astro +56 -0
  34. package/src/ImageNode.astro +17 -0
  35. package/src/LinkNode.astro +22 -0
  36. package/src/MdxNode.astro +24 -0
  37. package/src/Node.astro +11 -4
  38. package/src/TinaIsland.astro +44 -0
  39. package/src/TinaMarkdown.astro +8 -0
  40. package/src/__tests__/IslandStub.astro +8 -0
  41. package/src/__tests__/TinaIsland.test.ts +60 -0
  42. package/src/__tests__/TinaMarkdown.test.ts +112 -0
  43. package/src/__tests__/__snapshots__/TinaMarkdown.test.ts.snap +7 -0
  44. package/src/__tests__/fixtures/FancyHeading.astro +3 -0
  45. package/src/__tests__/fixtures/MyFeature.astro +4 -0
  46. package/src/__tests__/fixtures/basic-kitchen-sink.json +60 -0
  47. package/src/__tests__/fixtures/code-block.json +34 -0
  48. package/src/__tests__/fixtures/leaf-marks.json +199 -0
  49. package/src/__tests__/fixtures/mdx-jsx-flow.json +40 -0
  50. package/src/__tests__/fixtures/mdx-jsx-text.json +53 -0
  51. package/src/__tests__/forms-store.test.ts +70 -0
  52. package/src/__tests__/integration.test.ts +124 -0
  53. package/src/__tests__/island-route.test.ts +119 -0
  54. package/src/__tests__/middleware.test.ts +102 -0
  55. package/src/__tests__/sanitize.test.ts +75 -0
  56. package/src/__tests__/vite.test.ts +67 -0
  57. package/src/bridge.ts +7 -0
  58. package/src/data.test-d.ts +53 -0
  59. package/src/data.ts +73 -0
  60. package/src/experimental.ts +14 -0
  61. package/src/index.ts +54 -0
  62. package/src/integration.ts +94 -0
  63. package/src/internal/admin-origin.ts +19 -0
  64. package/src/internal/escape.ts +15 -0
  65. package/src/internal/forms-store.ts +50 -0
  66. package/src/internal/request-context.ts +23 -0
  67. package/src/is-edit-mode.ts +68 -0
  68. package/src/island-route.ts +109 -0
  69. package/src/middleware.ts +89 -0
  70. package/src/sanitize.ts +64 -0
  71. package/src/tina-field.ts +1 -0
  72. package/src/types.ts +97 -0
  73. package/src/vite.ts +40 -0
  74. package/dist/preview.d.ts +0 -1
  75. package/dist/preview.js +0 -1
@@ -0,0 +1,18 @@
1
+ import type { APIRoute } from 'astro';
2
+ import type { AstroComponentFactory } from 'astro/runtime/server/index.js';
3
+ export interface IslandWrapper {
4
+ tag: string;
5
+ className?: string;
6
+ }
7
+ export interface IslandConfig {
8
+ /** Resolve the data the component needs. May ignore the search params. */
9
+ fetch: (request: Request, params: URLSearchParams) => Promise<unknown>;
10
+ /** Astro component to render with the fetched data. */
11
+ component: AstroComponentFactory;
12
+ /** Outer element the bridge swaps into — must match the page-side wrapper. */
13
+ wrapper: IslandWrapper;
14
+ /** Map fetched data + URL params to the component's props. */
15
+ propsFromData: (data: unknown, params: URLSearchParams) => Record<string, unknown>;
16
+ }
17
+ export type IslandRegistry = Record<string, IslandConfig>;
18
+ export declare function experimental_createIslandRoute(islands: IslandRegistry): APIRoute;
@@ -0,0 +1,87 @@
1
+ // src/island-route.ts
2
+ import { PREVIEW_CONTENT_TYPE, PRIME_HEADER } from "@tinacms/bridge/preview";
3
+ import { experimental_AstroContainer as AstroContainer } from "astro/container";
4
+
5
+ // src/internal/escape.ts
6
+ function escapeAttr(s) {
7
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
8
+ }
9
+
10
+ // src/internal/forms-store.ts
11
+ import { AsyncLocalStorage } from "node:async_hooks";
12
+ var STORE_KEY = Symbol.for("@tinacms/astro/forms-store");
13
+ var slot = globalThis;
14
+ var formsStore = slot[STORE_KEY] ??= new AsyncLocalStorage();
15
+ function sortByPriority(forms) {
16
+ return [...forms].sort(
17
+ (a, b) => (a.priority === "primary" ? 0 : 1) - (b.priority === "primary" ? 0 : 1)
18
+ );
19
+ }
20
+ function renderFormPayloadDiv(form, primary) {
21
+ return `<div data-tina-form="${escapeAttr(JSON.stringify(form))}"${primary ? " data-tina-primary" : ""} hidden></div>`;
22
+ }
23
+
24
+ // src/internal/request-context.ts
25
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
26
+ var STORE_KEY2 = Symbol.for("@tinacms/astro/request-context");
27
+ var slot2 = globalThis;
28
+ var requestStore = slot2[STORE_KEY2] ??= new AsyncLocalStorage2();
29
+
30
+ // src/island-route.ts
31
+ function experimental_createIslandRoute(islands) {
32
+ return async ({ params, request, url }) => {
33
+ const rejection = rejectIfUnsafe(request);
34
+ if (rejection) return rejection;
35
+ const island = islands[params.name ?? ""];
36
+ if (!island) {
37
+ return new Response(`Unknown island "${params.name}"`, { status: 404 });
38
+ }
39
+ const priming = request.headers.get(PRIME_HEADER) !== null;
40
+ try {
41
+ const forms = [];
42
+ const html = await requestStore.run(
43
+ request,
44
+ () => formsStore.run(forms, async () => {
45
+ const data = await island.fetch(request, url.searchParams);
46
+ const container = await AstroContainer.create();
47
+ return container.renderToString(island.component, {
48
+ props: island.propsFromData(data, url.searchParams)
49
+ });
50
+ })
51
+ );
52
+ const body = (priming ? renderFormPayloads(forms) : "") + wrapIsland(html, island.wrapper, url);
53
+ return new Response(body, {
54
+ headers: {
55
+ "Content-Type": "text/html; charset=utf-8",
56
+ "Cache-Control": "no-store"
57
+ }
58
+ });
59
+ } catch {
60
+ return new Response("Island render failed", { status: 500 });
61
+ }
62
+ };
63
+ }
64
+ function rejectIfUnsafe(request) {
65
+ if (request.method !== "POST") {
66
+ return new Response("Method Not Allowed", { status: 405 });
67
+ }
68
+ const contentType = request.headers.get("content-type") ?? "";
69
+ if (!contentType.includes(PREVIEW_CONTENT_TYPE)) {
70
+ return new Response("Not Found", { status: 404 });
71
+ }
72
+ if (request.headers.get("sec-fetch-site") === "cross-site") {
73
+ return new Response("Forbidden", { status: 403 });
74
+ }
75
+ return null;
76
+ }
77
+ function renderFormPayloads(forms) {
78
+ return sortByPriority(forms).map((form) => renderFormPayloadDiv(form, form.priority === "primary")).join("");
79
+ }
80
+ function wrapIsland(html, wrapper, url) {
81
+ const cls = wrapper.className ? ` class="${escapeAttr(wrapper.className)}"` : "";
82
+ const marker = escapeAttr(`${url.pathname}${url.search}`);
83
+ return `<${wrapper.tag}${cls} data-tina-island="${marker}">${html}</${wrapper.tag}>`;
84
+ }
85
+ export {
86
+ experimental_createIslandRoute
87
+ };
@@ -0,0 +1,3 @@
1
+ import type { MiddlewareHandler } from 'astro';
2
+ export declare const onRequest: MiddlewareHandler;
3
+ export default onRequest;
@@ -0,0 +1,123 @@
1
+ // src/internal/admin-origin.ts
2
+ function adminOrigins() {
3
+ const env = import.meta.env;
4
+ const raw = env?.PUBLIC_TINA_ADMIN_ORIGIN;
5
+ if (!raw) return null;
6
+ const origins = raw.split(",").map((s) => s.trim()).filter(Boolean);
7
+ return origins.length > 0 ? origins : null;
8
+ }
9
+
10
+ // src/internal/forms-store.ts
11
+ import { AsyncLocalStorage } from "node:async_hooks";
12
+
13
+ // src/internal/escape.ts
14
+ function escapeAttr(s) {
15
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
16
+ }
17
+
18
+ // src/internal/forms-store.ts
19
+ var STORE_KEY = Symbol.for("@tinacms/astro/forms-store");
20
+ var slot = globalThis;
21
+ var formsStore = slot[STORE_KEY] ??= new AsyncLocalStorage();
22
+ function sortByPriority(forms) {
23
+ return [...forms].sort(
24
+ (a, b) => (a.priority === "primary" ? 0 : 1) - (b.priority === "primary" ? 0 : 1)
25
+ );
26
+ }
27
+ function renderFormPayloadDiv(form, primary) {
28
+ return `<div data-tina-form="${escapeAttr(JSON.stringify(form))}"${primary ? " data-tina-primary" : ""} hidden></div>`;
29
+ }
30
+
31
+ // src/internal/request-context.ts
32
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
33
+ var STORE_KEY2 = Symbol.for("@tinacms/astro/request-context");
34
+ var slot2 = globalThis;
35
+ var requestStore = slot2[STORE_KEY2] ??= new AsyncLocalStorage2();
36
+
37
+ // src/is-edit-mode.ts
38
+ var EDIT_COOKIE = "__tina_edit";
39
+ var EDIT_COOKIE_HEADER = `${EDIT_COOKIE}=1; Path=/; SameSite=Strict; Max-Age=3600`;
40
+ function isEditMode(request) {
41
+ const url = new URL(request.url);
42
+ if (url.searchParams.get("tina-edit") === "1") return true;
43
+ const dest = request.headers.get("Sec-Fetch-Dest");
44
+ if (dest !== "iframe") return false;
45
+ const referer = request.headers.get("Referer");
46
+ if (referer) {
47
+ try {
48
+ const refererUrl = new URL(referer);
49
+ if (refererUrl.origin === url.origin && refererUrl.pathname.startsWith("/admin/")) {
50
+ return true;
51
+ }
52
+ } catch {
53
+ }
54
+ }
55
+ return readCookie(request, EDIT_COOKIE) === "1";
56
+ }
57
+ function readCookie(request, name) {
58
+ const header = request.headers.get("Cookie");
59
+ if (!header) return null;
60
+ for (const pair of header.split(";")) {
61
+ const eq = pair.indexOf("=");
62
+ if (eq === -1) continue;
63
+ const key = pair.slice(0, eq).trim();
64
+ if (key === name) return pair.slice(eq + 1).trim();
65
+ }
66
+ return null;
67
+ }
68
+
69
+ // src/middleware.ts
70
+ var HEAD_CLOSE = "</head>";
71
+ var onRequest = (context, next) => {
72
+ if (context.isPrerendered) {
73
+ context.locals.tinaEdit = false;
74
+ return next();
75
+ }
76
+ const editing = isEditMode(context.request);
77
+ context.locals.tinaEdit = editing;
78
+ const forms = [];
79
+ return requestStore.run(
80
+ context.request,
81
+ () => formsStore.run(forms, async () => {
82
+ const response = await next();
83
+ return editing ? injectEditMode(response, forms) : response;
84
+ })
85
+ );
86
+ };
87
+ var middleware_default = onRequest;
88
+ async function injectEditMode(response, forms) {
89
+ const init = editModeInit(response);
90
+ const contentType = response.headers.get("content-type") ?? "";
91
+ if (!contentType.includes("text/html")) {
92
+ return new Response(response.body, init);
93
+ }
94
+ const html = await response.text();
95
+ const headEnd = html.indexOf(HEAD_CLOSE);
96
+ if (headEnd === -1) {
97
+ return new Response(html, init);
98
+ }
99
+ const injection = renderInjection(forms);
100
+ return new Response(
101
+ html.slice(0, headEnd) + injection + html.slice(headEnd),
102
+ init
103
+ );
104
+ }
105
+ function editModeInit(response) {
106
+ const headers = new Headers(response.headers);
107
+ headers.delete("content-length");
108
+ headers.append("Set-Cookie", EDIT_COOKIE_HEADER);
109
+ return { status: response.status, statusText: response.statusText, headers };
110
+ }
111
+ function renderInjection(forms) {
112
+ const formDivs = sortByPriority(forms).map((form, i) => renderFormPayloadDiv(form, i === 0)).join("");
113
+ return formDivs + bridgeScript();
114
+ }
115
+ function bridgeScript() {
116
+ const origins = adminOrigins();
117
+ const initArg = origins ? `{adminOrigin:${JSON.stringify(origins)}}` : "";
118
+ return `<script type="module">import{init,refreshForms}from"/admin/bridge.js";init(${initArg});document.addEventListener("astro:page-load",refreshForms);</script>`;
119
+ }
120
+ export {
121
+ middleware_default as default,
122
+ onRequest
123
+ };
@@ -1 +1,12 @@
1
- export * from "../src/sanitize"
1
+ /**
2
+ * Sanitizes a CMS-supplied href, returning a safe URL or the fallback.
3
+ * Blocks dangerous schemes (javascript:, data:, vbscript:) and
4
+ * protocol-relative URLs (//evil.com). Allows relative paths, http(s),
5
+ * and mailto:.
6
+ */
7
+ export declare function sanitizeHref(value: unknown, fallback?: string): string;
8
+ /**
9
+ * Validates a CMS-supplied image src, returning the src string if safe or ''
10
+ * if it is empty, not a string, or uses a non-http(s)/relative scheme.
11
+ */
12
+ export declare function sanitizeImageSrc(src: unknown): string;
package/dist/sanitize.js CHANGED
@@ -1,9 +1,8 @@
1
+ // src/sanitize.ts
1
2
  function sanitizeHref(value, fallback = "#") {
2
- if (typeof value !== "string")
3
- return fallback;
3
+ if (typeof value !== "string") return fallback;
4
4
  const trimmed = value.trim();
5
- if (!trimmed)
6
- return fallback;
5
+ if (!trimmed) return fallback;
7
6
  const lower = trimmed.toLowerCase();
8
7
  if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:")) {
9
8
  return fallback;
@@ -22,18 +21,15 @@ function sanitizeHref(value, fallback = "#") {
22
21
  return fallback;
23
22
  }
24
23
  function sanitizeImageSrc(src) {
25
- if (typeof src !== "string")
26
- return "";
24
+ if (typeof src !== "string") return "";
27
25
  const trimmed = src.trim();
28
- if (!trimmed)
29
- return "";
26
+ if (!trimmed) return "";
30
27
  if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("/") && !trimmed.startsWith("//")) {
31
28
  return trimmed;
32
29
  }
33
30
  try {
34
31
  const url = new URL(trimmed);
35
- if (url.protocol === "http:" || url.protocol === "https:")
36
- return trimmed;
32
+ if (url.protocol === "http:" || url.protocol === "https:") return trimmed;
37
33
  } catch {
38
34
  return "";
39
35
  }
@@ -1 +1 @@
1
- export * from "../src/tina-field"
1
+ export * from '@tinacms/bridge/tina-field';
@@ -1 +1,2 @@
1
+ // src/tina-field.ts
1
2
  export * from "@tinacms/bridge/tina-field";
package/dist/types.d.ts CHANGED
@@ -1 +1,92 @@
1
- export * from "../src/types"
1
+ /**
2
+ * Plate AST node types Tina returns from rich-text fields. Mirrors the shape
3
+ * documented in `packages/@tinacms/mdx/src/parse/plate.ts` — kept inline so
4
+ * the Astro renderer can stay framework-free and not pull @tinacms/mdx into
5
+ * the page bundle.
6
+ */
7
+ /**
8
+ * Astro doesn't publicly export `AstroComponentFactory`, so we use a
9
+ * structural placeholder. The consumer's Astro pipeline performs the real
10
+ * compile-time check that whatever they pass is a valid component.
11
+ */
12
+ export type AstroComponent = (...args: never[]) => unknown;
13
+ export type TinaRichTextRoot = {
14
+ type: 'root';
15
+ children: TinaRichTextNode[];
16
+ };
17
+ export type TinaRichTextNode = BlockElement | InlineElement | TextElement | MdxElement;
18
+ export type LinkElement = {
19
+ type: 'a';
20
+ url: string;
21
+ title?: string;
22
+ children: InlineElement[];
23
+ };
24
+ export type ImageElement = {
25
+ type: 'img';
26
+ url: string;
27
+ alt?: string;
28
+ caption?: string;
29
+ };
30
+ export type CodeBlockElement = {
31
+ type: 'code_block';
32
+ lang?: string;
33
+ value?: string;
34
+ children?: {
35
+ children: TextElement[];
36
+ }[];
37
+ };
38
+ export type BlockElement = {
39
+ type: 'p';
40
+ children: InlineElement[];
41
+ } | {
42
+ type: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
43
+ children: InlineElement[];
44
+ } | {
45
+ type: 'blockquote';
46
+ children: TinaRichTextNode[];
47
+ } | {
48
+ type: 'ul' | 'ol';
49
+ children: TinaRichTextNode[];
50
+ } | {
51
+ type: 'li';
52
+ children: TinaRichTextNode[];
53
+ } | {
54
+ type: 'lic';
55
+ children: InlineElement[];
56
+ } | {
57
+ type: 'hr';
58
+ } | {
59
+ type: 'break';
60
+ } | ImageElement | CodeBlockElement | {
61
+ type: 'maybe_mdx';
62
+ } | {
63
+ type: 'html';
64
+ value: string;
65
+ } | {
66
+ type: 'invalid_markdown';
67
+ value: string;
68
+ };
69
+ export type InlineElement = TextElement | LinkElement | {
70
+ type: 'html_inline';
71
+ value: string;
72
+ } | MdxElement;
73
+ export type TextElement = {
74
+ type: 'text';
75
+ text: string;
76
+ bold?: boolean;
77
+ italic?: boolean;
78
+ underline?: boolean;
79
+ strikethrough?: boolean;
80
+ code?: boolean;
81
+ highlight?: boolean;
82
+ highlightColor?: string;
83
+ };
84
+ export type MdxElement = {
85
+ type: 'mdxJsxFlowElement' | 'mdxJsxTextElement';
86
+ name: string;
87
+ props: Record<string, unknown>;
88
+ children?: TinaRichTextNode[];
89
+ };
90
+ export type TinaRichTextContent = TinaRichTextRoot | TinaRichTextNode[] | null | undefined;
91
+ /** A map of mdxJsx name (or default tag override) → Astro component. */
92
+ export type CustomComponentsMap = Record<string, AstroComponent>;
package/dist/types.js CHANGED
@@ -1 +0,0 @@
1
-
package/dist/vite.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * Dev-only Vite plugin that redirects `/admin` (and `/admin/`) to
4
+ * `/admin/index.html`.
5
+ *
6
+ * In `astro dev`, the admin SPA is served straight from `public/admin/` and
7
+ * Vite does not resolve a directory index for it, so a bare `/admin` request
8
+ * 404s. This plugin makes `/admin` land on the SPA the same way it does in a
9
+ * production build. It only applies to the dev server (`apply: 'serve'`); a
10
+ * built site serves `public/admin/index.html` itself.
11
+ *
12
+ * @example
13
+ * // astro.config.mjs
14
+ * import { tinaAdminDevRedirect } from '@tinacms/astro/vite';
15
+ *
16
+ * export default defineConfig({
17
+ * vite: { plugins: [tinaAdminDevRedirect()] },
18
+ * });
19
+ */
20
+ export declare function tinaAdminDevRedirect(): Plugin;
21
+ export default tinaAdminDevRedirect;
package/dist/vite.js ADDED
@@ -0,0 +1,24 @@
1
+ // src/vite.ts
2
+ function tinaAdminDevRedirect() {
3
+ return {
4
+ name: "tina-admin-dev-redirect",
5
+ apply: "serve",
6
+ configureServer(server) {
7
+ server.middlewares.use((req, res, next) => {
8
+ const path = (req.url || "").split("?")[0];
9
+ if (path === "/admin" || path === "/admin/") {
10
+ res.statusCode = 302;
11
+ res.setHeader("Location", "/admin/index.html");
12
+ res.end();
13
+ return;
14
+ }
15
+ next();
16
+ });
17
+ }
18
+ };
19
+ }
20
+ var vite_default = tinaAdminDevRedirect;
21
+ export {
22
+ vite_default as default,
23
+ tinaAdminDevRedirect
24
+ };
package/package.json CHANGED
@@ -1,56 +1,127 @@
1
1
  {
2
2
  "name": "@tinacms/astro",
3
- "version": "0.0.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "src/TinaMarkdown.astro",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
8
  "package.json",
9
- "README.md",
10
- "src/TinaMarkdown.astro",
11
- "src/Node.astro",
12
- "src/Leaf.astro",
9
+ "src",
13
10
  "dist"
14
11
  ],
15
12
  "exports": {
16
13
  ".": {
17
14
  "types": "./dist/index.d.ts",
18
- "default": "./src/TinaMarkdown.astro"
15
+ "astro": "./src/TinaMarkdown.astro",
16
+ "default": "./dist/index.js"
19
17
  },
18
+ "./TinaMarkdown.astro": "./src/TinaMarkdown.astro",
19
+ "./TinaIsland.astro": "./src/TinaIsland.astro",
20
20
  "./types": {
21
21
  "types": "./dist/types.d.ts",
22
+ "astro": "./src/types.ts",
22
23
  "default": "./dist/types.js"
23
24
  },
24
25
  "./sanitize": {
25
26
  "types": "./dist/sanitize.d.ts",
27
+ "astro": "./src/sanitize.ts",
26
28
  "default": "./dist/sanitize.js"
27
29
  },
28
30
  "./bridge": {
29
31
  "types": "./dist/bridge.d.ts",
32
+ "astro": "./src/bridge.ts",
30
33
  "default": "./dist/bridge.js"
31
34
  },
32
35
  "./tina-field": {
33
36
  "types": "./dist/tina-field.d.ts",
37
+ "astro": "./src/tina-field.ts",
34
38
  "default": "./dist/tina-field.js"
35
39
  },
36
- "./preview": {
37
- "types": "./dist/preview.d.ts",
38
- "default": "./dist/preview.js"
40
+ "./data": {
41
+ "types": "./dist/data.d.ts",
42
+ "astro": "./src/data.ts",
43
+ "default": "./dist/data.js"
44
+ },
45
+ "./is-edit-mode": {
46
+ "types": "./dist/is-edit-mode.d.ts",
47
+ "astro": "./src/is-edit-mode.ts",
48
+ "default": "./dist/is-edit-mode.js"
49
+ },
50
+ "./middleware": {
51
+ "types": "./dist/middleware.d.ts",
52
+ "astro": "./src/middleware.ts",
53
+ "default": "./dist/middleware.js"
54
+ },
55
+ "./integration": {
56
+ "types": "./dist/integration.d.ts",
57
+ "astro": "./src/integration.ts",
58
+ "default": "./dist/integration.js"
59
+ },
60
+ "./experimental": {
61
+ "types": "./dist/experimental.d.ts",
62
+ "astro": "./src/experimental.ts",
63
+ "default": "./dist/experimental.js"
64
+ },
65
+ "./vite": {
66
+ "types": "./dist/vite.d.ts",
67
+ "default": "./dist/vite.js"
39
68
  }
40
69
  },
41
70
  "license": "Apache-2.0",
42
71
  "buildConfig": {
43
72
  "entryPoints": [
44
- "src/index.ts",
45
- "src/types.ts",
46
- "src/sanitize.ts",
47
- "src/bridge.ts",
48
- "src/tina-field.ts",
49
- "src/preview.ts"
73
+ {
74
+ "name": "src/index.ts",
75
+ "target": "node"
76
+ },
77
+ {
78
+ "name": "src/types.ts",
79
+ "target": "node"
80
+ },
81
+ {
82
+ "name": "src/sanitize.ts",
83
+ "target": "node"
84
+ },
85
+ {
86
+ "name": "src/bridge.ts",
87
+ "target": "node"
88
+ },
89
+ {
90
+ "name": "src/tina-field.ts",
91
+ "target": "node"
92
+ },
93
+ {
94
+ "name": "src/data.ts",
95
+ "target": "node"
96
+ },
97
+ {
98
+ "name": "src/is-edit-mode.ts",
99
+ "target": "node"
100
+ },
101
+ {
102
+ "name": "src/middleware.ts",
103
+ "target": "node"
104
+ },
105
+ {
106
+ "name": "src/integration.ts",
107
+ "target": "node"
108
+ },
109
+ {
110
+ "name": "src/experimental.ts",
111
+ "target": "node"
112
+ },
113
+ {
114
+ "name": "src/island-route.ts",
115
+ "target": "node"
116
+ },
117
+ {
118
+ "name": "src/vite.ts",
119
+ "target": "node"
120
+ }
50
121
  ]
51
122
  },
52
123
  "dependencies": {
53
- "@tinacms/bridge": "0.0.1"
124
+ "@tinacms/bridge": "0.3.0"
54
125
  },
55
126
  "peerDependencies": {
56
127
  "astro": "^5.0.0"
@@ -61,7 +132,7 @@
61
132
  "typescript": "^5.7.3",
62
133
  "vite": "^6.0.0",
63
134
  "vitest": "^3.0.0",
64
- "@tinacms/scripts": "1.6.0"
135
+ "@tinacms/scripts": "1.6.1"
65
136
  },
66
137
  "publishConfig": {
67
138
  "registry": "https://registry.npmjs.org"
@@ -0,0 +1,28 @@
1
+ ---
2
+ /**
3
+ * `code_block` nodes carry their value as either a flat `value` string or as
4
+ * `children: [{ children: [{ text }, ...] }, ...]` (one outer entry per line).
5
+ * Flatten both shapes to a single string before rendering.
6
+ */
7
+ import type { CodeBlockElement, CustomComponentsMap } from './types';
8
+
9
+ interface Props {
10
+ node: CodeBlockElement;
11
+ components: CustomComponentsMap;
12
+ }
13
+
14
+ const { node, components } = Astro.props;
15
+ const Override = components.code_block;
16
+
17
+ const value =
18
+ node.value ??
19
+ node.children
20
+ ?.map((line) => line.children?.map((tn) => tn.text).join('') ?? '')
21
+ .join('\n') ??
22
+ '';
23
+ ---
24
+ {Override ? (
25
+ <Override value={value} lang={node.lang} />
26
+ ) : (
27
+ <pre><code class={node.lang ? `language-${node.lang}` : undefined}>{value}</code></pre>
28
+ )}
@@ -0,0 +1,56 @@
1
+ ---
2
+ /**
3
+ * Renders any node whose default form is `<Tag>{children}</Tag>` —
4
+ * paragraphs, headings, lists, list items, blockquote, lic. The tag itself
5
+ * is derived from `node.type`; if the consumer registered an override on
6
+ * `components[type]`, we render that instead and pass children as a slot.
7
+ */
8
+ import TinaMarkdown from './TinaMarkdown.astro';
9
+ import type { CustomComponentsMap, TinaRichTextNode } from './types';
10
+
11
+ interface Props {
12
+ node: TinaRichTextNode & { children: TinaRichTextNode[] };
13
+ components: CustomComponentsMap;
14
+ }
15
+
16
+ const { node, components } = Astro.props;
17
+ const t = node.type;
18
+ const Override = components[t];
19
+
20
+ const tagFor: Record<
21
+ string,
22
+ | 'p'
23
+ | 'h1'
24
+ | 'h2'
25
+ | 'h3'
26
+ | 'h4'
27
+ | 'h5'
28
+ | 'h6'
29
+ | 'ol'
30
+ | 'ul'
31
+ | 'li'
32
+ | 'div'
33
+ | 'blockquote'
34
+ > = {
35
+ p: 'p',
36
+ h1: 'h1',
37
+ h2: 'h2',
38
+ h3: 'h3',
39
+ h4: 'h4',
40
+ h5: 'h5',
41
+ h6: 'h6',
42
+ ol: 'ol',
43
+ ul: 'ul',
44
+ li: 'li',
45
+ blockquote: 'blockquote',
46
+ lic: 'div',
47
+ };
48
+ const Tag = tagFor[t];
49
+ ---
50
+ {Override ? (
51
+ <Override>
52
+ <TinaMarkdown content={node.children} components={components} />
53
+ </Override>
54
+ ) : (
55
+ <Tag><TinaMarkdown content={node.children} components={components} /></Tag>
56
+ )}