astro-xmdx 0.0.2

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 (48) hide show
  1. package/index.ts +8 -0
  2. package/package.json +80 -0
  3. package/src/constants.ts +52 -0
  4. package/src/index.ts +150 -0
  5. package/src/pipeline/index.ts +38 -0
  6. package/src/pipeline/orchestrator.test.ts +324 -0
  7. package/src/pipeline/orchestrator.ts +121 -0
  8. package/src/pipeline/pipe.test.ts +251 -0
  9. package/src/pipeline/pipe.ts +70 -0
  10. package/src/pipeline/types.ts +59 -0
  11. package/src/plugins.test.ts +274 -0
  12. package/src/presets/index.ts +225 -0
  13. package/src/transforms/blocks-to-jsx.test.ts +590 -0
  14. package/src/transforms/blocks-to-jsx.ts +617 -0
  15. package/src/transforms/expressive-code.test.ts +274 -0
  16. package/src/transforms/expressive-code.ts +147 -0
  17. package/src/transforms/index.test.ts +143 -0
  18. package/src/transforms/index.ts +100 -0
  19. package/src/transforms/inject-components.test.ts +406 -0
  20. package/src/transforms/inject-components.ts +184 -0
  21. package/src/transforms/shiki.test.ts +289 -0
  22. package/src/transforms/shiki.ts +312 -0
  23. package/src/types.ts +92 -0
  24. package/src/utils/config.test.ts +252 -0
  25. package/src/utils/config.ts +146 -0
  26. package/src/utils/frontmatter.ts +33 -0
  27. package/src/utils/imports.test.ts +518 -0
  28. package/src/utils/imports.ts +201 -0
  29. package/src/utils/mdx-detection.test.ts +41 -0
  30. package/src/utils/mdx-detection.ts +209 -0
  31. package/src/utils/paths.test.ts +206 -0
  32. package/src/utils/paths.ts +92 -0
  33. package/src/utils/validation.test.ts +60 -0
  34. package/src/utils/validation.ts +15 -0
  35. package/src/vite-plugin/binding-loader.ts +81 -0
  36. package/src/vite-plugin/directive-rewriter.test.ts +331 -0
  37. package/src/vite-plugin/directive-rewriter.ts +272 -0
  38. package/src/vite-plugin/esbuild-pool.ts +173 -0
  39. package/src/vite-plugin/index.ts +37 -0
  40. package/src/vite-plugin/jsx-module.ts +106 -0
  41. package/src/vite-plugin/mdx-wrapper.ts +328 -0
  42. package/src/vite-plugin/normalize-config.test.ts +78 -0
  43. package/src/vite-plugin/normalize-config.ts +29 -0
  44. package/src/vite-plugin/shiki-highlighter.ts +46 -0
  45. package/src/vite-plugin/shiki-manager.test.ts +175 -0
  46. package/src/vite-plugin/shiki-manager.ts +53 -0
  47. package/src/vite-plugin/types.ts +189 -0
  48. package/src/vite-plugin.ts +1342 -0
@@ -0,0 +1,518 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import { collectImportedNames, insertAfterImports, extractImportStatements } from './imports.js';
3
+
4
+ describe('collectImportedNames', () => {
5
+ it('should collect default imports', () => {
6
+ const code = `import React from 'react';`;
7
+ const names = collectImportedNames(code);
8
+
9
+ expect(names.has('React')).toBe(true);
10
+ expect(names.size).toBe(1);
11
+ });
12
+
13
+ it('should collect named imports', () => {
14
+ const code = `import { useState, useEffect } from 'react';`;
15
+ const names = collectImportedNames(code);
16
+
17
+ expect(names.has('useState')).toBe(true);
18
+ expect(names.has('useEffect')).toBe(true);
19
+ expect(names.size).toBe(2);
20
+ });
21
+
22
+ it('should collect namespace imports', () => {
23
+ const code = `import * as React from 'react';`;
24
+ const names = collectImportedNames(code);
25
+
26
+ expect(names.has('React')).toBe(true);
27
+ expect(names.size).toBe(1);
28
+ });
29
+
30
+ it('should handle named imports with aliases', () => {
31
+ const code = `import { Component as Comp, Fragment as Frag } from 'react';`;
32
+ const names = collectImportedNames(code);
33
+
34
+ expect(names.has('Comp')).toBe(true);
35
+ expect(names.has('Frag')).toBe(true);
36
+ expect(names.has('Component')).toBe(false);
37
+ expect(names.has('Fragment')).toBe(false);
38
+ expect(names.size).toBe(2);
39
+ });
40
+
41
+ it('should handle mixed default and named imports', () => {
42
+ const code = `import React, { useState } from 'react';`;
43
+ const names = collectImportedNames(code);
44
+
45
+ expect(names.has('React')).toBe(true);
46
+ // Note: Current implementation doesn't fully parse mixed imports
47
+ // It only captures the default import when comma is present
48
+ expect(names.size).toBe(1);
49
+ });
50
+
51
+ it('should handle multiple import statements', () => {
52
+ const code = `
53
+ import React from 'react';
54
+ import { Aside, Tabs } from '@astrojs/starlight/components';
55
+ import * as utils from './utils';
56
+ `.trim();
57
+ const names = collectImportedNames(code);
58
+
59
+ expect(names.has('React')).toBe(true);
60
+ expect(names.has('Aside')).toBe(true);
61
+ expect(names.has('Tabs')).toBe(true);
62
+ expect(names.has('utils')).toBe(true);
63
+ expect(names.size).toBe(4);
64
+ });
65
+
66
+ it('should ignore dynamic imports', () => {
67
+ const code = `
68
+ import React from 'react';
69
+ const lazy = import('./lazy.js');
70
+ `.trim();
71
+ const names = collectImportedNames(code);
72
+
73
+ expect(names.has('React')).toBe(true);
74
+ expect(names.size).toBe(1);
75
+ });
76
+
77
+ it('should handle imports with trailing commas', () => {
78
+ const code = `import { Aside, Tabs, } from '@astrojs/starlight/components';`;
79
+ const names = collectImportedNames(code);
80
+
81
+ expect(names.has('Aside')).toBe(true);
82
+ expect(names.has('Tabs')).toBe(true);
83
+ expect(names.size).toBe(2);
84
+ });
85
+
86
+ it('should handle multiline imports', () => {
87
+ const code = `
88
+ import {
89
+ Aside,
90
+ Tabs,
91
+ TabItem
92
+ } from '@astrojs/starlight/components';
93
+ `.trim();
94
+ // Note: This function only processes single lines starting with 'import'
95
+ // So multiline imports won't be fully parsed
96
+ const names = collectImportedNames(code);
97
+
98
+ // Only the first line is processed
99
+ expect(names.size).toBeGreaterThanOrEqual(0);
100
+ });
101
+
102
+ it('should return empty set for code without imports', () => {
103
+ const code = `
104
+ export default function Component() {
105
+ return <div>Hello</div>;
106
+ }
107
+ `.trim();
108
+ const names = collectImportedNames(code);
109
+
110
+ expect(names.size).toBe(0);
111
+ });
112
+
113
+ it('should handle imports with special characters in identifiers', () => {
114
+ const code = `import { Component$1, _Helper } from './module';`;
115
+ const names = collectImportedNames(code);
116
+
117
+ expect(names.has('Component$1')).toBe(true);
118
+ expect(names.has('_Helper')).toBe(true);
119
+ expect(names.size).toBe(2);
120
+ });
121
+ });
122
+
123
+ describe('insertAfterImports', () => {
124
+ it('should insert import after existing imports', () => {
125
+ const code = `import React from 'react';
126
+
127
+ export default function App() {}`;
128
+
129
+ const result = insertAfterImports(code, "import { Aside } from '@astrojs/starlight/components';");
130
+
131
+ expect(result).toContain("import React from 'react';");
132
+ expect(result).toContain("import { Aside } from '@astrojs/starlight/components';");
133
+
134
+ const lines = result.split('\n');
135
+ const reactIndex = lines.findIndex(l => l.includes('React'));
136
+ const asideIndex = lines.findIndex(l => l.includes('Aside'));
137
+
138
+ expect(asideIndex).toBeGreaterThan(reactIndex);
139
+ });
140
+
141
+ it('should insert at beginning of empty code', () => {
142
+ const code = '';
143
+ const result = insertAfterImports(code, "import { Aside } from '@astrojs/starlight/components';");
144
+
145
+ // Note: Empty array split/join behavior adds newline
146
+ expect(result).toContain("import { Aside } from '@astrojs/starlight/components';");
147
+ });
148
+
149
+ it('should insert after multiple imports', () => {
150
+ const code = `import React from 'react';
151
+ import { useState } from 'react';
152
+ import * as utils from './utils';
153
+
154
+ export default function App() {}`;
155
+
156
+ const result = insertAfterImports(code, "import { Aside } from '@astrojs/starlight/components';");
157
+
158
+ const lines = result.split('\n');
159
+ const asideIndex = lines.findIndex(l => l.includes('Aside'));
160
+ const exportIndex = lines.findIndex(l => l.includes('export default'));
161
+
162
+ expect(asideIndex).toBeLessThan(exportIndex);
163
+ expect(asideIndex).toBe(4); // After 3 imports and blank line
164
+ });
165
+
166
+ it('should skip comments before imports', () => {
167
+ const code = `// File header comment
168
+ import React from 'react';
169
+
170
+ export default function App() {}`;
171
+
172
+ const result = insertAfterImports(code, "import { Aside } from '@astrojs/starlight/components';");
173
+
174
+ const lines = result.split('\n');
175
+ expect(lines[0]).toContain('// File header comment');
176
+ expect(lines[1]).toContain('React');
177
+ // New import is inserted after the last import (line 1), so it's at line 2
178
+ // But there might be a blank line, so check it exists somewhere
179
+ expect(result).toContain('Aside');
180
+ });
181
+
182
+ it('should skip block comments', () => {
183
+ const code = `/* Block comment */
184
+ import React from 'react';
185
+
186
+ export default function App() {}`;
187
+
188
+ const result = insertAfterImports(code, "import { Aside } from '@astrojs/starlight/components';");
189
+
190
+ expect(result).toContain('/* Block comment */');
191
+ expect(result).toContain('import React');
192
+ expect(result).toContain('import { Aside }');
193
+ });
194
+
195
+ it('should handle code with no imports', () => {
196
+ const code = `export default function App() {
197
+ return <div>Hello</div>;
198
+ }`;
199
+
200
+ const result = insertAfterImports(code, "import { Aside } from '@astrojs/starlight/components';");
201
+
202
+ const lines = result.split('\n');
203
+ expect(lines[0]).toContain('import { Aside }');
204
+ expect(lines[1]).toContain('export default');
205
+ });
206
+
207
+ it('should skip blank lines before imports', () => {
208
+ const code = `
209
+
210
+ import React from 'react';
211
+
212
+ export default function App() {}`;
213
+
214
+ const result = insertAfterImports(code, "import { Aside } from '@astrojs/starlight/components';");
215
+
216
+ const lines = result.split('\n');
217
+ const asideIndex = lines.findIndex(l => l.includes('Aside'));
218
+ const reactIndex = lines.findIndex(l => l.includes('React'));
219
+
220
+ expect(asideIndex).toBeGreaterThan(reactIndex);
221
+ });
222
+
223
+ it('should preserve original line endings', () => {
224
+ const code = `import React from 'react';\n\nexport default function App() {}`;
225
+ const result = insertAfterImports(code, "import { Aside } from '@astrojs/starlight/components';");
226
+
227
+ expect(result).toContain('\n');
228
+ expect(result.split('\n').length).toBeGreaterThan(1);
229
+ });
230
+
231
+ it('should insert import with proper spacing', () => {
232
+ const code = `import React from 'react';
233
+ import { useState } from 'react';
234
+
235
+ const foo = 'bar';`;
236
+
237
+ const result = insertAfterImports(code, "import { Aside } from '@astrojs/starlight/components';");
238
+
239
+ const lines = result.split('\n');
240
+ // The new import is inserted after line 1 (index 1), at index 2
241
+ // But since there's already a blank line at index 2, it shifts
242
+ const asideIndex = lines.findIndex(l => l.includes('Aside'));
243
+ expect(asideIndex).toBeGreaterThan(1);
244
+ expect(result).toContain("import { Aside } from '@astrojs/starlight/components';");
245
+ expect(result).toContain("const foo = 'bar';");
246
+ });
247
+ });
248
+
249
+ describe('integration', () => {
250
+ it('should work together for import injection', () => {
251
+ const code = `import React from 'react';
252
+
253
+ export default function Content() {
254
+ return <Aside>Content</Aside>;
255
+ }`;
256
+
257
+ // Check if Aside is already imported
258
+ const imported = collectImportedNames(code);
259
+
260
+ if (!imported.has('Aside')) {
261
+ const newCode = insertAfterImports(code, "import { Aside } from '@astrojs/starlight/components';");
262
+ const newImported = collectImportedNames(newCode);
263
+
264
+ expect(newImported.has('Aside')).toBe(true);
265
+ expect(newImported.has('React')).toBe(true);
266
+ }
267
+ });
268
+
269
+ it('should not duplicate imports', () => {
270
+ const code = `import React from 'react';
271
+ import { Aside } from '@astrojs/starlight/components';
272
+
273
+ export default function Content() {
274
+ return <Aside>Content</Aside>;
275
+ }`;
276
+
277
+ const imported = collectImportedNames(code);
278
+
279
+ // Aside is already imported, so don't add it again
280
+ expect(imported.has('Aside')).toBe(true);
281
+
282
+ // Should not modify code
283
+ const shouldNotModify = imported.has('Aside');
284
+ expect(shouldNotModify).toBe(true);
285
+ });
286
+ });
287
+
288
+ describe('extractImportStatements', () => {
289
+ it('should extract default imports', () => {
290
+ const code = `import Card from '~/components/Landing/Card.astro';
291
+
292
+ # Hello World`;
293
+
294
+ const imports = extractImportStatements(code);
295
+
296
+ expect(imports).toHaveLength(1);
297
+ expect(imports[0]).toBe("import Card from '~/components/Landing/Card.astro';");
298
+ });
299
+
300
+ it('should extract named imports', () => {
301
+ const code = `import { useState, useEffect } from 'react';
302
+
303
+ export default function App() {}`;
304
+
305
+ const imports = extractImportStatements(code);
306
+
307
+ expect(imports).toHaveLength(1);
308
+ expect(imports[0]).toBe("import { useState, useEffect } from 'react';");
309
+ });
310
+
311
+ it('should extract namespace imports', () => {
312
+ const code = `import * as React from 'react';`;
313
+
314
+ const imports = extractImportStatements(code);
315
+
316
+ expect(imports).toHaveLength(1);
317
+ expect(imports[0]).toBe("import * as React from 'react';");
318
+ });
319
+
320
+ it('should extract multiple import statements', () => {
321
+ const code = `import Card from '~/components/Card.astro';
322
+ import { Aside, Tabs } from '@astrojs/starlight/components';
323
+ import * as utils from './utils';
324
+
325
+ # Hello World
326
+
327
+ <Card>Content</Card>`;
328
+
329
+ const imports = extractImportStatements(code);
330
+
331
+ expect(imports).toHaveLength(3);
332
+ expect(imports[0]).toBe("import Card from '~/components/Card.astro';");
333
+ expect(imports[1]).toBe("import { Aside, Tabs } from '@astrojs/starlight/components';");
334
+ expect(imports[2]).toBe("import * as utils from './utils';");
335
+ });
336
+
337
+ it('should ignore dynamic imports', () => {
338
+ const code = `import Card from './Card';
339
+ const lazy = import('./lazy.js');`;
340
+
341
+ const imports = extractImportStatements(code);
342
+
343
+ expect(imports).toHaveLength(1);
344
+ expect(imports[0]).toBe("import Card from './Card';");
345
+ });
346
+
347
+ it('should return empty array for code without imports', () => {
348
+ const code = `# Hello World
349
+
350
+ This is some markdown content.`;
351
+
352
+ const imports = extractImportStatements(code);
353
+
354
+ expect(imports).toHaveLength(0);
355
+ });
356
+
357
+ it('should return empty array for empty or invalid input', () => {
358
+ expect(extractImportStatements('')).toHaveLength(0);
359
+ expect(extractImportStatements(null as unknown as string)).toHaveLength(0);
360
+ expect(extractImportStatements(undefined as unknown as string)).toHaveLength(0);
361
+ });
362
+
363
+ it('should ignore imports inside code fences', () => {
364
+ const code = `import Card from './Card.astro';
365
+
366
+ \`\`\`js
367
+ import React from 'react';
368
+ \`\`\`
369
+
370
+ Some content`;
371
+
372
+ const imports = extractImportStatements(code);
373
+
374
+ expect(imports).toHaveLength(1);
375
+ expect(imports[0]).toBe("import Card from './Card.astro';");
376
+ });
377
+
378
+ it('should handle MDX with frontmatter', () => {
379
+ const code = `---
380
+ title: Hello
381
+ ---
382
+ import Card from '~/components/Card.astro';
383
+
384
+ # Hello World`;
385
+
386
+ const imports = extractImportStatements(code);
387
+
388
+ expect(imports).toHaveLength(1);
389
+ expect(imports[0]).toBe("import Card from '~/components/Card.astro';");
390
+ });
391
+
392
+ it('should preserve exact import statement format', () => {
393
+ const code = `import { Component as Comp } from 'lib';`;
394
+
395
+ const imports = extractImportStatements(code);
396
+
397
+ expect(imports).toHaveLength(1);
398
+ expect(imports[0]).toBe("import { Component as Comp } from 'lib';");
399
+ });
400
+
401
+ it('should extract multi-line imports', () => {
402
+ const code = `import {
403
+ Foo,
404
+ Bar
405
+ } from 'something';`;
406
+
407
+ const result = extractImportStatements(code);
408
+
409
+ expect(result).toHaveLength(1);
410
+ expect(result[0]).toContain('Foo');
411
+ expect(result[0]).toContain('Bar');
412
+ expect(result[0]).toContain('something');
413
+ });
414
+
415
+ it('should extract multiple multi-line imports', () => {
416
+ const code = `import {
417
+ Aside,
418
+ Tabs
419
+ } from '@astrojs/starlight/components';
420
+ import {
421
+ Card,
422
+ CardGrid
423
+ } from './components';
424
+
425
+ # Content`;
426
+
427
+ const result = extractImportStatements(code);
428
+
429
+ expect(result).toHaveLength(2);
430
+ expect(result[0]).toContain('Aside');
431
+ expect(result[0]).toContain('Tabs');
432
+ expect(result[1]).toContain('Card');
433
+ expect(result[1]).toContain('CardGrid');
434
+ });
435
+
436
+ it('should handle mix of single-line and multi-line imports', () => {
437
+ const code = `import React from 'react';
438
+ import {
439
+ Foo,
440
+ Bar
441
+ } from 'module';
442
+ import { Simple } from 'simple';`;
443
+
444
+ const result = extractImportStatements(code);
445
+
446
+ expect(result).toHaveLength(3);
447
+ expect(result[0]).toBe("import React from 'react';");
448
+ expect(result[1]).toContain('Foo');
449
+ expect(result[1]).toContain('Bar');
450
+ expect(result[2]).toBe("import { Simple } from 'simple';");
451
+ });
452
+
453
+ it('should extract side-effect imports without semicolons', () => {
454
+ const code = `import './styles.css'
455
+ import Card from './Card.astro'
456
+
457
+ # Content`;
458
+
459
+ const result = extractImportStatements(code);
460
+
461
+ expect(result).toHaveLength(2);
462
+ expect(result[0]).toBe("import './styles.css'");
463
+ expect(result[1]).toBe("import Card from './Card.astro'");
464
+ });
465
+
466
+ it('should extract side-effect imports with semicolons', () => {
467
+ const code = `import "./polyfill.js";
468
+ import './styles.css';`;
469
+
470
+ const result = extractImportStatements(code);
471
+
472
+ expect(result).toHaveLength(2);
473
+ expect(result[0]).toBe('import "./polyfill.js";');
474
+ expect(result[1]).toBe("import './styles.css';");
475
+ });
476
+
477
+ it('should handle multi-line imports with inline comments', () => {
478
+ const code = `import {
479
+ Foo, // note about Foo
480
+ Bar // another comment
481
+ } from 'x';`;
482
+
483
+ const result = extractImportStatements(code);
484
+
485
+ expect(result).toHaveLength(1);
486
+ // Comments should be stripped, producing valid syntax
487
+ expect(result[0]).toContain('Foo');
488
+ expect(result[0]).toContain('Bar');
489
+ expect(result[0]).toContain("from 'x'");
490
+ // Should NOT contain the comment (which would break syntax)
491
+ expect(result[0]).not.toContain('//');
492
+ });
493
+
494
+ it('should not misparsing side-effect import as multi-line', () => {
495
+ // Regression test: side-effect import followed by regular import
496
+ // should NOT concatenate them into a single import
497
+ const code = `import './styles.css'
498
+ import { foo } from 'bar'
499
+
500
+ # Content`;
501
+
502
+ const result = extractImportStatements(code);
503
+
504
+ expect(result).toHaveLength(2);
505
+ // Each import should be separate
506
+ expect(result[0]).toBe("import './styles.css'");
507
+ expect(result[1]).toBe("import { foo } from 'bar'");
508
+ // Should NOT contain both in one string
509
+ expect(result[0]).not.toContain('foo');
510
+ });
511
+
512
+ it('should preserve URL specifiers with // in multi-line imports', () => {
513
+ const code = `import {\n Foo\n} from 'https://example.com/mod.js';`;
514
+ const result = extractImportStatements(code);
515
+ expect(result).toHaveLength(1);
516
+ expect(result[0]).toContain('https://example.com/mod.js');
517
+ });
518
+ });