@specverse/engines 4.2.1 → 4.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.
- package/dist/inference/core/specly-converter.d.ts.map +1 -1
- package/dist/inference/core/specly-converter.js +43 -22
- package/dist/inference/core/specly-converter.js.map +1 -1
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +29 -0
- package/dist/inference/index.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/react/index-html-generator.js +2 -2
- package/dist/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.js +291 -59
- package/dist/libs/instance-factories/applications/templates/react-starter/belongs-to.js +37 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.js +9 -124
- package/dist/libs/instance-factories/applications/templates/react-starter/detail-body-composer.js +9 -75
- package/dist/libs/instance-factories/applications/templates/react-starter/emit-jsx-source.js +129 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js +9 -212
- package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js +260 -5
- package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js +8 -50
- package/dist/libs/instance-factories/applications/templates/react-starter/operation-emitters.js +470 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/operation-view-generator.js +136 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/package-json-generator.js +2 -1
- package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +54 -61
- package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +115 -27
- package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +17 -9
- package/dist/libs/instance-factories/applications/templates/react-starter/use-api-hooks-starter-generator.js +49 -10
- package/dist/libs/instance-factories/applications/templates/react-starter/view-emitter.js +223 -10
- package/dist/libs/instance-factories/applications/templates/react-starter/views-generator.js +14 -1
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +13 -1
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +18 -0
- package/libs/instance-factories/applications/templates/react/index-html-generator.ts +2 -2
- package/libs/instance-factories/applications/templates/react-starter/__tests__/dashboard-body-composer.test.ts +3 -1
- package/libs/instance-factories/applications/templates/react-starter/__tests__/detail-body-composer.test.ts +24 -25
- package/libs/instance-factories/applications/templates/react-starter/__tests__/list-body-composer.test.ts +3 -3
- package/libs/instance-factories/applications/templates/react-starter/__tests__/orchestrator.test.ts +2 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p3-rendered-output.test.ts +1 -1
- package/libs/instance-factories/applications/templates/react-starter/__tests__/starter-generators.test.ts +5 -4
- package/libs/instance-factories/applications/templates/react-starter/__tests__/views-generator.test.ts +11 -4
- package/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.ts +377 -71
- package/libs/instance-factories/applications/templates/react-starter/belongs-to.ts +66 -0
- package/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.ts +15 -182
- package/libs/instance-factories/applications/templates/react-starter/detail-body-composer.ts +16 -128
- package/libs/instance-factories/applications/templates/react-starter/emit-jsx-source.ts +233 -0
- package/libs/instance-factories/applications/templates/react-starter/form-body-composer.ts +16 -376
- package/libs/instance-factories/applications/templates/react-starter/helpers-emitter.ts +282 -4
- package/libs/instance-factories/applications/templates/react-starter/list-body-composer.ts +26 -135
- package/libs/instance-factories/applications/templates/react-starter/operation-emitters.ts +497 -0
- package/libs/instance-factories/applications/templates/react-starter/operation-view-generator.ts +209 -0
- package/libs/instance-factories/applications/templates/react-starter/package-json-generator.ts +1 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +54 -61
- package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +115 -27
- package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +17 -9
- package/libs/instance-factories/applications/templates/react-starter/use-api-hooks-starter-generator.ts +71 -10
- package/libs/instance-factories/applications/templates/react-starter/view-emitter.ts +359 -18
- package/libs/instance-factories/applications/templates/react-starter/views-generator.ts +28 -1
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +13 -1
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +18 -0
- package/package.json +2 -1
package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js
CHANGED
|
@@ -7,18 +7,19 @@ function emitEntityDisplay() {
|
|
|
7
7
|
* @specverse/realize (ReactAppStarter); edit freely \u2014 nothing in
|
|
8
8
|
* the factory's regeneration will clobber your edits.
|
|
9
9
|
*/
|
|
10
|
-
export function getEntityDisplayName(entity:
|
|
11
|
-
if (!entity) return '';
|
|
10
|
+
export function getEntityDisplayName(entity: unknown): string {
|
|
11
|
+
if (!entity || typeof entity !== 'object') return '';
|
|
12
|
+
const record = entity as Record<string, unknown>;
|
|
12
13
|
|
|
13
14
|
const candidates = ['name', 'title', 'displayName', 'label', 'username', 'email'];
|
|
14
15
|
for (const key of candidates) {
|
|
15
|
-
const value =
|
|
16
|
+
const value = record[key];
|
|
16
17
|
if (typeof value === 'string' && value.trim().length > 0) {
|
|
17
18
|
return value;
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
const id =
|
|
22
|
+
const id = record.id;
|
|
22
23
|
if (typeof id === 'string') {
|
|
23
24
|
return id.length > 8 ? id.slice(0, 8) + '\u2026' : id;
|
|
24
25
|
}
|
|
@@ -44,8 +45,262 @@ export function resolveEntityDisplayName(
|
|
|
44
45
|
);
|
|
45
46
|
return match ? getEntityDisplayName(match) : String(id);
|
|
46
47
|
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format an arbitrary value for display in list cells, detail rows,
|
|
51
|
+
* and dashboard previews. Handles the cases where \`String(v ?? '')\`
|
|
52
|
+
* produces ugly output: nulls become em-dashes, booleans become
|
|
53
|
+
* "Yes"/"No", nested objects become \`{k: v, ...}\` summaries
|
|
54
|
+
* instead of "[object Object]", ISO datetimes are localized.
|
|
55
|
+
*
|
|
56
|
+
* Single source of truth for value display. Resolvers / emitters
|
|
57
|
+
* call this from expression strings.
|
|
58
|
+
*/
|
|
59
|
+
export function formatDisplayValue(value: unknown): string {
|
|
60
|
+
if (value === null || value === undefined) return '\u2014';
|
|
61
|
+
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
|
|
62
|
+
if (value instanceof Date) return value.toLocaleString();
|
|
63
|
+
if (typeof value === 'string' && /^\\d{4}-\\d{2}-\\d{2}T/.test(value)) {
|
|
64
|
+
return new Date(value).toLocaleString();
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(value)) return \`[\${value.length} items]\`;
|
|
67
|
+
if (typeof value === 'object') {
|
|
68
|
+
const keys = Object.keys(value as Record<string, unknown>);
|
|
69
|
+
if (keys.length === 0) return '{}';
|
|
70
|
+
if (keys.length <= 5) {
|
|
71
|
+
const rec = value as Record<string, unknown>;
|
|
72
|
+
return keys.map(k => \`\${k}: \${formatDisplayValue(rec[k])}\`).join(', ');
|
|
73
|
+
}
|
|
74
|
+
return \`{\${keys.length} fields}\`;
|
|
75
|
+
}
|
|
76
|
+
return String(value);
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
function emitUseAppEvents() {
|
|
81
|
+
return `/**
|
|
82
|
+
* AppEventsProvider + useAppEvents + useRecentEvents
|
|
83
|
+
*
|
|
84
|
+
* One WebSocket connection to the backend's /ws, shared across the
|
|
85
|
+
* app. Two consumers:
|
|
86
|
+
* - useAppEvents(): React Query invalidation on mutations (no-op
|
|
87
|
+
* call \u2014 side effect is via the provider).
|
|
88
|
+
* - useRecentEvents(): the Events tab's scrolling stream display.
|
|
89
|
+
*
|
|
90
|
+
* Inlined into this project by @specverse/realize (ReactAppStarter).
|
|
91
|
+
* Edit freely.
|
|
92
|
+
*/
|
|
93
|
+
import { useEffect, useState, useContext, createContext, type ReactNode } from 'react';
|
|
94
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
95
|
+
|
|
96
|
+
export interface AppEvent {
|
|
97
|
+
event: string;
|
|
98
|
+
payload: any;
|
|
99
|
+
timestamp: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface AppEventsCtx {
|
|
103
|
+
recentEvents: AppEvent[];
|
|
104
|
+
clear: () => void;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const EventsContext = createContext<AppEventsCtx | null>(null);
|
|
108
|
+
|
|
109
|
+
function pluralize(s: string): string {
|
|
110
|
+
if (/[^aeiou]y$/i.test(s)) return s.slice(0, -1) + 'ies';
|
|
111
|
+
if (/(s|x|z|ch|sh)$/i.test(s)) return s + 'es';
|
|
112
|
+
return s + 's';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function deriveWsUrl(): string {
|
|
116
|
+
const apiBase = import.meta.env.VITE_API_BASE_URL || '';
|
|
117
|
+
if (apiBase) return apiBase.replace(/^http/, 'ws') + '/ws';
|
|
118
|
+
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
119
|
+
return \`\${proto}//\${window.location.host}/ws\`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Parse "CategoryCreated" \u2192 "Category". Returns null for event names
|
|
123
|
+
* that don't end in one of the 4 CURED verbs. */
|
|
124
|
+
function modelFromEvent(eventName: string): string | null {
|
|
125
|
+
const m = eventName.match(/^(.+)(Created|Updated|Deleted|Evolved)$/);
|
|
126
|
+
return m ? m[1] : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Provider \u2014 wrap the app once. Maintains a single WS connection,
|
|
131
|
+
* invalidates React Query caches on mutation events, and keeps a
|
|
132
|
+
* buffer of recent events (default 100) for the Events tab.
|
|
133
|
+
*/
|
|
134
|
+
export function AppEventsProvider({ children, bufferSize = 100 }: { children: ReactNode; bufferSize?: number }) {
|
|
135
|
+
const qc = useQueryClient();
|
|
136
|
+
const [recentEvents, setRecentEvents] = useState<AppEvent[]>([]);
|
|
137
|
+
|
|
138
|
+
const clear = () => setRecentEvents([]);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
let cancelled = false;
|
|
142
|
+
let ws: WebSocket | null = null;
|
|
143
|
+
let reconnectDelay = 500;
|
|
144
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
145
|
+
|
|
146
|
+
const connect = () => {
|
|
147
|
+
if (cancelled) return;
|
|
148
|
+
try {
|
|
149
|
+
ws = new WebSocket(deriveWsUrl());
|
|
150
|
+
} catch {
|
|
151
|
+
scheduleReconnect();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
ws.addEventListener('open', () => {
|
|
156
|
+
if (cancelled) { ws?.close(); return; }
|
|
157
|
+
reconnectDelay = 500;
|
|
158
|
+
ws?.send(JSON.stringify({ type: 'subscribe-all' }));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
ws.addEventListener('message', (evt) => {
|
|
162
|
+
if (cancelled) return;
|
|
163
|
+
let data: any;
|
|
164
|
+
try { data = JSON.parse(evt.data as string); } catch { return; }
|
|
165
|
+
if (data?.type !== 'event') return;
|
|
166
|
+
const appEvent: AppEvent = {
|
|
167
|
+
event: data.event,
|
|
168
|
+
payload: data.payload,
|
|
169
|
+
timestamp: data.timestamp ?? new Date().toISOString(),
|
|
170
|
+
};
|
|
171
|
+
// Append + cap at bufferSize.
|
|
172
|
+
setRecentEvents(prev => {
|
|
173
|
+
const next = [...prev, appEvent];
|
|
174
|
+
return next.length > bufferSize ? next.slice(-bufferSize) : next;
|
|
175
|
+
});
|
|
176
|
+
// Invalidate React Query cache for affected model.
|
|
177
|
+
const model = modelFromEvent(data.event);
|
|
178
|
+
if (model) {
|
|
179
|
+
const resource = pluralize(model).toLowerCase();
|
|
180
|
+
qc.invalidateQueries({ queryKey: [resource] });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
ws.addEventListener('close', () => {
|
|
185
|
+
if (!cancelled) scheduleReconnect();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
ws.addEventListener('error', () => {
|
|
189
|
+
try { ws?.close(); } catch { /* ignore */ }
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const scheduleReconnect = () => {
|
|
194
|
+
if (cancelled) return;
|
|
195
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
196
|
+
reconnectTimer = setTimeout(() => {
|
|
197
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 10_000);
|
|
198
|
+
connect();
|
|
199
|
+
}, reconnectDelay);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Microtask-deferred connect \u2014 StrictMode-safe (synchronous
|
|
203
|
+
// mount/unmount/remount won't leave a half-open socket).
|
|
204
|
+
Promise.resolve().then(() => { if (!cancelled) connect(); });
|
|
205
|
+
|
|
206
|
+
return () => {
|
|
207
|
+
cancelled = true;
|
|
208
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
209
|
+
if (ws && ws.readyState === WebSocket.CONNECTING) {
|
|
210
|
+
ws.addEventListener('open', () => ws?.close(), { once: true });
|
|
211
|
+
} else if (ws) {
|
|
212
|
+
try { ws.close(); } catch { /* ignore */ }
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}, [qc, bufferSize]);
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<EventsContext.Provider value={{ recentEvents, clear }}>
|
|
219
|
+
{children}
|
|
220
|
+
</EventsContext.Provider>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Shorthand for components that don't care about the buffer \u2014 they
|
|
226
|
+
* get live-refresh behavior "for free" because the provider runs
|
|
227
|
+
* query invalidation. Kept as a no-op hook so sites that called it
|
|
228
|
+
* under the old API still type-check and still document intent.
|
|
229
|
+
*/
|
|
230
|
+
export function useAppEvents() {
|
|
231
|
+
// No-op \u2014 the Provider does the work. Retained to preserve intent
|
|
232
|
+
// at call sites like "the app needs live refresh".
|
|
233
|
+
useContext(EventsContext);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Read the recent-events buffer \u2014 used by the Events tab to render
|
|
238
|
+
* a scrolling stream. Returns [] if no AppEventsProvider is mounted.
|
|
239
|
+
*/
|
|
240
|
+
export function useRecentEvents(): AppEventsCtx {
|
|
241
|
+
return useContext(EventsContext) ?? { recentEvents: [], clear: () => {} };
|
|
242
|
+
}
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
function emitUseResizableSidebar() {
|
|
246
|
+
return `/**
|
|
247
|
+
* useResizableSidebar \u2014 drag-to-resize sidebar width.
|
|
248
|
+
*
|
|
249
|
+
* Inlined into this project by @specverse/realize (ReactAppStarter);
|
|
250
|
+
* edit freely \u2014 nothing in the factory's regeneration will clobber
|
|
251
|
+
* your edits.
|
|
252
|
+
*
|
|
253
|
+
* Returns { width, isResizing, startResizing } \u2014 width is the current
|
|
254
|
+
* pixel value, isResizing is true during an active drag, startResizing
|
|
255
|
+
* is an onMouseDown handler to attach to the drag handle element.
|
|
256
|
+
*/
|
|
257
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
258
|
+
|
|
259
|
+
export interface ResizableSidebarOptions {
|
|
260
|
+
defaultWidth?: number;
|
|
261
|
+
minWidth?: number;
|
|
262
|
+
maxWidth?: number;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function useResizableSidebar(options: ResizableSidebarOptions = {}) {
|
|
266
|
+
const { defaultWidth = 256, minWidth = 200, maxWidth = 500 } = options;
|
|
267
|
+
const [width, setWidth] = useState(defaultWidth);
|
|
268
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
269
|
+
|
|
270
|
+
const startResizing = useCallback(() => {
|
|
271
|
+
setIsResizing(true);
|
|
272
|
+
}, []);
|
|
273
|
+
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (!isResizing) return;
|
|
276
|
+
|
|
277
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
278
|
+
const newWidth = Math.min(maxWidth, Math.max(minWidth, e.clientX));
|
|
279
|
+
setWidth(newWidth);
|
|
280
|
+
};
|
|
281
|
+
const handleMouseUp = () => {
|
|
282
|
+
setIsResizing(false);
|
|
283
|
+
document.body.style.cursor = '';
|
|
284
|
+
document.body.style.userSelect = '';
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
document.body.style.cursor = 'col-resize';
|
|
288
|
+
document.body.style.userSelect = 'none';
|
|
289
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
290
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
291
|
+
|
|
292
|
+
return () => {
|
|
293
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
294
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
295
|
+
};
|
|
296
|
+
}, [isResizing, minWidth, maxWidth]);
|
|
297
|
+
|
|
298
|
+
return { width, isResizing, startResizing };
|
|
299
|
+
}
|
|
47
300
|
`;
|
|
48
301
|
}
|
|
49
302
|
export {
|
|
50
|
-
emitEntityDisplay
|
|
303
|
+
emitEntityDisplay,
|
|
304
|
+
emitUseAppEvents,
|
|
305
|
+
emitUseResizableSidebar
|
|
51
306
|
};
|
package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js
CHANGED
|
@@ -1,55 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { htmlToJsx } from "./html-to-jsx.js";
|
|
4
|
-
import { buildFKMap } from "./belongs-to.js";
|
|
5
|
-
const TBODY_SENTINEL = "__SPECVERSE_TBODY_ROWS__";
|
|
1
|
+
import { walkPattern } from "@specverse/runtime/views/core";
|
|
2
|
+
import { emitJsxSource } from "./emit-jsx-source.js";
|
|
6
3
|
function composeListBody(context) {
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const headers = columns.map((col) => {
|
|
13
|
-
const fk = fkMap.get(col);
|
|
14
|
-
return humanize(fk ? fk.name : col);
|
|
4
|
+
const imports = /* @__PURE__ */ new Set();
|
|
5
|
+
const nodes = walkPattern("list-view", {
|
|
6
|
+
model: context.model,
|
|
7
|
+
modelSchemas: context.modelSchemas,
|
|
8
|
+
view: context.view
|
|
15
9
|
});
|
|
16
|
-
|
|
17
|
-
const shellHtml = adapter.components.table.render({
|
|
18
|
-
properties: { columns: headers },
|
|
19
|
-
children: TBODY_SENTINEL
|
|
20
|
-
});
|
|
21
|
-
const shellJsx = htmlToJsx(shellHtml);
|
|
22
|
-
const rowMap = buildRowMap(columns, fkMap);
|
|
23
|
-
if (!shellJsx.includes(TBODY_SENTINEL)) {
|
|
24
|
-
throw new Error(
|
|
25
|
-
"composeListBody: tbody sentinel not present in adapter output. The canonical Tailwind adapter may have changed its table rendering."
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
return shellJsx.replace(TBODY_SENTINEL, rowMap);
|
|
29
|
-
}
|
|
30
|
-
function inferColumns(context) {
|
|
31
|
-
return inferFieldsFromSchema(context.modelSchemas, context.model.name);
|
|
32
|
-
}
|
|
33
|
-
function humanize(name) {
|
|
34
|
-
return name.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase()).trim();
|
|
35
|
-
}
|
|
36
|
-
function buildRowMap(columns, fkMap) {
|
|
37
|
-
const cells = columns.map((col) => {
|
|
38
|
-
const fk = fkMap.get(col);
|
|
39
|
-
const expr = fk ? `{resolveEntityDisplayName((item as any).${col}, ${fk.name}Options)}` : `{String((item as any).${col} ?? '')}`;
|
|
40
|
-
return ` <td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">${expr}</td>`;
|
|
41
|
-
}).join("\n");
|
|
42
|
-
return [
|
|
43
|
-
"{filtered.map((item, idx) => (",
|
|
44
|
-
" <tr",
|
|
45
|
-
" key={idx}",
|
|
46
|
-
" onClick={() => onSelect?.(item)}",
|
|
47
|
-
' className="hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"',
|
|
48
|
-
" >",
|
|
49
|
-
cells,
|
|
50
|
-
" </tr>",
|
|
51
|
-
"))}"
|
|
52
|
-
].join("\n");
|
|
10
|
+
return emitJsxSource(nodes, { imports, indent: 6 });
|
|
53
11
|
}
|
|
54
12
|
export {
|
|
55
13
|
composeListBody
|