koguma 0.6.6 → 2.0.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 (44) hide show
  1. package/README.md +109 -139
  2. package/cli/auth.ts +101 -0
  3. package/cli/config.ts +149 -0
  4. package/cli/constants.ts +38 -0
  5. package/cli/content.ts +503 -0
  6. package/cli/dev-sync.ts +305 -0
  7. package/cli/exec.ts +61 -0
  8. package/cli/index.ts +779 -1545
  9. package/cli/log.ts +49 -0
  10. package/cli/preflight.ts +105 -0
  11. package/cli/scaffold.ts +680 -0
  12. package/cli/typegen.ts +190 -0
  13. package/cli/ui.ts +55 -0
  14. package/cli/wrangler.ts +367 -0
  15. package/package.json +7 -4
  16. package/src/admin/_bundle.ts +1 -1
  17. package/src/api/router.integration.test.ts +63 -80
  18. package/src/api/router.ts +85 -59
  19. package/src/config/define.ts +1 -1
  20. package/src/config/field.ts +10 -9
  21. package/src/config/index.ts +1 -13
  22. package/src/config/meta.ts +7 -7
  23. package/src/config/types.ts +1 -95
  24. package/src/db/init.ts +68 -0
  25. package/src/db/queries.ts +120 -211
  26. package/src/db/sql.ts +10 -25
  27. package/src/media/index.ts +105 -47
  28. package/src/react/Markdown.test.tsx +195 -0
  29. package/src/react/Markdown.tsx +40 -0
  30. package/src/react/index.ts +6 -22
  31. package/src/react/types.ts +3 -112
  32. package/src/db/migrate.ts +0 -182
  33. package/src/db/schema.ts +0 -122
  34. package/src/react/RichText.test.tsx +0 -535
  35. package/src/react/RichText.tsx +0 -350
  36. package/src/rich-text/index.ts +0 -4
  37. package/src/rich-text/koguma-to-lexical.ts +0 -340
  38. package/src/rich-text/lexical-compat.test.ts +0 -513
  39. package/src/rich-text/lexical-to-koguma.test.ts +0 -906
  40. package/src/rich-text/lexical-to-koguma.ts +0 -400
  41. package/src/rich-text/markdown-to-koguma.ts +0 -164
  42. package/src/rich-text/plain.test.ts +0 -208
  43. package/src/rich-text/plain.ts +0 -114
  44. package/src/rich-text/snapshots.test.ts +0 -284
@@ -1,513 +0,0 @@
1
- /**
2
- * lexical-compat.test.ts
3
- *
4
- * Compatibility matrix — ensures lexicalToKoguma() correctly handles every
5
- * node type registered in koguma's admin editor (admin/src/components/blocks/editor-x/nodes.ts).
6
- *
7
- * Uses real-world JSON fixtures that mirror what Lexical actually serializes.
8
- * No Lexical package import required — purely JSON-driven.
9
- */
10
- import { describe, test, expect } from 'bun:test';
11
- import { lexicalToKoguma } from './lexical-to-koguma.ts';
12
-
13
- // ── Node registry (mirrors admin/src/components/blocks/editor-x/nodes.ts) ──
14
- // This list defines what MUST be handled (or intentionally stripped).
15
- // If you add a node to the editor, add a fixture and assertion here.
16
-
17
- const NODE_REGISTRY = [
18
- // Core Lexical
19
- 'root', // handled: wraps everything
20
- 'paragraph', // → { type: 'paragraph' }
21
- 'text', // → { type: 'text' }
22
- 'linebreak', // → { type: 'line-break' } inline
23
- // @lexical/rich-text
24
- 'heading', // → { type: 'heading', level }
25
- 'quote', // → { type: 'quote' }
26
- // @lexical/list
27
- 'list', // → { type: 'list', ordered, items }
28
- 'listitem', // → KogumaListItem
29
- // @lexical/link
30
- 'link', // → { type: 'link' }
31
- 'autolink', // → { type: 'link' } (same shape)
32
- // @lexical/table
33
- 'table', // → { type: 'table' }
34
- 'tablerow', // → KogumaTableRow
35
- 'tablecell', // → KogumaTableCell
36
- // @lexical/code
37
- 'code', // → { type: 'code', text }
38
- 'code-highlight', // → concatenated into parent code.text
39
- // @lexical/overflow
40
- 'overflow', // STRIPPED: editor UI only
41
- // @lexical/hashtag
42
- 'hashtag', // → { type: 'custom', name: 'hashtag' } inline
43
- // Custom admin nodes
44
- 'image', // → { type: 'image' }
45
- 'tweet', // → { type: 'custom', name: 'tweet' }
46
- 'youtube', // → { type: 'custom', name: 'youtube' }
47
- 'layoutcontainer', // → { type: 'layout', columns }
48
- 'layoutitem', // handled by layoutcontainer conversion
49
- 'mention', // → { type: 'custom', name: 'mention' } inline
50
- 'emoji', // → { type: 'text' } (flattened)
51
- 'keyword', // STRIPPED: editor UI only
52
- 'autocomplete' // STRIPPED: editor UI only
53
- ] as const;
54
-
55
- // Helper: wrap node(s) in a Lexical root state
56
- function state(children: Record<string, unknown>[]): unknown {
57
- return { root: { type: 'root', version: 1, children } };
58
- }
59
- function para(children: Record<string, unknown>[]): Record<string, unknown> {
60
- return { type: 'paragraph', version: 1, format: 0, children };
61
- }
62
- function text(t: string, format = 0): Record<string, unknown> {
63
- return { type: 'text', text: t, format, version: 1 };
64
- }
65
-
66
- // ── Per-node assertions ───────────────────────────────────────────────
67
-
68
- describe('Lexical compat — registered node coverage', () => {
69
- test('paragraph → { type: "paragraph" }', () => {
70
- const doc = lexicalToKoguma(state([para([text('Hello')])]));
71
- expect(doc.nodes[0].type).toBe('paragraph');
72
- });
73
-
74
- test('text → { type: "text" } inline', () => {
75
- const doc = lexicalToKoguma(state([para([text('World')])]));
76
- const p = doc.nodes[0] as {
77
- children: Array<{ type: string; text: string }>;
78
- };
79
- expect(p.children[0].type).toBe('text');
80
- expect(p.children[0].text).toBe('World');
81
- });
82
-
83
- test('linebreak → { type: "line-break" } inline', () => {
84
- const doc = lexicalToKoguma(
85
- state([para([text('A'), { type: 'linebreak', version: 1 }, text('B')])])
86
- );
87
- const p = doc.nodes[0] as { children: Array<{ type: string }> };
88
- expect(p.children[1].type).toBe('line-break');
89
- });
90
-
91
- test('heading h1–h6 → { type: "heading", level }', () => {
92
- for (let level = 1; level <= 6; level++) {
93
- const doc = lexicalToKoguma(
94
- state([
95
- {
96
- type: 'heading',
97
- version: 1,
98
- tag: `h${level}`,
99
- children: [text(`H${level}`)]
100
- }
101
- ])
102
- );
103
- const h = doc.nodes[0] as { type: string; level: number };
104
- expect(h.type).toBe('heading');
105
- expect(h.level).toBe(level);
106
- }
107
- });
108
-
109
- test('quote → { type: "quote" }', () => {
110
- const doc = lexicalToKoguma(
111
- state([
112
- {
113
- type: 'quote',
114
- version: 1,
115
- children: [para([text('Wise words')])]
116
- }
117
- ])
118
- );
119
- expect(doc.nodes[0].type).toBe('quote');
120
- });
121
-
122
- test('list (bullet) → { type: "list", ordered: false }', () => {
123
- const doc = lexicalToKoguma(
124
- state([
125
- {
126
- type: 'list',
127
- version: 1,
128
- listType: 'bullet',
129
- start: 1,
130
- tag: 'ul',
131
- children: [
132
- { type: 'listitem', version: 1, value: 1, children: [text('Item')] }
133
- ]
134
- }
135
- ])
136
- );
137
- const l = doc.nodes[0] as { ordered: boolean; items: unknown[] };
138
- expect(l.ordered).toBe(false);
139
- expect(l.items).toHaveLength(1);
140
- });
141
-
142
- test('list (number) → { type: "list", ordered: true }', () => {
143
- const doc = lexicalToKoguma(
144
- state([
145
- {
146
- type: 'list',
147
- version: 1,
148
- listType: 'number',
149
- start: 1,
150
- tag: 'ol',
151
- children: [
152
- { type: 'listitem', version: 1, value: 1, children: [text('One')] }
153
- ]
154
- }
155
- ])
156
- );
157
- expect((doc.nodes[0] as { ordered: boolean }).ordered).toBe(true);
158
- });
159
-
160
- test('list (check) → items with checked property', () => {
161
- const doc = lexicalToKoguma(
162
- state([
163
- {
164
- type: 'list',
165
- version: 1,
166
- listType: 'check',
167
- start: 1,
168
- tag: 'ul',
169
- children: [
170
- {
171
- type: 'listitem',
172
- version: 1,
173
- value: 1,
174
- checked: true,
175
- children: [text('Done')]
176
- },
177
- {
178
- type: 'listitem',
179
- version: 1,
180
- value: 2,
181
- checked: false,
182
- children: [text('Todo')]
183
- }
184
- ]
185
- }
186
- ])
187
- );
188
- const items = (doc.nodes[0] as { items: Array<{ checked?: boolean }> })
189
- .items;
190
- expect(items[0].checked).toBe(true);
191
- expect(items[1].checked).toBe(false);
192
- });
193
-
194
- test('link → { type: "link" } inline', () => {
195
- const doc = lexicalToKoguma(
196
- state([
197
- para([
198
- {
199
- type: 'link',
200
- version: 1,
201
- url: 'https://koguma.dev',
202
- target: null,
203
- children: [text('Koguma')]
204
- }
205
- ])
206
- ])
207
- );
208
- const p = doc.nodes[0] as { children: Array<{ type: string }> };
209
- expect(p.children[0].type).toBe('link');
210
- });
211
-
212
- test('autolink → { type: "link" } (same as link)', () => {
213
- const doc = lexicalToKoguma(
214
- state([
215
- para([
216
- {
217
- type: 'autolink',
218
- version: 1,
219
- url: 'https://auto.link',
220
- target: null,
221
- children: [text('auto')]
222
- }
223
- ])
224
- ])
225
- );
226
- const p = doc.nodes[0] as { children: Array<{ type: string }> };
227
- expect(p.children[0].type).toBe('link');
228
- });
229
-
230
- test('table → { type: "table" } with rows/cells', () => {
231
- const doc = lexicalToKoguma(
232
- state([
233
- {
234
- type: 'table',
235
- version: 1,
236
- children: [
237
- {
238
- type: 'tablerow',
239
- version: 1,
240
- children: [
241
- { type: 'tablecell', version: 1, children: [text('Col A')] }
242
- ]
243
- }
244
- ]
245
- }
246
- ])
247
- );
248
- expect(doc.nodes[0].type).toBe('table');
249
- const t = doc.nodes[0] as { rows: Array<{ cells: unknown[] }> };
250
- expect(t.rows[0].cells).toHaveLength(1);
251
- });
252
-
253
- test('code + code-highlight children → { type: "code", text }', () => {
254
- const doc = lexicalToKoguma(
255
- state([
256
- {
257
- type: 'code',
258
- version: 1,
259
- language: 'js',
260
- children: [
261
- {
262
- type: 'code-highlight',
263
- text: 'let',
264
- highlightType: 'keyword',
265
- version: 1
266
- },
267
- { type: 'text', text: ' x = 1', format: 0, version: 1 }
268
- ]
269
- }
270
- ])
271
- );
272
- const code = doc.nodes[0] as { type: string; text: string };
273
- expect(code.type).toBe('code');
274
- expect(code.text).toBe('let x = 1');
275
- });
276
-
277
- test('overflow → stripped (null/not in output)', () => {
278
- const doc = lexicalToKoguma(
279
- state([
280
- para([text('Real')]),
281
- { type: 'overflow', version: 1, children: [] }
282
- ])
283
- );
284
- expect(doc.nodes).toHaveLength(1);
285
- });
286
-
287
- test('hashtag → { type: "custom", name: "hashtag" } inline', () => {
288
- const doc = lexicalToKoguma(
289
- state([
290
- para([
291
- { type: 'hashtag', version: 1, hashtag: 'openai', text: '#openai' }
292
- ])
293
- ])
294
- );
295
- const p = doc.nodes[0] as {
296
- children: Array<{
297
- type: string;
298
- name: string;
299
- data: Record<string, unknown>;
300
- }>;
301
- };
302
- expect(p.children[0].type).toBe('custom');
303
- expect(p.children[0].name).toBe('hashtag');
304
- expect(p.children[0].data.tag).toBe('openai');
305
- });
306
-
307
- test('ImageNode → { type: "image" } block', () => {
308
- const doc = lexicalToKoguma(
309
- state([
310
- {
311
- type: 'image',
312
- version: 1,
313
- src: 'https://cdn.example.com/bear.jpg',
314
- altText: 'A bear',
315
- width: 1200,
316
- height: 800,
317
- children: []
318
- }
319
- ])
320
- );
321
- const img = doc.nodes[0] as {
322
- type: string;
323
- url: string;
324
- alt?: string;
325
- width?: number;
326
- };
327
- expect(img.type).toBe('image');
328
- expect(img.url).toBe('https://cdn.example.com/bear.jpg');
329
- expect(img.alt).toBe('A bear');
330
- expect(img.width).toBe(1200);
331
- });
332
-
333
- test('TweetNode → { type: "custom", name: "tweet" } block', () => {
334
- const doc = lexicalToKoguma(
335
- state([
336
- {
337
- type: 'tweet',
338
- version: 1,
339
- tweetId: '1234567890',
340
- children: []
341
- }
342
- ])
343
- );
344
- const custom = doc.nodes[0] as {
345
- type: string;
346
- name: string;
347
- data: Record<string, unknown>;
348
- };
349
- expect(custom.type).toBe('custom');
350
- expect(custom.name).toBe('tweet');
351
- expect(custom.data.tweetId).toBe('1234567890');
352
- });
353
-
354
- test('YouTubeNode → { type: "custom", name: "youtube" } block', () => {
355
- const doc = lexicalToKoguma(
356
- state([
357
- {
358
- type: 'youtube',
359
- version: 1,
360
- videoId: 'dQw4w9WgXcQ',
361
- children: []
362
- }
363
- ])
364
- );
365
- const custom = doc.nodes[0] as {
366
- type: string;
367
- name: string;
368
- data: Record<string, unknown>;
369
- };
370
- expect(custom.type).toBe('custom');
371
- expect(custom.name).toBe('youtube');
372
- expect(custom.data.videoId).toBe('dQw4w9WgXcQ');
373
- });
374
-
375
- test('LayoutContainerNode → { type: "layout", columns }', () => {
376
- const doc = lexicalToKoguma(
377
- state([
378
- {
379
- type: 'layoutcontainer',
380
- version: 1,
381
- children: [
382
- {
383
- type: 'layoutitem',
384
- version: 1,
385
- children: [para([text('Col A')])]
386
- },
387
- {
388
- type: 'layoutitem',
389
- version: 1,
390
- children: [para([text('Col B')])]
391
- }
392
- ]
393
- }
394
- ])
395
- );
396
- const layout = doc.nodes[0] as { type: string; columns: unknown[][] };
397
- expect(layout.type).toBe('layout');
398
- expect(layout.columns).toHaveLength(2);
399
- expect(layout.columns[0]).toHaveLength(1);
400
- expect(layout.columns[1]).toHaveLength(1);
401
- });
402
-
403
- test('MentionNode → { type: "custom", name: "mention" } inline', () => {
404
- const doc = lexicalToKoguma(
405
- state([
406
- para([
407
- {
408
- type: 'mention',
409
- version: 1,
410
- id: 'user_42',
411
- name: 'Matthew',
412
- text: '@Matthew'
413
- }
414
- ])
415
- ])
416
- );
417
- const p = doc.nodes[0] as {
418
- children: Array<{
419
- type: string;
420
- name: string;
421
- data: Record<string, unknown>;
422
- }>;
423
- };
424
- const mention = p.children[0];
425
- expect(mention.type).toBe('custom');
426
- expect(mention.name).toBe('mention');
427
- expect(mention.data.id).toBe('user_42');
428
- expect(mention.data.name).toBe('Matthew');
429
- });
430
-
431
- test('EmojiNode → flattened to { type: "text" } inline', () => {
432
- const doc = lexicalToKoguma(
433
- state([
434
- para([
435
- {
436
- type: 'emoji',
437
- version: 1,
438
- text: '🐻'
439
- }
440
- ])
441
- ])
442
- );
443
- const p = doc.nodes[0] as {
444
- children: Array<{ type: string; text: string }>;
445
- };
446
- expect(p.children[0].type).toBe('text');
447
- expect(p.children[0].text).toBe('🐻');
448
- });
449
-
450
- test('KeywordNode → stripped', () => {
451
- const doc = lexicalToKoguma(
452
- state([
453
- para([
454
- text('Before'),
455
- { type: 'keyword', version: 1, text: 'magic' },
456
- text('After')
457
- ])
458
- ])
459
- );
460
- const p = doc.nodes[0] as { children: unknown[] };
461
- expect(p.children).toHaveLength(2); // keyword stripped
462
- });
463
-
464
- test('AutocompleteNode (block) → stripped', () => {
465
- const doc = lexicalToKoguma(
466
- state([
467
- para([text('Real content')]),
468
- { type: 'autocomplete', version: 1, uuid: 'abc', children: [] }
469
- ])
470
- );
471
- expect(doc.nodes).toHaveLength(1);
472
- });
473
- });
474
-
475
- // ── Coverage integrity check ──────────────────────────────────────────
476
-
477
- describe('Lexical compat — registry coverage', () => {
478
- test('all registered node types are accounted for in this test file', () => {
479
- // This test ensures that as new nodes are added to NODE_REGISTRY,
480
- // someone notices this file needs updating.
481
- const accountedFor = new Set([
482
- 'root',
483
- 'paragraph',
484
- 'text',
485
- 'linebreak',
486
- 'heading',
487
- 'quote',
488
- 'list',
489
- 'listitem',
490
- 'link',
491
- 'autolink',
492
- 'table',
493
- 'tablerow',
494
- 'tablecell',
495
- 'code',
496
- 'code-highlight',
497
- 'overflow',
498
- 'hashtag',
499
- 'image',
500
- 'tweet',
501
- 'youtube',
502
- 'layoutcontainer',
503
- 'layoutitem',
504
- 'mention',
505
- 'emoji',
506
- 'keyword',
507
- 'autocomplete'
508
- ]);
509
- for (const nodeType of NODE_REGISTRY) {
510
- expect(accountedFor.has(nodeType)).toBe(true);
511
- }
512
- });
513
- });