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,535 +0,0 @@
1
- /**
2
- * RichText.test.tsx — component rendering tests
3
- *
4
- * Tests the <RichText> component using @testing-library/react.
5
- * Environment: happy-dom (configured in bunfig.toml)
6
- */
7
- import { describe, test, expect, afterEach } from 'bun:test';
8
- import { cleanup, render, screen } from '@testing-library/react';
9
- import { RichText } from './RichText.tsx';
10
- import type { KogumaDocument } from '../config/types.ts';
11
-
12
- afterEach(() => cleanup());
13
-
14
- // ── Helpers ───────────────────────────────────────────────────────────
15
-
16
- function doc(nodes: KogumaDocument['nodes']): KogumaDocument {
17
- return { nodes };
18
- }
19
-
20
- // ── null / empty ──────────────────────────────────────────────────────
21
-
22
- describe('<RichText> — null/empty', () => {
23
- test('renders nothing for null doc', () => {
24
- const { container } = render(<RichText doc={null} />);
25
- expect(container.firstChild).toBeNull();
26
- });
27
-
28
- test('renders nothing for empty nodes', () => {
29
- const { container } = render(<RichText doc={doc([])} />);
30
- expect(container.firstChild).toBeNull();
31
- });
32
- });
33
-
34
- // ── Wrapper ───────────────────────────────────────────────────────────
35
-
36
- describe('<RichText> — className', () => {
37
- test('applies className to wrapper div', () => {
38
- const { container } = render(
39
- <RichText
40
- doc={doc([
41
- { type: 'paragraph', children: [{ type: 'text', text: 'Hi' }] }
42
- ])}
43
- className='prose'
44
- />
45
- );
46
- expect(container.querySelector('.prose')).not.toBeNull();
47
- });
48
- });
49
-
50
- // ── Paragraph ─────────────────────────────────────────────────────────
51
-
52
- describe('<RichText> — paragraph', () => {
53
- test('renders a <p>', () => {
54
- render(
55
- <RichText
56
- doc={doc([
57
- {
58
- type: 'paragraph',
59
- children: [{ type: 'text', text: 'Hello world' }]
60
- }
61
- ])}
62
- />
63
- );
64
- expect(screen.getByText('Hello world').tagName).toBe('P');
65
- });
66
-
67
- test('applies text-align style from align prop', () => {
68
- const { container } = render(
69
- <RichText
70
- doc={doc([
71
- {
72
- type: 'paragraph',
73
- align: 'center',
74
- children: [{ type: 'text', text: 'Centered' }]
75
- }
76
- ])}
77
- />
78
- );
79
- const p = container.querySelector('p');
80
- expect(p?.style.textAlign).toBe('center');
81
- });
82
-
83
- test('paragraph override renders custom element', () => {
84
- render(
85
- <RichText
86
- doc={doc([
87
- { type: 'paragraph', children: [{ type: 'text', text: 'Override' }] }
88
- ])}
89
- components={{
90
- paragraph: ({ children }) => (
91
- <div data-testid='custom-p'>{children}</div>
92
- )
93
- }}
94
- />
95
- );
96
- expect(screen.getByTestId('custom-p')).not.toBeNull();
97
- });
98
- });
99
-
100
- // ── Headings ──────────────────────────────────────────────────────────
101
-
102
- describe('<RichText> — headings', () => {
103
- ([1, 2, 3, 4, 5, 6] as const).forEach(level => {
104
- test(`renders h${level}`, () => {
105
- const { container } = render(
106
- <RichText
107
- doc={doc([
108
- {
109
- type: 'heading',
110
- level,
111
- children: [{ type: 'text', text: `Heading ${level}` }]
112
- }
113
- ])}
114
- />
115
- );
116
- expect(container.querySelector(`h${level}`)).not.toBeNull();
117
- });
118
- });
119
- });
120
-
121
- // ── Text marks ────────────────────────────────────────────────────────
122
-
123
- describe('<RichText> — text marks', () => {
124
- test('bold renders <strong>', () => {
125
- const { container } = render(
126
- <RichText
127
- doc={doc([
128
- {
129
- type: 'paragraph',
130
- children: [{ type: 'text', text: 'Bold', bold: true }]
131
- }
132
- ])}
133
- />
134
- );
135
- expect(container.querySelector('strong')).not.toBeNull();
136
- });
137
-
138
- test('italic renders <em>', () => {
139
- const { container } = render(
140
- <RichText
141
- doc={doc([
142
- {
143
- type: 'paragraph',
144
- children: [{ type: 'text', text: 'Italic', italic: true }]
145
- }
146
- ])}
147
- />
148
- );
149
- expect(container.querySelector('em')).not.toBeNull();
150
- });
151
-
152
- test('code renders <code>', () => {
153
- const { container } = render(
154
- <RichText
155
- doc={doc([
156
- {
157
- type: 'paragraph',
158
- children: [{ type: 'text', text: 'code', code: true }]
159
- }
160
- ])}
161
- />
162
- );
163
- expect(container.querySelector('code')).not.toBeNull();
164
- });
165
-
166
- test('stacked marks — bold+italic nests both', () => {
167
- const { container } = render(
168
- <RichText
169
- doc={doc([
170
- {
171
- type: 'paragraph',
172
- children: [
173
- { type: 'text', text: 'BoldItalic', bold: true, italic: true }
174
- ]
175
- }
176
- ])}
177
- />
178
- );
179
- expect(container.querySelector('strong')).not.toBeNull();
180
- expect(container.querySelector('em')).not.toBeNull();
181
- });
182
- });
183
-
184
- // ── Lists ─────────────────────────────────────────────────────────────
185
-
186
- describe('<RichText> — lists', () => {
187
- test('unordered list renders <ul>', () => {
188
- const { container } = render(
189
- <RichText
190
- doc={doc([
191
- {
192
- type: 'list',
193
- ordered: false,
194
- items: [{ children: [{ type: 'text', text: 'Item' }] }]
195
- }
196
- ])}
197
- />
198
- );
199
- expect(container.querySelector('ul')).not.toBeNull();
200
- expect(container.querySelector('li')).not.toBeNull();
201
- });
202
-
203
- test('ordered list renders <ol>', () => {
204
- const { container } = render(
205
- <RichText
206
- doc={doc([
207
- {
208
- type: 'list',
209
- ordered: true,
210
- items: [{ children: [{ type: 'text', text: 'First' }] }]
211
- }
212
- ])}
213
- />
214
- );
215
- expect(container.querySelector('ol')).not.toBeNull();
216
- });
217
-
218
- test('checklist item renders checkbox', () => {
219
- const { container } = render(
220
- <RichText
221
- doc={doc([
222
- {
223
- type: 'list',
224
- ordered: false,
225
- items: [
226
- { checked: true, children: [{ type: 'text', text: 'Done' }] }
227
- ]
228
- }
229
- ])}
230
- />
231
- );
232
- const checkbox = container.querySelector(
233
- 'input[type="checkbox"]'
234
- ) as HTMLInputElement;
235
- expect(checkbox).not.toBeNull();
236
- expect(checkbox.checked).toBe(true);
237
- });
238
-
239
- test('nested list renders nested <ul> inside <li>', () => {
240
- const { container } = render(
241
- <RichText
242
- doc={doc([
243
- {
244
- type: 'list',
245
- ordered: false,
246
- items: [
247
- {
248
- children: [{ type: 'text', text: 'Parent' }],
249
- nestedList: {
250
- ordered: false,
251
- items: [{ children: [{ type: 'text', text: 'Child' }] }]
252
- }
253
- }
254
- ]
255
- }
256
- ])}
257
- />
258
- );
259
- expect(container.querySelectorAll('li').length).toBe(2);
260
- expect(container.querySelector('ul ul')).not.toBeNull();
261
- });
262
- });
263
-
264
- // ── Blockquote ────────────────────────────────────────────────────────
265
-
266
- describe('<RichText> — quote', () => {
267
- test('renders <blockquote>', () => {
268
- const { container } = render(
269
- <RichText
270
- doc={doc([
271
- {
272
- type: 'quote',
273
- children: [{ type: 'text', text: 'A wise quote' }]
274
- }
275
- ])}
276
- />
277
- );
278
- expect(container.querySelector('blockquote')).not.toBeNull();
279
- });
280
- });
281
-
282
- // ── Code block ────────────────────────────────────────────────────────
283
-
284
- describe('<RichText> — code block', () => {
285
- test('renders <pre><code> with language class', () => {
286
- const { container } = render(
287
- <RichText
288
- doc={doc([
289
- {
290
- type: 'code',
291
- language: 'typescript',
292
- text: 'const x = 1'
293
- }
294
- ])}
295
- />
296
- );
297
- expect(container.querySelector('pre')).not.toBeNull();
298
- const code = container.querySelector('code');
299
- expect(code?.className).toContain('language-typescript');
300
- expect(code?.textContent).toBe('const x = 1');
301
- });
302
-
303
- test('code block override receives node', () => {
304
- let receivedLang: string | undefined;
305
- render(
306
- <RichText
307
- doc={doc([{ type: 'code', language: 'ts', text: 'x' }])}
308
- components={{
309
- code: ({ node }) => {
310
- receivedLang = node.language;
311
- return null;
312
- }
313
- }}
314
- />
315
- );
316
- expect(receivedLang).toBe('ts');
317
- });
318
- });
319
-
320
- // ── Link ──────────────────────────────────────────────────────────────
321
-
322
- describe('<RichText> — link', () => {
323
- test('renders <a> with href and text', () => {
324
- const { container } = render(
325
- <RichText
326
- doc={doc([
327
- {
328
- type: 'paragraph',
329
- children: [
330
- {
331
- type: 'link',
332
- url: 'https://example.com',
333
- children: [{ type: 'text', text: 'Example' }]
334
- }
335
- ]
336
- }
337
- ])}
338
- />
339
- );
340
- const link = container.querySelector('a');
341
- expect(link?.getAttribute('href')).toBe('https://example.com');
342
- expect(link?.textContent).toBe('Example');
343
- });
344
-
345
- test('newTab link adds target=_blank and rel=noopener', () => {
346
- const { container } = render(
347
- <RichText
348
- doc={doc([
349
- {
350
- type: 'paragraph',
351
- children: [
352
- {
353
- type: 'link',
354
- url: 'https://x.com',
355
- newTab: true,
356
- children: [{ type: 'text', text: 'X' }]
357
- }
358
- ]
359
- }
360
- ])}
361
- />
362
- );
363
- const a = container.querySelector('a');
364
- expect(a?.getAttribute('target')).toBe('_blank');
365
- expect(a?.getAttribute('rel')).toContain('noopener');
366
- });
367
- });
368
-
369
- // ── HR ────────────────────────────────────────────────────────────────
370
-
371
- describe('<RichText> — hr', () => {
372
- test('renders <hr>', () => {
373
- const { container } = render(<RichText doc={doc([{ type: 'hr' }])} />);
374
- expect(container.querySelector('hr')).not.toBeNull();
375
- });
376
- });
377
-
378
- // ── Line break ────────────────────────────────────────────────────────
379
-
380
- describe('<RichText> — line-break', () => {
381
- test('renders <br>', () => {
382
- const { container } = render(
383
- <RichText
384
- doc={doc([
385
- {
386
- type: 'paragraph',
387
- children: [
388
- { type: 'text', text: 'line1' },
389
- { type: 'line-break' },
390
- { type: 'text', text: 'line2' }
391
- ]
392
- }
393
- ])}
394
- />
395
- );
396
- expect(container.querySelector('br')).not.toBeNull();
397
- });
398
- });
399
-
400
- // ── Table ─────────────────────────────────────────────────────────────
401
-
402
- describe('<RichText> — table', () => {
403
- test('renders table with thead and tbody', () => {
404
- const { container } = render(
405
- <RichText
406
- doc={doc([
407
- {
408
- type: 'table',
409
- rows: [
410
- {
411
- isHeader: true,
412
- cells: [{ children: [{ type: 'text', text: 'Name' }] }]
413
- },
414
- { cells: [{ children: [{ type: 'text', text: 'Alice' }] }] }
415
- ]
416
- }
417
- ])}
418
- />
419
- );
420
- expect(container.querySelector('table')).not.toBeNull();
421
- expect(container.querySelector('thead')).not.toBeNull();
422
- expect(container.querySelector('tbody')).not.toBeNull();
423
- expect(container.querySelector('th')?.textContent).toBe('Name');
424
- expect(container.querySelector('td')?.textContent).toBe('Alice');
425
- });
426
- });
427
-
428
- // ── Layout ────────────────────────────────────────────────────────────
429
-
430
- describe('<RichText> — layout', () => {
431
- test('renders columns in a flex container', () => {
432
- render(
433
- <RichText
434
- doc={doc([
435
- {
436
- type: 'layout',
437
- columns: [
438
- [
439
- {
440
- type: 'paragraph',
441
- children: [{ type: 'text', text: 'Col 1' }]
442
- }
443
- ],
444
- [
445
- {
446
- type: 'paragraph',
447
- children: [{ type: 'text', text: 'Col 2' }]
448
- }
449
- ]
450
- ]
451
- }
452
- ])}
453
- />
454
- );
455
- expect(screen.getByText('Col 1')).not.toBeNull();
456
- expect(screen.getByText('Col 2')).not.toBeNull();
457
- });
458
- });
459
-
460
- // ── Custom blocks ─────────────────────────────────────────────────────
461
-
462
- describe('<RichText> — custom nodes', () => {
463
- test('custom block renders null by default (no crash)', () => {
464
- const { container } = render(
465
- <RichText
466
- doc={doc([
467
- {
468
- type: 'custom',
469
- name: 'tweet',
470
- data: { id: '123' }
471
- }
472
- ])}
473
- />
474
- );
475
- expect(container.querySelector('div')?.children.length).toBe(0);
476
- });
477
-
478
- test('customBlock override renders element', () => {
479
- render(
480
- <RichText
481
- doc={doc([{ type: 'custom', name: 'tweet', data: { id: '999' } }])}
482
- components={{
483
- customBlock: ({ data }) => (
484
- <div data-testid='tweet'>{data.id as string}</div>
485
- )
486
- }}
487
- />
488
- );
489
- expect(screen.getByTestId('tweet').textContent).toBe('999');
490
- });
491
-
492
- test('customInline override renders for hashtag', () => {
493
- render(
494
- <RichText
495
- doc={doc([
496
- {
497
- type: 'paragraph',
498
- children: [
499
- { type: 'custom', name: 'hashtag', data: { tag: 'react' } }
500
- ]
501
- }
502
- ])}
503
- components={{
504
- customInline: ({ data }) => (
505
- <span data-testid='hashtag'>#{data.tag as string}</span>
506
- )
507
- }}
508
- />
509
- );
510
- expect(screen.getByTestId('hashtag').textContent).toBe('#react');
511
- });
512
- });
513
-
514
- // ── Image ─────────────────────────────────────────────────────────────
515
-
516
- describe('<RichText> — image', () => {
517
- test('renders <img> with src and alt', () => {
518
- const { container } = render(
519
- <RichText
520
- doc={doc([
521
- {
522
- type: 'image',
523
- url: 'https://example.com/cat.jpg',
524
- alt: 'A cat',
525
- width: 400,
526
- height: 300
527
- }
528
- ])}
529
- />
530
- );
531
- const img = container.querySelector('img');
532
- expect(img?.getAttribute('src')).toBe('https://example.com/cat.jpg');
533
- expect(img?.getAttribute('alt')).toBe('A cat');
534
- });
535
- });