@tinacms/astro 0.4.0 → 0.5.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/integration.d.ts +7 -0
- package/dist/integration.js +25 -1
- package/dist/types.d.ts +130 -5
- package/package.json +1 -1
- package/src/MdxNode.astro +14 -3
- package/src/MdxTableNode.astro +84 -0
- package/src/Node.astro +4 -0
- package/src/TableNode.astro +65 -0
- package/src/__tests__/TinaMarkdown.test.ts +47 -0
- package/src/__tests__/__snapshots__/TinaMarkdown.test.ts.snap +4 -0
- package/src/__tests__/fixtures/FancyTable.astro +3 -0
- package/src/__tests__/fixtures/mdx-table.json +44 -0
- package/src/__tests__/fixtures/table.json +60 -0
- package/src/__tests__/integration.test.ts +106 -2
- package/src/integration.ts +69 -1
- package/src/types.ts +142 -3
package/dist/integration.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { AstroIntegration } from 'astro';
|
|
2
2
|
export interface TinaIntegrationOptions {
|
|
3
3
|
middlewareOrder?: 'pre' | 'post';
|
|
4
|
+
/**
|
|
5
|
+
* Force the Cloudflare Workers (workerd) `import.meta.url` workaround on or
|
|
6
|
+
* off. When omitted, it is applied automatically whenever the
|
|
7
|
+
* `@astrojs/cloudflare` adapter is detected. Set `false` to opt out, or
|
|
8
|
+
* `true` to force it for a custom Cloudflare setup.
|
|
9
|
+
*/
|
|
10
|
+
cloudflareWorkers?: boolean;
|
|
4
11
|
}
|
|
5
12
|
export default function tina(options?: TinaIntegrationOptions): AstroIntegration;
|
package/dist/integration.js
CHANGED
|
@@ -4,9 +4,13 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
var BRIDGE_ROUTE = "/admin/bridge.js";
|
|
7
|
+
var CLOUDFLARE_ADAPTER_NAME = "@astrojs/cloudflare";
|
|
8
|
+
var WORKERD_IMPORT_META_URL = JSON.stringify("file:///worker.mjs");
|
|
9
|
+
var SERVER_ENVIRONMENTS = /* @__PURE__ */ new Set(["ssr", "astro"]);
|
|
7
10
|
function tina(options = {}) {
|
|
8
11
|
const { middlewareOrder = "pre" } = options;
|
|
9
12
|
let clientDir;
|
|
13
|
+
let isCloudflareAdapter = false;
|
|
10
14
|
return {
|
|
11
15
|
name: "@tinacms/astro",
|
|
12
16
|
hooks: {
|
|
@@ -15,10 +19,18 @@ function tina(options = {}) {
|
|
|
15
19
|
entrypoint: "@tinacms/astro/middleware",
|
|
16
20
|
order: middlewareOrder
|
|
17
21
|
});
|
|
18
|
-
updateConfig({
|
|
22
|
+
updateConfig({
|
|
23
|
+
vite: {
|
|
24
|
+
plugins: [
|
|
25
|
+
bridgeDevPlugin(),
|
|
26
|
+
cloudflareImportMetaUrlPlugin(() => isCloudflareAdapter)
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
});
|
|
19
30
|
},
|
|
20
31
|
"astro:config:done": ({ config }) => {
|
|
21
32
|
clientDir = config.build.client;
|
|
33
|
+
isCloudflareAdapter = options.cloudflareWorkers ?? config.adapter?.name === CLOUDFLARE_ADAPTER_NAME;
|
|
22
34
|
},
|
|
23
35
|
"astro:build:done": ({ logger }) => {
|
|
24
36
|
if (!clientDir) return;
|
|
@@ -54,6 +66,18 @@ function bridgeDevPlugin() {
|
|
|
54
66
|
}
|
|
55
67
|
};
|
|
56
68
|
}
|
|
69
|
+
function cloudflareImportMetaUrlPlugin(isCloudflare) {
|
|
70
|
+
const define = { "import.meta.url": WORKERD_IMPORT_META_URL };
|
|
71
|
+
return {
|
|
72
|
+
name: "@tinacms/astro:cloudflare-import-meta-url",
|
|
73
|
+
// No `apply`: the define is needed in both the build (ssr) and dev envs.
|
|
74
|
+
configEnvironment(name) {
|
|
75
|
+
if (!isCloudflare()) return;
|
|
76
|
+
if (!SERVER_ENVIRONMENTS.has(name)) return;
|
|
77
|
+
return { define };
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
57
81
|
function emitBridgeAsset(adminDir, logger) {
|
|
58
82
|
try {
|
|
59
83
|
mkdirSync(adminDir, { recursive: true });
|
package/dist/types.d.ts
CHANGED
|
@@ -35,10 +35,54 @@ export type CodeBlockElement = {
|
|
|
35
35
|
children: TextElement[];
|
|
36
36
|
}[];
|
|
37
37
|
};
|
|
38
|
-
|
|
38
|
+
/** Per-column horizontal alignment for table cells. */
|
|
39
|
+
export type TableAlign = 'left' | 'right' | 'center';
|
|
40
|
+
/** A paragraph — the block wrapper Tina puts around inline content. */
|
|
41
|
+
export type ParagraphElement = {
|
|
39
42
|
type: 'p';
|
|
40
43
|
children: InlineElement[];
|
|
41
|
-
}
|
|
44
|
+
};
|
|
45
|
+
/** A single table cell. Its content is wrapped in a paragraph by the parser. */
|
|
46
|
+
export type TableCellElement = {
|
|
47
|
+
type: 'td';
|
|
48
|
+
children: ParagraphElement[];
|
|
49
|
+
};
|
|
50
|
+
export type TableRowElement = {
|
|
51
|
+
type: 'tr';
|
|
52
|
+
children: TableCellElement[];
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Native markdown table node (the shape `@tinacms/mdx` emits from a GFM
|
|
56
|
+
* table). Rows/cells are plain `tr`/`td`; per-column text-align lives on
|
|
57
|
+
* `props.align`. Mirrors `TableElement` in
|
|
58
|
+
* `packages/@tinacms/mdx/src/parse/plate.ts`.
|
|
59
|
+
*/
|
|
60
|
+
export type TableElement = {
|
|
61
|
+
type: 'table';
|
|
62
|
+
props?: {
|
|
63
|
+
align?: TableAlign[];
|
|
64
|
+
};
|
|
65
|
+
children: TableRowElement[];
|
|
66
|
+
};
|
|
67
|
+
/** A cell in the legacy MDX-flow table; its `value` is itself rich text. */
|
|
68
|
+
export type MdxTableCell = {
|
|
69
|
+
value: TinaRichTextContent;
|
|
70
|
+
};
|
|
71
|
+
export type MdxTableRow = {
|
|
72
|
+
tableCells: MdxTableCell[];
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Props of the legacy MDX-flow table — an `mdxJsxFlowElement` named `table`
|
|
76
|
+
* whose cells live on `props.tableRows` rather than as `tr`/`td` child nodes.
|
|
77
|
+
* Older editors produced this shape; `MdxTableNode.astro` still renders it.
|
|
78
|
+
*/
|
|
79
|
+
export type MdxTableProps = {
|
|
80
|
+
align?: TableAlign[];
|
|
81
|
+
/** When true, the first row is rendered as a `<thead>` of `<th>`. */
|
|
82
|
+
firstRowHeader?: boolean;
|
|
83
|
+
tableRows?: MdxTableRow[];
|
|
84
|
+
};
|
|
85
|
+
export type BlockElement = ParagraphElement | {
|
|
42
86
|
type: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
43
87
|
children: InlineElement[];
|
|
44
88
|
} | {
|
|
@@ -57,7 +101,7 @@ export type BlockElement = {
|
|
|
57
101
|
type: 'hr';
|
|
58
102
|
} | {
|
|
59
103
|
type: 'break';
|
|
60
|
-
} | ImageElement | CodeBlockElement | {
|
|
104
|
+
} | ImageElement | CodeBlockElement | TableElement | {
|
|
61
105
|
type: 'maybe_mdx';
|
|
62
106
|
} | {
|
|
63
107
|
type: 'html';
|
|
@@ -88,5 +132,86 @@ export type MdxElement = {
|
|
|
88
132
|
children?: TinaRichTextNode[];
|
|
89
133
|
};
|
|
90
134
|
export type TinaRichTextContent = TinaRichTextRoot | TinaRichTextNode[] | null | undefined;
|
|
91
|
-
/**
|
|
92
|
-
|
|
135
|
+
/**
|
|
136
|
+
* An Astro (or framework) component that accepts props `P`. Astro's language
|
|
137
|
+
* server types an imported `.astro` component as `(props: Props) => any`, so
|
|
138
|
+
* this both accepts such a component and checks that its `Props` match what
|
|
139
|
+
* the renderer passes to the override.
|
|
140
|
+
*/
|
|
141
|
+
export type AstroComponentWithProps<P> = (props: P) => any;
|
|
142
|
+
/** Props passed to an `a` (link) override. */
|
|
143
|
+
export interface LinkComponentProps {
|
|
144
|
+
url: string;
|
|
145
|
+
}
|
|
146
|
+
/** Props passed to an `img` (image) override. */
|
|
147
|
+
export interface ImageComponentProps {
|
|
148
|
+
url: string;
|
|
149
|
+
alt?: string;
|
|
150
|
+
caption?: string;
|
|
151
|
+
}
|
|
152
|
+
/** Props passed to a `code_block` override. */
|
|
153
|
+
export interface CodeBlockComponentProps {
|
|
154
|
+
value: string;
|
|
155
|
+
lang?: string;
|
|
156
|
+
}
|
|
157
|
+
/** Props passed to `html` / `html_inline` overrides. */
|
|
158
|
+
export interface HtmlComponentProps {
|
|
159
|
+
value: string;
|
|
160
|
+
}
|
|
161
|
+
/** Props passed to a `table` override (native table node). */
|
|
162
|
+
export interface TableComponentProps {
|
|
163
|
+
node: TableElement;
|
|
164
|
+
}
|
|
165
|
+
/** Props passed to `td` / `th` overrides. */
|
|
166
|
+
export interface TableCellComponentProps {
|
|
167
|
+
align?: TableAlign;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Map of component name → Astro component, passed to `<TinaMarkdown>`.
|
|
171
|
+
*
|
|
172
|
+
* The built-in keys below are suggested by editor autocomplete and override
|
|
173
|
+
* the matching default element/tag; the named-prop overrides are typed with
|
|
174
|
+
* the exact props the renderer passes (e.g. `code_block` receives
|
|
175
|
+
* `{ value, lang }`).
|
|
176
|
+
*
|
|
177
|
+
* Custom rich-text **templates** render as mdxJsx nodes dispatched by `name`,
|
|
178
|
+
* with their fields passed as props. Register one under its template name.
|
|
179
|
+
* By default a custom key accepts any component; supply the optional `Custom`
|
|
180
|
+
* type param to type them precisely and have the registration checked:
|
|
181
|
+
*
|
|
182
|
+
* ```ts
|
|
183
|
+
* // template `cta` with a `title: string` field
|
|
184
|
+
* const components: CustomComponentsMap<{ cta: { title: string } }> = {
|
|
185
|
+
* cta: Cta, // ← Cta's Props must be assignable from `{ title: string }`
|
|
186
|
+
* };
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export type CustomComponentsMap<Custom extends Record<string, object> = Record<never, never>> = {
|
|
190
|
+
p?: AstroComponent;
|
|
191
|
+
h1?: AstroComponent;
|
|
192
|
+
h2?: AstroComponent;
|
|
193
|
+
h3?: AstroComponent;
|
|
194
|
+
h4?: AstroComponent;
|
|
195
|
+
h5?: AstroComponent;
|
|
196
|
+
h6?: AstroComponent;
|
|
197
|
+
ul?: AstroComponent;
|
|
198
|
+
ol?: AstroComponent;
|
|
199
|
+
li?: AstroComponent;
|
|
200
|
+
lic?: AstroComponent;
|
|
201
|
+
blockquote?: AstroComponent;
|
|
202
|
+
tr?: AstroComponent;
|
|
203
|
+
hr?: AstroComponent;
|
|
204
|
+
break?: AstroComponent;
|
|
205
|
+
a?: AstroComponentWithProps<LinkComponentProps>;
|
|
206
|
+
img?: AstroComponentWithProps<ImageComponentProps>;
|
|
207
|
+
code_block?: AstroComponentWithProps<CodeBlockComponentProps>;
|
|
208
|
+
html?: AstroComponentWithProps<HtmlComponentProps>;
|
|
209
|
+
html_inline?: AstroComponentWithProps<HtmlComponentProps>;
|
|
210
|
+
table?: AstroComponentWithProps<TableComponentProps>;
|
|
211
|
+
td?: AstroComponentWithProps<TableCellComponentProps>;
|
|
212
|
+
th?: AstroComponentWithProps<TableCellComponentProps>;
|
|
213
|
+
} & {
|
|
214
|
+
[K in keyof Custom]?: AstroComponentWithProps<Custom[K]>;
|
|
215
|
+
} & {
|
|
216
|
+
[name: string]: AstroComponent | undefined;
|
|
217
|
+
};
|
package/package.json
CHANGED
package/src/MdxNode.astro
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* mdxJsx{Flow,Text}Element nodes are dispatched by `node.name` — registered
|
|
4
4
|
* components on the `components` map render with the node's props spread in.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* When no component is registered for the name, an `mdxJsxFlowElement` named
|
|
7
|
+
* `table` renders the built-in legacy table (`MdxTableNode`), mirroring the
|
|
8
|
+
* React renderer. Any other unregistered name emits a visible placeholder so
|
|
9
|
+
* missing registrations surface during development.
|
|
7
10
|
*/
|
|
11
|
+
import MdxTableNode from './MdxTableNode.astro';
|
|
8
12
|
import type { CustomComponentsMap, MdxElement } from './types';
|
|
9
13
|
|
|
10
14
|
interface Props {
|
|
@@ -13,10 +17,17 @@ interface Props {
|
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
const { node, components } = Astro.props;
|
|
20
|
+
// Single place that guarantees mdx props are an object, so neither the
|
|
21
|
+
// component spread nor the legacy-table renderer downstream needs its own
|
|
22
|
+
// guard. Parsed content always sets props; this only defends malformed nodes.
|
|
23
|
+
const props: Record<string, unknown> = node.props ?? {};
|
|
16
24
|
const MdxComponent = components[node.name];
|
|
25
|
+
const isLegacyTable = !MdxComponent && node.name === 'table';
|
|
17
26
|
---
|
|
18
27
|
{MdxComponent ? (
|
|
19
|
-
<MdxComponent {...
|
|
28
|
+
<MdxComponent {...props} />
|
|
29
|
+
) : isLegacyTable ? (
|
|
30
|
+
<MdxTableNode node={{ ...node, props }} components={components} />
|
|
20
31
|
) : (
|
|
21
32
|
<span style="display:inline-block;padding:0.25rem 0.5rem;background:#fee;color:#900;border-radius:0.25rem;font-family:monospace;font-size:0.85em;">
|
|
22
33
|
No component provided for {node.name}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Renders the legacy MDX-flow table shape — an `mdxJsxFlowElement` named
|
|
4
|
+
* `table` whose cells live on `props.tableRows` (rather than as `tr`/`td`
|
|
5
|
+
* child nodes). Mirrors the matching branch of
|
|
6
|
+
* `packages/tinacms/src/rich-text/index.tsx`: when `props.firstRowHeader` is
|
|
7
|
+
* set the first row becomes a `<thead>` of `<th>` and the rest are `<td>` in
|
|
8
|
+
* `<tbody>`. Per-column alignment comes from `props.align`, and the `th` / `td`
|
|
9
|
+
* overrides apply. The `table` / `tr` overrides are intentionally NOT honored
|
|
10
|
+
* here: the `table` override is typed to receive a native `{ node:
|
|
11
|
+
* TableElement }`, which this legacy `MdxElement`-based shape can't provide.
|
|
12
|
+
*/
|
|
13
|
+
import TinaMarkdown from './TinaMarkdown.astro';
|
|
14
|
+
import type {
|
|
15
|
+
CustomComponentsMap,
|
|
16
|
+
InlineElement,
|
|
17
|
+
MdxElement,
|
|
18
|
+
MdxTableProps,
|
|
19
|
+
TinaRichTextContent,
|
|
20
|
+
} from './types';
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
node: MdxElement;
|
|
24
|
+
components: CustomComponentsMap;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { node, components } = Astro.props;
|
|
28
|
+
// `node.props` is guaranteed to be an object — MdxNode defaults it before
|
|
29
|
+
// dispatching here — so no further guard is needed at this layer.
|
|
30
|
+
const props = node.props as MdxTableProps;
|
|
31
|
+
|
|
32
|
+
const align = props.align ?? [];
|
|
33
|
+
const allRows = props.tableRows ?? [];
|
|
34
|
+
const header = props.firstRowHeader ? allRows.at(0) : undefined;
|
|
35
|
+
const bodyRows = props.firstRowHeader ? allRows.slice(1) : allRows;
|
|
36
|
+
|
|
37
|
+
const ThOverride = components.th;
|
|
38
|
+
const TdOverride = components.td;
|
|
39
|
+
|
|
40
|
+
// A cell `value` is rich text; unwrap each wrapping paragraph to its inline
|
|
41
|
+
// nodes so a cell renders `<td>text</td>` rather than `<td><p>text</p></td>`.
|
|
42
|
+
const cellInline = (value: TinaRichTextContent): InlineElement[] => {
|
|
43
|
+
const nodes = Array.isArray(value) ? value : (value?.children ?? []);
|
|
44
|
+
return nodes.flatMap((n) => (n.type === 'p' ? n.children : []));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const cellStyle = (i: number) => `text-align:${align[i] ?? 'auto'}`;
|
|
48
|
+
---
|
|
49
|
+
<table>
|
|
50
|
+
{header && (
|
|
51
|
+
<thead>
|
|
52
|
+
<tr>
|
|
53
|
+
{(header.tableCells ?? []).map((cell, i) =>
|
|
54
|
+
ThOverride ? (
|
|
55
|
+
<ThOverride align={align[i]}>
|
|
56
|
+
<TinaMarkdown content={cellInline(cell.value)} components={components} />
|
|
57
|
+
</ThOverride>
|
|
58
|
+
) : (
|
|
59
|
+
<th style={cellStyle(i)}>
|
|
60
|
+
<TinaMarkdown content={cellInline(cell.value)} components={components} />
|
|
61
|
+
</th>
|
|
62
|
+
)
|
|
63
|
+
)}
|
|
64
|
+
</tr>
|
|
65
|
+
</thead>
|
|
66
|
+
)}
|
|
67
|
+
<tbody>
|
|
68
|
+
{bodyRows.map((row) => (
|
|
69
|
+
<tr>
|
|
70
|
+
{(row?.tableCells ?? []).map((cell, i) =>
|
|
71
|
+
TdOverride ? (
|
|
72
|
+
<TdOverride align={align[i]}>
|
|
73
|
+
<TinaMarkdown content={cellInline(cell.value)} components={components} />
|
|
74
|
+
</TdOverride>
|
|
75
|
+
) : (
|
|
76
|
+
<td style={cellStyle(i)}>
|
|
77
|
+
<TinaMarkdown content={cellInline(cell.value)} components={components} />
|
|
78
|
+
</td>
|
|
79
|
+
)
|
|
80
|
+
)}
|
|
81
|
+
</tr>
|
|
82
|
+
))}
|
|
83
|
+
</tbody>
|
|
84
|
+
</table>
|
package/src/Node.astro
CHANGED
|
@@ -12,12 +12,14 @@ import ImageNode from './ImageNode.astro';
|
|
|
12
12
|
import Leaf from './Leaf.astro';
|
|
13
13
|
import LinkNode from './LinkNode.astro';
|
|
14
14
|
import MdxNode from './MdxNode.astro';
|
|
15
|
+
import TableNode from './TableNode.astro';
|
|
15
16
|
import type {
|
|
16
17
|
CodeBlockElement,
|
|
17
18
|
CustomComponentsMap,
|
|
18
19
|
ImageElement,
|
|
19
20
|
LinkElement,
|
|
20
21
|
MdxElement,
|
|
22
|
+
TableElement,
|
|
21
23
|
TextElement,
|
|
22
24
|
TinaRichTextNode,
|
|
23
25
|
} from './types';
|
|
@@ -54,6 +56,8 @@ const containerTypes = new Set([
|
|
|
54
56
|
<ImageNode node={node as ImageElement} components={components} />
|
|
55
57
|
) : t === 'code_block' ? (
|
|
56
58
|
<CodeBlockNode node={node as CodeBlockElement} components={components} />
|
|
59
|
+
) : t === 'table' ? (
|
|
60
|
+
<TableNode node={node as TableElement} components={components} />
|
|
57
61
|
) : t === 'text' ? (
|
|
58
62
|
<Leaf node={node as TextElement} />
|
|
59
63
|
) : t === 'mdxJsxFlowElement' || t === 'mdxJsxTextElement' ? (
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Renders a native markdown `table` node. Mirrors the `case 'table'` branch
|
|
4
|
+
* in `packages/tinacms/src/rich-text/index.tsx`: every row goes into a single
|
|
5
|
+
* `<tbody>` as `<td>` cells (the first row is NOT promoted to a header — that
|
|
6
|
+
* matches the React renderer's native-table path), per-column alignment comes
|
|
7
|
+
* from `node.props.align`, and `components.table` / `.tr` / `.td` override the
|
|
8
|
+
* default tags. Each cell's content is the inline content of its paragraph,
|
|
9
|
+
* rendered without a wrapping `<p>` (the equivalent of React's `p` → `td`
|
|
10
|
+
* override).
|
|
11
|
+
*/
|
|
12
|
+
import TinaMarkdown from './TinaMarkdown.astro';
|
|
13
|
+
import type {
|
|
14
|
+
CustomComponentsMap,
|
|
15
|
+
InlineElement,
|
|
16
|
+
TableCellElement,
|
|
17
|
+
TableElement,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
node: TableElement;
|
|
22
|
+
components: CustomComponentsMap;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { node, components } = Astro.props;
|
|
26
|
+
|
|
27
|
+
const TableOverride = components.table;
|
|
28
|
+
const TrOverride = components.tr;
|
|
29
|
+
const TdOverride = components.td;
|
|
30
|
+
|
|
31
|
+
const align = node.props?.align ?? [];
|
|
32
|
+
const rows = node.children ?? [];
|
|
33
|
+
|
|
34
|
+
const TABLE_STYLE = 'border:1px solid #EDECF3';
|
|
35
|
+
const tdStyle = (i: number) =>
|
|
36
|
+
`text-align:${align[i] ?? 'auto'};border:1px solid #EDECF3;padding:0.25rem`;
|
|
37
|
+
|
|
38
|
+
// A cell wraps its content in a paragraph; unwrap to the inline nodes so the
|
|
39
|
+
// cell renders `<td>text</td>` rather than `<td><p>text</p></td>`.
|
|
40
|
+
const cellContent = (cell: TableCellElement): InlineElement[] =>
|
|
41
|
+
(cell?.children ?? []).flatMap((paragraph) => paragraph?.children ?? []);
|
|
42
|
+
|
|
43
|
+
const Table = TableOverride ?? 'table';
|
|
44
|
+
const tableProps = TableOverride ? { node } : { style: TABLE_STYLE };
|
|
45
|
+
const Tr = TrOverride ?? 'tr';
|
|
46
|
+
---
|
|
47
|
+
<Table {...tableProps}>
|
|
48
|
+
<tbody>
|
|
49
|
+
{rows.map((row) => (
|
|
50
|
+
<Tr>
|
|
51
|
+
{(row?.children ?? []).map((cell, i) =>
|
|
52
|
+
TdOverride ? (
|
|
53
|
+
<TdOverride align={align[i]}>
|
|
54
|
+
<TinaMarkdown content={cellContent(cell)} components={components} />
|
|
55
|
+
</TdOverride>
|
|
56
|
+
) : (
|
|
57
|
+
<td style={tdStyle(i)}>
|
|
58
|
+
<TinaMarkdown content={cellContent(cell)} components={components} />
|
|
59
|
+
</td>
|
|
60
|
+
)
|
|
61
|
+
)}
|
|
62
|
+
</Tr>
|
|
63
|
+
))}
|
|
64
|
+
</tbody>
|
|
65
|
+
</Table>
|
|
@@ -6,6 +6,8 @@ import codeBlock from './fixtures/code-block.json';
|
|
|
6
6
|
import leafMarks from './fixtures/leaf-marks.json';
|
|
7
7
|
import mdxJsxFlow from './fixtures/mdx-jsx-flow.json';
|
|
8
8
|
import mdxJsxText from './fixtures/mdx-jsx-text.json';
|
|
9
|
+
import mdxTable from './fixtures/mdx-table.json';
|
|
10
|
+
import table from './fixtures/table.json';
|
|
9
11
|
|
|
10
12
|
let container: AstroContainer;
|
|
11
13
|
|
|
@@ -84,6 +86,51 @@ describe('TinaMarkdown', () => {
|
|
|
84
86
|
});
|
|
85
87
|
});
|
|
86
88
|
|
|
89
|
+
describe('TinaMarkdown — tables', () => {
|
|
90
|
+
it('renders a native table node with rows, cells and column alignment', async () => {
|
|
91
|
+
const html = await render({ props: { content: table } });
|
|
92
|
+
expect(html).toMatchSnapshot();
|
|
93
|
+
expect(html).toContain('<table');
|
|
94
|
+
expect(html).toContain('<tbody>');
|
|
95
|
+
expect(html).toContain('<tr>');
|
|
96
|
+
expect(html).toContain('<td');
|
|
97
|
+
// Per-column alignment from props.align.
|
|
98
|
+
expect(html).toContain('text-align:left');
|
|
99
|
+
expect(html).toContain('text-align:center');
|
|
100
|
+
expect(html).toContain('text-align:right');
|
|
101
|
+
// Inline marks render inside cells.
|
|
102
|
+
expect(html).toContain('<strong>Ada</strong>');
|
|
103
|
+
// No <thead>/<th> on the native path (matches the React renderer).
|
|
104
|
+
expect(html).not.toContain('<thead>');
|
|
105
|
+
expect(html).not.toContain('<th');
|
|
106
|
+
// Cell content is not wrapped in an extra <p>.
|
|
107
|
+
expect(html).not.toMatch(/<td[^>]*><p[\s>]/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('overrides the table tag via components.table', async () => {
|
|
111
|
+
const FancyTable = await import('./fixtures/FancyTable.astro');
|
|
112
|
+
const html = await render({
|
|
113
|
+
props: { content: table, components: { table: FancyTable.default } },
|
|
114
|
+
});
|
|
115
|
+
expect(html).toContain('class="fancy-table"');
|
|
116
|
+
expect(html).toContain('Name');
|
|
117
|
+
expect(html).toContain('Eng');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('renders the legacy mdx-flow table with a header row', async () => {
|
|
121
|
+
const html = await render({ props: { content: mdxTable } });
|
|
122
|
+
expect(html).toMatchSnapshot();
|
|
123
|
+
expect(html).toContain('<thead>');
|
|
124
|
+
expect(html).toContain('<th');
|
|
125
|
+
expect(html).toContain('Header A');
|
|
126
|
+
expect(html).toContain('<tbody>');
|
|
127
|
+
expect(html).toContain('a1');
|
|
128
|
+
expect(html).toContain('text-align:left');
|
|
129
|
+
expect(html).toContain('text-align:right');
|
|
130
|
+
expect(html).not.toContain('No component provided for table');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
87
134
|
describe('TinaMarkdown — components map', () => {
|
|
88
135
|
it('dispatches a registered mdxJsx component by name', async () => {
|
|
89
136
|
const MyFeature = await import('./fixtures/MyFeature.astro');
|
|
@@ -5,3 +5,7 @@ exports[`TinaMarkdown > renders code blocks with language class 1`] = `"<pre><co
|
|
|
5
5
|
exports[`TinaMarkdown > renders nested leaf-mark formatting 1`] = `"<p>Some <strong>bold</strong> text</p><p>Some <strong><em>bold and emphasized</em></strong> text</p><p>Marks with <em>emphasized text nesting </em><strong><em>bold</em></strong><em> text</em></p><p><strong>Hello </strong><strong><em>world</em></strong><strong>, again</strong> <em>here</em></p><p>Some <code>inline code</code> examples</p><p><em>Hello </em><em><code>some code</code></em><em>, again</em></p><p><strong>Hello </strong><a href="https://example.com"><strong>world</strong></a></p><p><strong><em>Hello </em></strong><a href="https://example.com"><strong><em>world</em></strong></a><em> And some other text, which has a </em><a href="https://something.com"><em>link to something</em></a></p>"`;
|
|
6
6
|
|
|
7
7
|
exports[`TinaMarkdown > renders the basic kitchen-sink fixture 1`] = `"<h1>Heading</h1><p>A paragraph.</p><blockquote>A quote.</blockquote><ul><li><div>An item</div></li></ul><hr><p>A <a href="http://example.com">link</a>.</p><p><img src="https://get.svg.workers.dev" alt="Alt Text"></p>"`;
|
|
8
|
+
|
|
9
|
+
exports[`TinaMarkdown — tables > renders a native table node with rows, cells and column alignment 1`] = `"<table style="border:1px solid #EDECF3"> <tbody> <tr><td style="text-align:left;border:1px solid #EDECF3;padding:0.25rem"> Name </td><td style="text-align:center;border:1px solid #EDECF3;padding:0.25rem"> Role </td><td style="text-align:right;border:1px solid #EDECF3;padding:0.25rem"> Score </td></tr><tr><td style="text-align:left;border:1px solid #EDECF3;padding:0.25rem"> <strong>Ada</strong> </td><td style="text-align:center;border:1px solid #EDECF3;padding:0.25rem"> Eng </td><td style="text-align:right;border:1px solid #EDECF3;padding:0.25rem"> 99 </td></tr> </tbody> </table>"`;
|
|
10
|
+
|
|
11
|
+
exports[`TinaMarkdown — tables > renders the legacy mdx-flow table with a header row 1`] = `"<table> <thead> <tr> <th style="text-align:left"> Header A </th><th style="text-align:right"> Header B </th> </tr> </thead> <tbody> <tr> <td style="text-align:left"> a1 </td><td style="text-align:right"> b1 </td> </tr> </tbody> </table>"`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "root",
|
|
3
|
+
"children": [
|
|
4
|
+
{
|
|
5
|
+
"type": "mdxJsxFlowElement",
|
|
6
|
+
"name": "table",
|
|
7
|
+
"props": {
|
|
8
|
+
"firstRowHeader": true,
|
|
9
|
+
"align": ["left", "right"],
|
|
10
|
+
"tableRows": [
|
|
11
|
+
{
|
|
12
|
+
"tableCells": [
|
|
13
|
+
{
|
|
14
|
+
"value": [
|
|
15
|
+
{ "type": "p", "children": [{ "type": "text", "text": "Header A" }] }
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"value": [
|
|
20
|
+
{ "type": "p", "children": [{ "type": "text", "text": "Header B" }] }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"tableCells": [
|
|
27
|
+
{
|
|
28
|
+
"value": [
|
|
29
|
+
{ "type": "p", "children": [{ "type": "text", "text": "a1" }] }
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"value": [
|
|
34
|
+
{ "type": "p", "children": [{ "type": "text", "text": "b1" }] }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"children": [{ "type": "text", "text": "" }]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "root",
|
|
3
|
+
"children": [
|
|
4
|
+
{
|
|
5
|
+
"type": "table",
|
|
6
|
+
"props": { "align": ["left", "center", "right"] },
|
|
7
|
+
"children": [
|
|
8
|
+
{
|
|
9
|
+
"type": "tr",
|
|
10
|
+
"children": [
|
|
11
|
+
{
|
|
12
|
+
"type": "td",
|
|
13
|
+
"children": [
|
|
14
|
+
{ "type": "p", "children": [{ "type": "text", "text": "Name" }] }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"type": "td",
|
|
19
|
+
"children": [
|
|
20
|
+
{ "type": "p", "children": [{ "type": "text", "text": "Role" }] }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "td",
|
|
25
|
+
"children": [
|
|
26
|
+
{ "type": "p", "children": [{ "type": "text", "text": "Score" }] }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "tr",
|
|
33
|
+
"children": [
|
|
34
|
+
{
|
|
35
|
+
"type": "td",
|
|
36
|
+
"children": [
|
|
37
|
+
{
|
|
38
|
+
"type": "p",
|
|
39
|
+
"children": [{ "type": "text", "text": "Ada", "bold": true }]
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"type": "td",
|
|
45
|
+
"children": [
|
|
46
|
+
{ "type": "p", "children": [{ "type": "text", "text": "Eng" }] }
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "td",
|
|
51
|
+
"children": [
|
|
52
|
+
{ "type": "p", "children": [{ "type": "text", "text": "99" }] }
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
@@ -12,11 +12,11 @@ type ConfigSetupArg = Parameters<NonNullable<Hooks['astro:config:setup']>>[0];
|
|
|
12
12
|
type ConfigDoneArg = Parameters<NonNullable<Hooks['astro:config:done']>>[0];
|
|
13
13
|
type BuildDoneArg = Parameters<NonNullable<Hooks['astro:build:done']>>[0];
|
|
14
14
|
|
|
15
|
-
function runConfigSetup() {
|
|
15
|
+
function runConfigSetup(options?: Parameters<typeof tina>[0]) {
|
|
16
16
|
const addMiddleware = vi.fn();
|
|
17
17
|
const updateConfig = vi.fn();
|
|
18
18
|
const logger = { warn: vi.fn(), info: vi.fn() };
|
|
19
|
-
const integration = tina();
|
|
19
|
+
const integration = tina(options);
|
|
20
20
|
(
|
|
21
21
|
integration.hooks['astro:config:setup'] as NonNullable<
|
|
22
22
|
Hooks['astro:config:setup']
|
|
@@ -28,6 +28,34 @@ function runConfigSetup() {
|
|
|
28
28
|
return { integration, addMiddleware, updateConfig, logger, plugins };
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Drive the astro:config:done hook with a chosen adapter so the integration can
|
|
32
|
+
// resolve whether the Cloudflare workaround should apply.
|
|
33
|
+
function runConfigDone(integration: AstroIntegration, adapterName?: string) {
|
|
34
|
+
(
|
|
35
|
+
integration.hooks['astro:config:done'] as NonNullable<
|
|
36
|
+
Hooks['astro:config:done']
|
|
37
|
+
>
|
|
38
|
+
)({
|
|
39
|
+
config: {
|
|
40
|
+
build: { client: pathToFileURL('/tmp/tina-client/') },
|
|
41
|
+
adapter: adapterName ? { name: adapterName, hooks: {} } : undefined,
|
|
42
|
+
},
|
|
43
|
+
} as unknown as ConfigDoneArg);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cfPlugin = (plugins: VitePlugin[]) =>
|
|
47
|
+
plugins.find(
|
|
48
|
+
(p) => p.name === '@tinacms/astro:cloudflare-import-meta-url'
|
|
49
|
+
) as VitePlugin;
|
|
50
|
+
|
|
51
|
+
// Invoke a plugin's `configEnvironment` hook (a function in our plugin) without
|
|
52
|
+
// a real Vite environment — it only branches on the environment name.
|
|
53
|
+
function runConfigEnvironment(plugin: VitePlugin, name: string) {
|
|
54
|
+
const hook = plugin.configEnvironment as any;
|
|
55
|
+
const fn = typeof hook === 'function' ? hook : hook?.handler;
|
|
56
|
+
return fn?.call({}, name, {}, {});
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
// Drive a Vite plugin's `configureServer` and capture the request handler it
|
|
32
60
|
// registers, so we can exercise it without a real dev server.
|
|
33
61
|
function devHandler(plugin: VitePlugin) {
|
|
@@ -96,6 +124,82 @@ describe('tina() integration — bridge dev plugin', () => {
|
|
|
96
124
|
});
|
|
97
125
|
});
|
|
98
126
|
|
|
127
|
+
describe('tina() integration — cloudflare import.meta.url plugin', () => {
|
|
128
|
+
const EXPECTED = JSON.stringify('file:///worker.mjs');
|
|
129
|
+
|
|
130
|
+
it('injects a valid import.meta.url define for the workerd server envs under the cloudflare adapter', () => {
|
|
131
|
+
const { integration, plugins } = runConfigSetup();
|
|
132
|
+
runConfigDone(integration, '@astrojs/cloudflare');
|
|
133
|
+
const plugin = cfPlugin(plugins);
|
|
134
|
+
expect(plugin).toBeDefined();
|
|
135
|
+
|
|
136
|
+
for (const env of ['ssr', 'astro']) {
|
|
137
|
+
const result = runConfigEnvironment(plugin, env);
|
|
138
|
+
expect(result?.define?.['import.meta.url']).toBe(EXPECTED);
|
|
139
|
+
// The placeholder must itself be a valid absolute URL.
|
|
140
|
+
expect(
|
|
141
|
+
() => new URL(JSON.parse(result.define['import.meta.url']))
|
|
142
|
+
).not.toThrow();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('never injects the define into the client or prerender envs', () => {
|
|
147
|
+
const { integration, plugins } = runConfigSetup();
|
|
148
|
+
runConfigDone(integration, '@astrojs/cloudflare');
|
|
149
|
+
const plugin = cfPlugin(plugins);
|
|
150
|
+
// `client` keeps the real import.meta.url; `prerender` may run in Node.
|
|
151
|
+
expect(runConfigEnvironment(plugin, 'client')).toBeUndefined();
|
|
152
|
+
expect(runConfigEnvironment(plugin, 'prerender')).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('does not inject the define for non-cloudflare adapters', () => {
|
|
156
|
+
for (const adapter of ['@astrojs/node', '@astrojs/vercel']) {
|
|
157
|
+
const { integration, plugins } = runConfigSetup();
|
|
158
|
+
runConfigDone(integration, adapter);
|
|
159
|
+
const plugin = cfPlugin(plugins);
|
|
160
|
+
for (const env of ['ssr', 'prerender', 'astro', 'client']) {
|
|
161
|
+
expect(runConfigEnvironment(plugin, env)).toBeUndefined();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('does not inject the define when no adapter is configured', () => {
|
|
167
|
+
const { integration, plugins } = runConfigSetup();
|
|
168
|
+
runConfigDone(integration);
|
|
169
|
+
expect(runConfigEnvironment(cfPlugin(plugins), 'ssr')).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('reads the adapter flag lazily, after config:done', () => {
|
|
173
|
+
const { integration, plugins } = runConfigSetup();
|
|
174
|
+
const plugin = cfPlugin(plugins);
|
|
175
|
+
// Before config:done the flag is false, so the plugin is inert.
|
|
176
|
+
expect(runConfigEnvironment(plugin, 'ssr')).toBeUndefined();
|
|
177
|
+
// The same plugin instance picks up the adapter once config:done runs.
|
|
178
|
+
runConfigDone(integration, '@astrojs/cloudflare');
|
|
179
|
+
expect(
|
|
180
|
+
runConfigEnvironment(plugin, 'ssr')?.define?.['import.meta.url']
|
|
181
|
+
).toBe(EXPECTED);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('honours the cloudflareWorkers option override', () => {
|
|
185
|
+
// Force on, even with a Node adapter.
|
|
186
|
+
const forcedOn = runConfigSetup({ cloudflareWorkers: true });
|
|
187
|
+
runConfigDone(forcedOn.integration, '@astrojs/node');
|
|
188
|
+
expect(
|
|
189
|
+
runConfigEnvironment(cfPlugin(forcedOn.plugins), 'ssr')?.define?.[
|
|
190
|
+
'import.meta.url'
|
|
191
|
+
]
|
|
192
|
+
).toBe(EXPECTED);
|
|
193
|
+
|
|
194
|
+
// Force off, even with the Cloudflare adapter.
|
|
195
|
+
const forcedOff = runConfigSetup({ cloudflareWorkers: false });
|
|
196
|
+
runConfigDone(forcedOff.integration, '@astrojs/cloudflare');
|
|
197
|
+
expect(
|
|
198
|
+
runConfigEnvironment(cfPlugin(forcedOff.plugins), 'ssr')
|
|
199
|
+
).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
99
203
|
describe('tina() integration — astro:build:done', () => {
|
|
100
204
|
it('emits bridge.js into the client output dir', () => {
|
|
101
205
|
const clientDir = mkdtempSync(join(tmpdir(), 'tina-client-'));
|
package/src/integration.ts
CHANGED
|
@@ -7,11 +7,32 @@ import type { Plugin as VitePlugin } from 'vite';
|
|
|
7
7
|
|
|
8
8
|
export interface TinaIntegrationOptions {
|
|
9
9
|
middlewareOrder?: 'pre' | 'post';
|
|
10
|
+
/**
|
|
11
|
+
* Force the Cloudflare Workers (workerd) `import.meta.url` workaround on or
|
|
12
|
+
* off. When omitted, it is applied automatically whenever the
|
|
13
|
+
* `@astrojs/cloudflare` adapter is detected. Set `false` to opt out, or
|
|
14
|
+
* `true` to force it for a custom Cloudflare setup.
|
|
15
|
+
*/
|
|
16
|
+
cloudflareWorkers?: boolean;
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
/** Where the injected bridge bootstrap imports the bridge bundle from. */
|
|
13
20
|
const BRIDGE_ROUTE = '/admin/bridge.js';
|
|
14
21
|
|
|
22
|
+
const CLOUDFLARE_ADAPTER_NAME = '@astrojs/cloudflare';
|
|
23
|
+
/** A valid absolute URL that stands in for `import.meta.url` in server bundles. */
|
|
24
|
+
const WORKERD_IMPORT_META_URL = JSON.stringify('file:///worker.mjs');
|
|
25
|
+
/**
|
|
26
|
+
* The Vite environments that run on workerd under `@astrojs/cloudflare` and
|
|
27
|
+
* therefore need the placeholder: `ssr` (the on-demand server build) and `astro`
|
|
28
|
+
* (the dev SSR runtime). The `client` bundle is excluded so the browser keeps
|
|
29
|
+
* the real `import.meta.url`, and `prerender` is excluded because it runs in
|
|
30
|
+
* real Node when `prerenderEnvironment: 'node'` (faking the URL there would
|
|
31
|
+
* break genuine file resolution) — and the island route is `prerender: false`,
|
|
32
|
+
* so it never renders during prerendering anyway.
|
|
33
|
+
*/
|
|
34
|
+
const SERVER_ENVIRONMENTS = new Set(['ssr', 'astro']);
|
|
35
|
+
|
|
15
36
|
export default function tina(
|
|
16
37
|
options: TinaIntegrationOptions = {}
|
|
17
38
|
): AstroIntegration {
|
|
@@ -19,6 +40,10 @@ export default function tina(
|
|
|
19
40
|
// Resolved client output dir, captured at config:done and consumed at
|
|
20
41
|
// build:done (only then is the final output location known).
|
|
21
42
|
let clientDir: URL | undefined;
|
|
43
|
+
// Whether the Cloudflare adapter is in use, resolved at config:done. The
|
|
44
|
+
// injected Vite plugin reads it lazily (via a thunk) because the plugin is
|
|
45
|
+
// constructed at config:setup, before the adapter is known.
|
|
46
|
+
let isCloudflareAdapter = false;
|
|
22
47
|
|
|
23
48
|
return {
|
|
24
49
|
name: '@tinacms/astro',
|
|
@@ -32,10 +57,23 @@ export default function tina(
|
|
|
32
57
|
// it into the user's source tree — config-time writes churn
|
|
33
58
|
// public/admin on every `astro dev`/`astro build`, break on read-only
|
|
34
59
|
// or sandboxed filesystems, and can race `tinacms build`.
|
|
35
|
-
updateConfig({
|
|
60
|
+
updateConfig({
|
|
61
|
+
vite: {
|
|
62
|
+
plugins: [
|
|
63
|
+
bridgeDevPlugin(),
|
|
64
|
+
cloudflareImportMetaUrlPlugin(() => isCloudflareAdapter),
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
});
|
|
36
68
|
},
|
|
37
69
|
'astro:config:done': ({ config }) => {
|
|
38
70
|
clientDir = config.build.client;
|
|
71
|
+
// config:done runs before Vite is created and after every integration
|
|
72
|
+
// (including a late-registered adapter) has resolved, so config.adapter
|
|
73
|
+
// is reliable here.
|
|
74
|
+
isCloudflareAdapter =
|
|
75
|
+
options.cloudflareWorkers ??
|
|
76
|
+
config.adapter?.name === CLOUDFLARE_ADAPTER_NAME;
|
|
39
77
|
},
|
|
40
78
|
'astro:build:done': ({ logger }) => {
|
|
41
79
|
// Build: emit the bridge next to the admin SPA in the *output* tree.
|
|
@@ -82,6 +120,36 @@ function bridgeDevPlugin(): VitePlugin {
|
|
|
82
120
|
};
|
|
83
121
|
}
|
|
84
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Cloudflare Workers (workerd) runs the server bundle in a runtime where the
|
|
125
|
+
* bundled `import.meta.url` is not a valid absolute URL. Astro's experimental
|
|
126
|
+
* Container API — used by the on-demand island route — calls
|
|
127
|
+
* `new URL(import.meta.url)` while building its manifest, which throws
|
|
128
|
+
* "Invalid URL string" and 500s the island render in production. The paths it
|
|
129
|
+
* seeds from that URL are never dereferenced for an in-memory render, so a
|
|
130
|
+
* valid placeholder is harmless. We inject it as a Vite `define`, but only in
|
|
131
|
+
* the server environments (never the client bundle, which needs the real value)
|
|
132
|
+
* and only under the Cloudflare adapter (faking it on a Node server would break
|
|
133
|
+
* real file resolution).
|
|
134
|
+
*
|
|
135
|
+
* TODO: remove once Astro guards the unconditional `new URL(import.meta.url)` in
|
|
136
|
+
* createManifest (tracked upstream in withastro/astro).
|
|
137
|
+
*/
|
|
138
|
+
function cloudflareImportMetaUrlPlugin(
|
|
139
|
+
isCloudflare: () => boolean
|
|
140
|
+
): VitePlugin {
|
|
141
|
+
const define = { 'import.meta.url': WORKERD_IMPORT_META_URL };
|
|
142
|
+
return {
|
|
143
|
+
name: '@tinacms/astro:cloudflare-import-meta-url',
|
|
144
|
+
// No `apply`: the define is needed in both the build (ssr) and dev envs.
|
|
145
|
+
configEnvironment(name) {
|
|
146
|
+
if (!isCloudflare()) return;
|
|
147
|
+
if (!SERVER_ENVIRONMENTS.has(name)) return;
|
|
148
|
+
return { define };
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
85
153
|
function emitBridgeAsset(adminDir: string, logger: AstroIntegrationLogger) {
|
|
86
154
|
try {
|
|
87
155
|
mkdirSync(adminDir, { recursive: true });
|
package/src/types.ts
CHANGED
|
@@ -44,8 +44,57 @@ export type CodeBlockElement = {
|
|
|
44
44
|
children?: { children: TextElement[] }[];
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
/** Per-column horizontal alignment for table cells. */
|
|
48
|
+
export type TableAlign = 'left' | 'right' | 'center';
|
|
49
|
+
|
|
50
|
+
/** A paragraph — the block wrapper Tina puts around inline content. */
|
|
51
|
+
export type ParagraphElement = {
|
|
52
|
+
type: 'p';
|
|
53
|
+
children: InlineElement[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** A single table cell. Its content is wrapped in a paragraph by the parser. */
|
|
57
|
+
export type TableCellElement = {
|
|
58
|
+
type: 'td';
|
|
59
|
+
children: ParagraphElement[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type TableRowElement = {
|
|
63
|
+
type: 'tr';
|
|
64
|
+
children: TableCellElement[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Native markdown table node (the shape `@tinacms/mdx` emits from a GFM
|
|
69
|
+
* table). Rows/cells are plain `tr`/`td`; per-column text-align lives on
|
|
70
|
+
* `props.align`. Mirrors `TableElement` in
|
|
71
|
+
* `packages/@tinacms/mdx/src/parse/plate.ts`.
|
|
72
|
+
*/
|
|
73
|
+
export type TableElement = {
|
|
74
|
+
type: 'table';
|
|
75
|
+
props?: { align?: TableAlign[] };
|
|
76
|
+
children: TableRowElement[];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** A cell in the legacy MDX-flow table; its `value` is itself rich text. */
|
|
80
|
+
export type MdxTableCell = { value: TinaRichTextContent };
|
|
81
|
+
|
|
82
|
+
export type MdxTableRow = { tableCells: MdxTableCell[] };
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Props of the legacy MDX-flow table — an `mdxJsxFlowElement` named `table`
|
|
86
|
+
* whose cells live on `props.tableRows` rather than as `tr`/`td` child nodes.
|
|
87
|
+
* Older editors produced this shape; `MdxTableNode.astro` still renders it.
|
|
88
|
+
*/
|
|
89
|
+
export type MdxTableProps = {
|
|
90
|
+
align?: TableAlign[];
|
|
91
|
+
/** When true, the first row is rendered as a `<thead>` of `<th>`. */
|
|
92
|
+
firstRowHeader?: boolean;
|
|
93
|
+
tableRows?: MdxTableRow[];
|
|
94
|
+
};
|
|
95
|
+
|
|
47
96
|
export type BlockElement =
|
|
48
|
-
|
|
|
97
|
+
| ParagraphElement
|
|
49
98
|
| {
|
|
50
99
|
type: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
51
100
|
children: InlineElement[];
|
|
@@ -58,6 +107,7 @@ export type BlockElement =
|
|
|
58
107
|
| { type: 'break' }
|
|
59
108
|
| ImageElement
|
|
60
109
|
| CodeBlockElement
|
|
110
|
+
| TableElement
|
|
61
111
|
| { type: 'maybe_mdx' }
|
|
62
112
|
| { type: 'html'; value: string }
|
|
63
113
|
| { type: 'invalid_markdown'; value: string };
|
|
@@ -93,5 +143,94 @@ export type TinaRichTextContent =
|
|
|
93
143
|
| null
|
|
94
144
|
| undefined;
|
|
95
145
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
146
|
+
/**
|
|
147
|
+
* An Astro (or framework) component that accepts props `P`. Astro's language
|
|
148
|
+
* server types an imported `.astro` component as `(props: Props) => any`, so
|
|
149
|
+
* this both accepts such a component and checks that its `Props` match what
|
|
150
|
+
* the renderer passes to the override.
|
|
151
|
+
*/
|
|
152
|
+
export type AstroComponentWithProps<P> = (props: P) => any;
|
|
153
|
+
|
|
154
|
+
/** Props passed to an `a` (link) override. */
|
|
155
|
+
export interface LinkComponentProps {
|
|
156
|
+
url: string;
|
|
157
|
+
}
|
|
158
|
+
/** Props passed to an `img` (image) override. */
|
|
159
|
+
export interface ImageComponentProps {
|
|
160
|
+
url: string;
|
|
161
|
+
alt?: string;
|
|
162
|
+
caption?: string;
|
|
163
|
+
}
|
|
164
|
+
/** Props passed to a `code_block` override. */
|
|
165
|
+
export interface CodeBlockComponentProps {
|
|
166
|
+
value: string;
|
|
167
|
+
lang?: string;
|
|
168
|
+
}
|
|
169
|
+
/** Props passed to `html` / `html_inline` overrides. */
|
|
170
|
+
export interface HtmlComponentProps {
|
|
171
|
+
value: string;
|
|
172
|
+
}
|
|
173
|
+
/** Props passed to a `table` override (native table node). */
|
|
174
|
+
export interface TableComponentProps {
|
|
175
|
+
node: TableElement;
|
|
176
|
+
}
|
|
177
|
+
/** Props passed to `td` / `th` overrides. */
|
|
178
|
+
export interface TableCellComponentProps {
|
|
179
|
+
align?: TableAlign;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Map of component name → Astro component, passed to `<TinaMarkdown>`.
|
|
184
|
+
*
|
|
185
|
+
* The built-in keys below are suggested by editor autocomplete and override
|
|
186
|
+
* the matching default element/tag; the named-prop overrides are typed with
|
|
187
|
+
* the exact props the renderer passes (e.g. `code_block` receives
|
|
188
|
+
* `{ value, lang }`).
|
|
189
|
+
*
|
|
190
|
+
* Custom rich-text **templates** render as mdxJsx nodes dispatched by `name`,
|
|
191
|
+
* with their fields passed as props. Register one under its template name.
|
|
192
|
+
* By default a custom key accepts any component; supply the optional `Custom`
|
|
193
|
+
* type param to type them precisely and have the registration checked:
|
|
194
|
+
*
|
|
195
|
+
* ```ts
|
|
196
|
+
* // template `cta` with a `title: string` field
|
|
197
|
+
* const components: CustomComponentsMap<{ cta: { title: string } }> = {
|
|
198
|
+
* cta: Cta, // ← Cta's Props must be assignable from `{ title: string }`
|
|
199
|
+
* };
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export type CustomComponentsMap<
|
|
203
|
+
Custom extends Record<string, object> = Record<never, never>,
|
|
204
|
+
> = {
|
|
205
|
+
// Block/inline overrides whose content comes through the default slot.
|
|
206
|
+
p?: AstroComponent;
|
|
207
|
+
h1?: AstroComponent;
|
|
208
|
+
h2?: AstroComponent;
|
|
209
|
+
h3?: AstroComponent;
|
|
210
|
+
h4?: AstroComponent;
|
|
211
|
+
h5?: AstroComponent;
|
|
212
|
+
h6?: AstroComponent;
|
|
213
|
+
ul?: AstroComponent;
|
|
214
|
+
ol?: AstroComponent;
|
|
215
|
+
li?: AstroComponent;
|
|
216
|
+
lic?: AstroComponent;
|
|
217
|
+
blockquote?: AstroComponent;
|
|
218
|
+
tr?: AstroComponent;
|
|
219
|
+
hr?: AstroComponent;
|
|
220
|
+
break?: AstroComponent;
|
|
221
|
+
// Overrides that receive named props.
|
|
222
|
+
a?: AstroComponentWithProps<LinkComponentProps>;
|
|
223
|
+
img?: AstroComponentWithProps<ImageComponentProps>;
|
|
224
|
+
code_block?: AstroComponentWithProps<CodeBlockComponentProps>;
|
|
225
|
+
html?: AstroComponentWithProps<HtmlComponentProps>;
|
|
226
|
+
html_inline?: AstroComponentWithProps<HtmlComponentProps>;
|
|
227
|
+
table?: AstroComponentWithProps<TableComponentProps>;
|
|
228
|
+
td?: AstroComponentWithProps<TableCellComponentProps>;
|
|
229
|
+
th?: AstroComponentWithProps<TableCellComponentProps>;
|
|
230
|
+
} & {
|
|
231
|
+
// Custom templates declared via the `Custom` param — typed by their props.
|
|
232
|
+
[K in keyof Custom]?: AstroComponentWithProps<Custom[K]>;
|
|
233
|
+
} & {
|
|
234
|
+
// Any other custom mdxJsx component name (matched by `node.name`).
|
|
235
|
+
[name: string]: AstroComponent | undefined;
|
|
236
|
+
};
|