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.
- package/index.ts +8 -0
- package/package.json +80 -0
- package/src/constants.ts +52 -0
- package/src/index.ts +150 -0
- package/src/pipeline/index.ts +38 -0
- package/src/pipeline/orchestrator.test.ts +324 -0
- package/src/pipeline/orchestrator.ts +121 -0
- package/src/pipeline/pipe.test.ts +251 -0
- package/src/pipeline/pipe.ts +70 -0
- package/src/pipeline/types.ts +59 -0
- package/src/plugins.test.ts +274 -0
- package/src/presets/index.ts +225 -0
- package/src/transforms/blocks-to-jsx.test.ts +590 -0
- package/src/transforms/blocks-to-jsx.ts +617 -0
- package/src/transforms/expressive-code.test.ts +274 -0
- package/src/transforms/expressive-code.ts +147 -0
- package/src/transforms/index.test.ts +143 -0
- package/src/transforms/index.ts +100 -0
- package/src/transforms/inject-components.test.ts +406 -0
- package/src/transforms/inject-components.ts +184 -0
- package/src/transforms/shiki.test.ts +289 -0
- package/src/transforms/shiki.ts +312 -0
- package/src/types.ts +92 -0
- package/src/utils/config.test.ts +252 -0
- package/src/utils/config.ts +146 -0
- package/src/utils/frontmatter.ts +33 -0
- package/src/utils/imports.test.ts +518 -0
- package/src/utils/imports.ts +201 -0
- package/src/utils/mdx-detection.test.ts +41 -0
- package/src/utils/mdx-detection.ts +209 -0
- package/src/utils/paths.test.ts +206 -0
- package/src/utils/paths.ts +92 -0
- package/src/utils/validation.test.ts +60 -0
- package/src/utils/validation.ts +15 -0
- package/src/vite-plugin/binding-loader.ts +81 -0
- package/src/vite-plugin/directive-rewriter.test.ts +331 -0
- package/src/vite-plugin/directive-rewriter.ts +272 -0
- package/src/vite-plugin/esbuild-pool.ts +173 -0
- package/src/vite-plugin/index.ts +37 -0
- package/src/vite-plugin/jsx-module.ts +106 -0
- package/src/vite-plugin/mdx-wrapper.ts +328 -0
- package/src/vite-plugin/normalize-config.test.ts +78 -0
- package/src/vite-plugin/normalize-config.ts +29 -0
- package/src/vite-plugin/shiki-highlighter.ts +46 -0
- package/src/vite-plugin/shiki-manager.test.ts +175 -0
- package/src/vite-plugin/shiki-manager.ts +53 -0
- package/src/vite-plugin/types.ts +189 -0
- package/src/vite-plugin.ts +1342 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
injectComponentImports,
|
|
4
|
+
injectStarlightComponents,
|
|
5
|
+
injectAstroComponents,
|
|
6
|
+
injectComponentImportsFromRegistry,
|
|
7
|
+
} from './inject-components.js';
|
|
8
|
+
import { createRegistry, starlightLibrary, astroLibrary, type Registry } from 'xmdx/registry';
|
|
9
|
+
|
|
10
|
+
// Create test registry
|
|
11
|
+
const testRegistry = createRegistry([starlightLibrary, astroLibrary]);
|
|
12
|
+
|
|
13
|
+
const ASTRO_COMPONENTS_MODULE = astroLibrary.defaultModulePath;
|
|
14
|
+
|
|
15
|
+
describe('injectComponentImports', () => {
|
|
16
|
+
it('should inject missing component imports', () => {
|
|
17
|
+
const code = `
|
|
18
|
+
export default function Content() {
|
|
19
|
+
return <Aside>Content</Aside>;
|
|
20
|
+
}`;
|
|
21
|
+
|
|
22
|
+
const result = injectComponentImports(code, ['Aside'], '@astrojs/starlight/components');
|
|
23
|
+
|
|
24
|
+
expect(result).toContain("import { Aside } from '@astrojs/starlight/components';");
|
|
25
|
+
expect(result).toContain('export default');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should not inject imports for unused components', () => {
|
|
29
|
+
const code = `
|
|
30
|
+
export default function Content() {
|
|
31
|
+
return <div>No components</div>;
|
|
32
|
+
}`;
|
|
33
|
+
|
|
34
|
+
const result = injectComponentImports(code, ['Aside', 'Tabs'], '@astrojs/starlight/components');
|
|
35
|
+
|
|
36
|
+
expect(result).toBe(code);
|
|
37
|
+
expect(result).not.toContain('import');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should not inject already imported components', () => {
|
|
41
|
+
const code = `
|
|
42
|
+
import { Aside } from '@astrojs/starlight/components';
|
|
43
|
+
|
|
44
|
+
export default function Content() {
|
|
45
|
+
return <Aside>Content</Aside>;
|
|
46
|
+
}`;
|
|
47
|
+
|
|
48
|
+
const result = injectComponentImports(code, ['Aside'], '@astrojs/starlight/components');
|
|
49
|
+
|
|
50
|
+
const importCount = (result.match(/import/g) || []).length;
|
|
51
|
+
expect(importCount).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should inject multiple missing components', () => {
|
|
55
|
+
const code = `
|
|
56
|
+
export default function Content() {
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<Aside>Note</Aside>
|
|
60
|
+
<Tabs><TabItem>Tab</TabItem></Tabs>
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
}`;
|
|
64
|
+
|
|
65
|
+
const result = injectComponentImports(
|
|
66
|
+
code,
|
|
67
|
+
['Aside', 'Tabs', 'TabItem'],
|
|
68
|
+
'@astrojs/starlight/components'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(result).toContain('import { Aside, Tabs, TabItem }');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should only inject components that are used and missing', () => {
|
|
75
|
+
const code = `
|
|
76
|
+
import { Aside } from '@astrojs/starlight/components';
|
|
77
|
+
|
|
78
|
+
export default function Content() {
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<Aside>Note</Aside>
|
|
82
|
+
<Tabs>Content</Tabs>
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
}`;
|
|
86
|
+
|
|
87
|
+
const result = injectComponentImports(
|
|
88
|
+
code,
|
|
89
|
+
['Aside', 'Tabs', 'Card'],
|
|
90
|
+
'@astrojs/starlight/components'
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Aside already imported, Tabs is used and missing, Card is not used
|
|
94
|
+
expect(result).toContain('import { Tabs }');
|
|
95
|
+
expect(result).not.toContain('Card');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should detect components with attributes', () => {
|
|
99
|
+
const code = `
|
|
100
|
+
export default function Content() {
|
|
101
|
+
return <Aside type="note">Content</Aside>;
|
|
102
|
+
}`;
|
|
103
|
+
|
|
104
|
+
const result = injectComponentImports(code, ['Aside'], '@astrojs/starlight/components');
|
|
105
|
+
|
|
106
|
+
expect(result).toContain('import { Aside }');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should detect self-closing components', () => {
|
|
110
|
+
const code = `
|
|
111
|
+
export default function Content() {
|
|
112
|
+
return <Card />;
|
|
113
|
+
}`;
|
|
114
|
+
|
|
115
|
+
const result = injectComponentImports(code, ['Card'], '@astrojs/starlight/components');
|
|
116
|
+
|
|
117
|
+
expect(result).toContain('import { Card }');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should strip heading metadata before scanning', () => {
|
|
121
|
+
const code = `
|
|
122
|
+
export const headings = [
|
|
123
|
+
{ depth: 1, slug: 'aside', text: 'Aside' }
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
export default function Content() {
|
|
127
|
+
return <Aside>Content</Aside>;
|
|
128
|
+
}`;
|
|
129
|
+
|
|
130
|
+
const result = injectComponentImports(code, ['Aside'], '@astrojs/starlight/components');
|
|
131
|
+
|
|
132
|
+
// Should detect Aside usage in actual content, not in headings
|
|
133
|
+
expect(result).toContain('import { Aside }');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should handle components in nested structures', () => {
|
|
137
|
+
const code = `
|
|
138
|
+
export default function Content() {
|
|
139
|
+
return (
|
|
140
|
+
<div>
|
|
141
|
+
<Aside>
|
|
142
|
+
<Tabs>
|
|
143
|
+
<TabItem label="One">Content</TabItem>
|
|
144
|
+
</Tabs>
|
|
145
|
+
</Aside>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}`;
|
|
149
|
+
|
|
150
|
+
const result = injectComponentImports(
|
|
151
|
+
code,
|
|
152
|
+
['Aside', 'Tabs', 'TabItem'],
|
|
153
|
+
'@astrojs/starlight/components'
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(result).toContain('import { Aside, Tabs, TabItem }');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should not inject if components array is empty', () => {
|
|
160
|
+
const code = `
|
|
161
|
+
export default function Content() {
|
|
162
|
+
return <Aside>Content</Aside>;
|
|
163
|
+
}`;
|
|
164
|
+
|
|
165
|
+
const result = injectComponentImports(code, [], '@astrojs/starlight/components');
|
|
166
|
+
|
|
167
|
+
expect(result).toBe(code);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('injectStarlightComponents', () => {
|
|
172
|
+
it('should inject Starlight components with true config', () => {
|
|
173
|
+
const code = `
|
|
174
|
+
export default function Content() {
|
|
175
|
+
return <Aside>Note</Aside>;
|
|
176
|
+
}`;
|
|
177
|
+
|
|
178
|
+
const result = injectStarlightComponents(code, true);
|
|
179
|
+
|
|
180
|
+
expect(result).toContain('import { Aside }');
|
|
181
|
+
expect(result).toContain('@astrojs/starlight/components');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should return code unchanged with false config', () => {
|
|
185
|
+
const code = `
|
|
186
|
+
export default function Content() {
|
|
187
|
+
return <Aside>Note</Aside>;
|
|
188
|
+
}`;
|
|
189
|
+
|
|
190
|
+
const result = injectStarlightComponents(code, false);
|
|
191
|
+
|
|
192
|
+
expect(result).toBe(code);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should handle custom components config', () => {
|
|
196
|
+
const code = `
|
|
197
|
+
export default function Content() {
|
|
198
|
+
return <CustomAside>Note</CustomAside>;
|
|
199
|
+
}`;
|
|
200
|
+
|
|
201
|
+
const result = injectStarlightComponents(code, {
|
|
202
|
+
components: ['CustomAside'],
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(result).toContain('import { CustomAside }');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should handle custom module config', () => {
|
|
209
|
+
const code = `
|
|
210
|
+
export default function Content() {
|
|
211
|
+
return <Aside>Note</Aside>;
|
|
212
|
+
}`;
|
|
213
|
+
|
|
214
|
+
const result = injectStarlightComponents(code, {
|
|
215
|
+
module: 'my-custom-module',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(result).toContain('import { Aside }');
|
|
219
|
+
expect(result).toContain('my-custom-module');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should inject multiple Starlight components', () => {
|
|
223
|
+
const code = `
|
|
224
|
+
export default function Content() {
|
|
225
|
+
return (
|
|
226
|
+
<>
|
|
227
|
+
<Aside>Note</Aside>
|
|
228
|
+
<Tabs><TabItem>Tab</TabItem></Tabs>
|
|
229
|
+
<Steps>
|
|
230
|
+
<li>Step 1</li>
|
|
231
|
+
</Steps>
|
|
232
|
+
</>
|
|
233
|
+
);
|
|
234
|
+
}`;
|
|
235
|
+
|
|
236
|
+
const result = injectStarlightComponents(code, true);
|
|
237
|
+
|
|
238
|
+
expect(result).toContain('import { Aside, Tabs, TabItem, Steps }');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('injectAstroComponents', () => {
|
|
243
|
+
it('should inject Code component', () => {
|
|
244
|
+
const code = `
|
|
245
|
+
export default function Content() {
|
|
246
|
+
return <Code lang="js">const x = 1;</Code>;
|
|
247
|
+
}`;
|
|
248
|
+
|
|
249
|
+
const result = injectAstroComponents(code);
|
|
250
|
+
|
|
251
|
+
expect(result).toContain('import { Code }');
|
|
252
|
+
expect(result).toContain(ASTRO_COMPONENTS_MODULE);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should not inject Prism (not a built-in Astro component)', () => {
|
|
256
|
+
const code = `
|
|
257
|
+
export default function Content() {
|
|
258
|
+
return <Prism lang="js">const x = 1;</Prism>;
|
|
259
|
+
}`;
|
|
260
|
+
|
|
261
|
+
const result = injectAstroComponents(code);
|
|
262
|
+
|
|
263
|
+
expect(result).not.toContain('import { Prism }');
|
|
264
|
+
expect(result).toBe(code);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should only inject Code when both Code and Prism are used', () => {
|
|
268
|
+
const code = `
|
|
269
|
+
export default function Content() {
|
|
270
|
+
return (
|
|
271
|
+
<>
|
|
272
|
+
<Code lang="js">const x = 1;</Code>
|
|
273
|
+
<Prism lang="python">print("hello")</Prism>
|
|
274
|
+
</>
|
|
275
|
+
);
|
|
276
|
+
}`;
|
|
277
|
+
|
|
278
|
+
const result = injectAstroComponents(code);
|
|
279
|
+
|
|
280
|
+
expect(result).toContain('import { Code }');
|
|
281
|
+
expect(result).not.toContain('import { Code, Prism }');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should not inject if no Astro components used', () => {
|
|
285
|
+
const code = `
|
|
286
|
+
export default function Content() {
|
|
287
|
+
return <div>No Astro components</div>;
|
|
288
|
+
}`;
|
|
289
|
+
|
|
290
|
+
const result = injectAstroComponents(code);
|
|
291
|
+
|
|
292
|
+
expect(result).toBe(code);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should not inject if already imported', () => {
|
|
296
|
+
const code = `
|
|
297
|
+
import { Code } from 'astro/components';
|
|
298
|
+
|
|
299
|
+
export default function Content() {
|
|
300
|
+
return <Code lang="js">const x = 1;</Code>;
|
|
301
|
+
}`;
|
|
302
|
+
|
|
303
|
+
const result = injectAstroComponents(code);
|
|
304
|
+
|
|
305
|
+
const importCount = (result.match(/import/g) || []).length;
|
|
306
|
+
expect(importCount).toBe(1);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('injectComponentImportsFromRegistry', () => {
|
|
311
|
+
it('should inject missing Starlight component imports', () => {
|
|
312
|
+
const code = `
|
|
313
|
+
export default function Content() {
|
|
314
|
+
return <Aside>Content</Aside>;
|
|
315
|
+
}`;
|
|
316
|
+
|
|
317
|
+
const result = injectComponentImportsFromRegistry(code, testRegistry);
|
|
318
|
+
|
|
319
|
+
expect(result).toContain("import { Aside } from '@astrojs/starlight/components';");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should inject missing Astro component imports', () => {
|
|
323
|
+
const code = `
|
|
324
|
+
export default function Content() {
|
|
325
|
+
return <Code lang="js">const x = 1;</Code>;
|
|
326
|
+
}`;
|
|
327
|
+
|
|
328
|
+
const result = injectComponentImportsFromRegistry(code, testRegistry);
|
|
329
|
+
|
|
330
|
+
expect(result).toContain("import { Code } from 'astro/components';");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should inject components from multiple modules', () => {
|
|
334
|
+
const code = `
|
|
335
|
+
export default function Content() {
|
|
336
|
+
return (
|
|
337
|
+
<>
|
|
338
|
+
<Aside>Note</Aside>
|
|
339
|
+
<Code lang="js">code</Code>
|
|
340
|
+
</>
|
|
341
|
+
);
|
|
342
|
+
}`;
|
|
343
|
+
|
|
344
|
+
const result = injectComponentImportsFromRegistry(code, testRegistry);
|
|
345
|
+
|
|
346
|
+
expect(result).toContain("import { Aside } from '@astrojs/starlight/components';");
|
|
347
|
+
expect(result).toContain("import { Code } from 'astro/components';");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should not inject already imported components', () => {
|
|
351
|
+
const code = `
|
|
352
|
+
import { Aside } from '@astrojs/starlight/components';
|
|
353
|
+
|
|
354
|
+
export default function Content() {
|
|
355
|
+
return <Aside>Content</Aside>;
|
|
356
|
+
}`;
|
|
357
|
+
|
|
358
|
+
const result = injectComponentImportsFromRegistry(code, testRegistry);
|
|
359
|
+
|
|
360
|
+
const importCount = (result.match(/import.*Aside/g) || []).length;
|
|
361
|
+
expect(importCount).toBe(1);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should return code unchanged if no registry provided', () => {
|
|
365
|
+
const code = `
|
|
366
|
+
export default function Content() {
|
|
367
|
+
return <Aside>Content</Aside>;
|
|
368
|
+
}`;
|
|
369
|
+
|
|
370
|
+
const result = injectComponentImportsFromRegistry(code, null as unknown as Registry);
|
|
371
|
+
|
|
372
|
+
expect(result).toBe(code);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should group multiple components from same module', () => {
|
|
376
|
+
const code = `
|
|
377
|
+
export default function Content() {
|
|
378
|
+
return (
|
|
379
|
+
<>
|
|
380
|
+
<Aside>Note</Aside>
|
|
381
|
+
<Tabs><TabItem>Tab</TabItem></Tabs>
|
|
382
|
+
</>
|
|
383
|
+
);
|
|
384
|
+
}`;
|
|
385
|
+
|
|
386
|
+
const result = injectComponentImportsFromRegistry(code, testRegistry);
|
|
387
|
+
|
|
388
|
+
// Should have grouped import
|
|
389
|
+
expect(result).toContain("import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should strip heading metadata before scanning', () => {
|
|
393
|
+
const code = `
|
|
394
|
+
export const headings = [
|
|
395
|
+
{ depth: 1, slug: 'aside', text: 'Aside' }
|
|
396
|
+
];
|
|
397
|
+
|
|
398
|
+
export default function Content() {
|
|
399
|
+
return <Aside>Content</Aside>;
|
|
400
|
+
}`;
|
|
401
|
+
|
|
402
|
+
const result = injectComponentImportsFromRegistry(code, testRegistry);
|
|
403
|
+
|
|
404
|
+
expect(result).toContain('import { Aside }');
|
|
405
|
+
});
|
|
406
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component import injection transformations
|
|
3
|
+
* @module transforms/inject-components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Registry } from 'xmdx/registry';
|
|
7
|
+
import { astroLibrary } from 'xmdx/registry';
|
|
8
|
+
import { collectImportedNames, insertAfterImports } from '../utils/imports.js';
|
|
9
|
+
import { resolveStarlightConfig, type StarlightUserConfig } from '../utils/config.js';
|
|
10
|
+
import { stripHeadingsMeta } from '../utils/validation.js';
|
|
11
|
+
|
|
12
|
+
/** Strip set:html={...} string content to avoid false component matches in code blocks */
|
|
13
|
+
function stripSetHtmlContent(code: string): string {
|
|
14
|
+
return code.replace(/set:html=\{("(?:[^"\\]|\\.)*")\}/g, 'set:html={""}');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generic component import injection.
|
|
19
|
+
* Scans code for component usage and injects missing imports.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const code = `
|
|
23
|
+
* export default function Content() {
|
|
24
|
+
* return <Aside>Hello</Aside>;
|
|
25
|
+
* }
|
|
26
|
+
* `;
|
|
27
|
+
* const result = injectComponentImports(code, ['Aside'], '@astrojs/starlight/components');
|
|
28
|
+
* // Adds: import { Aside } from '@astrojs/starlight/components';
|
|
29
|
+
*/
|
|
30
|
+
export function injectComponentImports(
|
|
31
|
+
code: string,
|
|
32
|
+
components: string[],
|
|
33
|
+
moduleId: string
|
|
34
|
+
): string {
|
|
35
|
+
if (!code || typeof code !== 'string' || components.length === 0) {
|
|
36
|
+
return code;
|
|
37
|
+
}
|
|
38
|
+
const scanTarget = stripSetHtmlContent(stripHeadingsMeta(code));
|
|
39
|
+
|
|
40
|
+
// PERF: Use single combined regex instead of per-component regex
|
|
41
|
+
// This reduces from O(n) regex compilations to O(1)
|
|
42
|
+
const combinedPattern = new RegExp(`<(${components.join('|')})\\b`, 'g');
|
|
43
|
+
const matches = scanTarget.match(combinedPattern);
|
|
44
|
+
if (!matches) return code;
|
|
45
|
+
|
|
46
|
+
// Extract unique component names from matches
|
|
47
|
+
const usedSet = new Set<string>();
|
|
48
|
+
for (const match of matches) {
|
|
49
|
+
const name = match.slice(1); // Remove leading '<'
|
|
50
|
+
usedSet.add(name);
|
|
51
|
+
}
|
|
52
|
+
const used = components.filter((name) => usedSet.has(name));
|
|
53
|
+
if (used.length === 0) return code;
|
|
54
|
+
|
|
55
|
+
const imported = collectImportedNames(code);
|
|
56
|
+
const missing = used.filter((name) => !imported.has(name));
|
|
57
|
+
if (missing.length === 0) return code;
|
|
58
|
+
|
|
59
|
+
const importLine = `import { ${missing.join(', ')} } from '${moduleId}';`;
|
|
60
|
+
return insertAfterImports(code, importLine);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Inject Starlight component imports based on usage.
|
|
65
|
+
* Normalizes config and delegates to injectComponentImports.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* const code = `<Aside>Note</Aside>`;
|
|
69
|
+
* const result = injectStarlightComponents(code, true);
|
|
70
|
+
* // Adds: import { Aside } from '@astrojs/starlight/components';
|
|
71
|
+
*/
|
|
72
|
+
export function injectStarlightComponents(
|
|
73
|
+
code: string,
|
|
74
|
+
config: boolean | StarlightUserConfig,
|
|
75
|
+
registry?: Registry
|
|
76
|
+
): string {
|
|
77
|
+
const resolved = resolveStarlightConfig(config, registry);
|
|
78
|
+
if (!resolved) return code;
|
|
79
|
+
|
|
80
|
+
return injectComponentImports(code, resolved.components, resolved.moduleId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Inject Astro component imports based on usage.
|
|
85
|
+
* Checks for Code/Prism component usage and adds imports.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* const code = `<Code lang="js">const x = 1;</Code>`;
|
|
89
|
+
* const result = injectAstroComponents(code);
|
|
90
|
+
* // Adds: import { Code } from 'astro/components';
|
|
91
|
+
*/
|
|
92
|
+
export function injectAstroComponents(code: string, registry?: Registry): string {
|
|
93
|
+
// Get components from registry if available, otherwise use library preset
|
|
94
|
+
let components = astroLibrary.components.map((c) => c.name);
|
|
95
|
+
let moduleId = astroLibrary.defaultModulePath;
|
|
96
|
+
|
|
97
|
+
if (registry) {
|
|
98
|
+
const astroComponents = registry.getComponentsByModule(astroLibrary.defaultModulePath);
|
|
99
|
+
if (astroComponents.length > 0 && astroComponents[0]) {
|
|
100
|
+
components = astroComponents.map((c) => c.name);
|
|
101
|
+
moduleId = astroComponents[0].modulePath;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return injectComponentImports(code, components, moduleId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Inject component imports from registry based on usage.
|
|
110
|
+
* Scans code for component usage and injects missing imports
|
|
111
|
+
* using information from the registry.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* const code = `<Aside>Note</Aside><Code lang="js">x</Code>`;
|
|
115
|
+
* const result = injectComponentImportsFromRegistry(code, registry);
|
|
116
|
+
* // Adds imports for both Aside and Code from their respective modules
|
|
117
|
+
*/
|
|
118
|
+
export function injectComponentImportsFromRegistry(
|
|
119
|
+
code: string,
|
|
120
|
+
registry: Registry
|
|
121
|
+
): string {
|
|
122
|
+
if (!code || typeof code !== 'string' || !registry) {
|
|
123
|
+
return code;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const allComponents = registry.getAllComponents();
|
|
127
|
+
if (allComponents.length === 0) {
|
|
128
|
+
return code;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const scanTarget = stripSetHtmlContent(stripHeadingsMeta(code));
|
|
132
|
+
const imported = collectImportedNames(code);
|
|
133
|
+
|
|
134
|
+
// PERF: Use single combined regex instead of per-component regex
|
|
135
|
+
// This reduces from O(n) regex compilations to O(1)
|
|
136
|
+
const componentNames = allComponents.map((c) => c.name);
|
|
137
|
+
const combinedPattern = new RegExp(`<(${componentNames.join('|')})\\b`, 'g');
|
|
138
|
+
const matches = scanTarget.match(combinedPattern);
|
|
139
|
+
if (!matches) return code;
|
|
140
|
+
|
|
141
|
+
// Extract unique component names from matches
|
|
142
|
+
const usedNames = new Set<string>();
|
|
143
|
+
for (const match of matches) {
|
|
144
|
+
const name = match.slice(1); // Remove leading '<'
|
|
145
|
+
usedNames.add(name);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Find used components that are missing imports
|
|
149
|
+
const missingByModule = new Map<string, Array<{ name: string; exportType: string }>>();
|
|
150
|
+
|
|
151
|
+
for (const comp of allComponents) {
|
|
152
|
+
if (usedNames.has(comp.name) && !imported.has(comp.name)) {
|
|
153
|
+
const modulePath = comp.modulePath;
|
|
154
|
+
if (!missingByModule.has(modulePath)) {
|
|
155
|
+
missingByModule.set(modulePath, []);
|
|
156
|
+
}
|
|
157
|
+
missingByModule.get(modulePath)!.push({ name: comp.name, exportType: comp.exportType });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (missingByModule.size === 0) {
|
|
162
|
+
return code;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Generate import statements grouped by module
|
|
166
|
+
let result = code;
|
|
167
|
+
for (const [modulePath, components] of missingByModule) {
|
|
168
|
+
// Check if all components use named exports
|
|
169
|
+
const allNamed = components.every((c) => c.exportType === 'named');
|
|
170
|
+
if (allNamed) {
|
|
171
|
+
const names = components.map((c) => c.name).join(', ');
|
|
172
|
+
const importLine = `import { ${names} } from '${modulePath}';`;
|
|
173
|
+
result = insertAfterImports(result, importLine);
|
|
174
|
+
} else {
|
|
175
|
+
// Individual default imports for each component
|
|
176
|
+
for (const comp of components) {
|
|
177
|
+
const importLine = `import ${comp.name} from '${modulePath}/${comp.name}.astro';`;
|
|
178
|
+
result = insertAfterImports(result, importLine);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
}
|