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,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tests for plugin system functionality
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'bun:test';
|
|
6
|
+
import { pipe, when } from './pipeline/pipe.js';
|
|
7
|
+
import type { TransformContext, TransformConfig } from './types.js';
|
|
8
|
+
|
|
9
|
+
interface TestContext {
|
|
10
|
+
code: string;
|
|
11
|
+
config?: { enabled: boolean };
|
|
12
|
+
source?: string;
|
|
13
|
+
filename?: string;
|
|
14
|
+
frontmatter?: Record<string, unknown>;
|
|
15
|
+
headings?: Array<{ text: string; depth: number }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PluginLike {
|
|
19
|
+
name: string;
|
|
20
|
+
enforce?: 'pre' | 'post';
|
|
21
|
+
afterParse?: (ctx: TestContext) => TestContext;
|
|
22
|
+
beforeOutput?: (ctx: TestContext) => TestContext;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Since collectHooks is not exported from index.js, we test the plugin behavior
|
|
27
|
+
* through the pipeline composition patterns that the plugin system uses.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
describe('plugin hook patterns', () => {
|
|
31
|
+
describe('hook collection and ordering', () => {
|
|
32
|
+
test('hooks execute in order', async () => {
|
|
33
|
+
const order: string[] = [];
|
|
34
|
+
|
|
35
|
+
const hooks = [
|
|
36
|
+
(ctx: TestContext) => {
|
|
37
|
+
order.push('first');
|
|
38
|
+
return ctx;
|
|
39
|
+
},
|
|
40
|
+
(ctx: TestContext) => {
|
|
41
|
+
order.push('second');
|
|
42
|
+
return ctx;
|
|
43
|
+
},
|
|
44
|
+
(ctx: TestContext) => {
|
|
45
|
+
order.push('third');
|
|
46
|
+
return ctx;
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const pipeline = pipe(...hooks);
|
|
51
|
+
await pipeline({ code: 'test' });
|
|
52
|
+
|
|
53
|
+
expect(order).toEqual(['first', 'second', 'third']);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('async hooks execute in sequence', async () => {
|
|
57
|
+
const order: string[] = [];
|
|
58
|
+
|
|
59
|
+
const hooks = [
|
|
60
|
+
async (ctx: TestContext) => {
|
|
61
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
62
|
+
order.push('first');
|
|
63
|
+
return ctx;
|
|
64
|
+
},
|
|
65
|
+
async (ctx: TestContext) => {
|
|
66
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
67
|
+
order.push('second');
|
|
68
|
+
return ctx;
|
|
69
|
+
},
|
|
70
|
+
(ctx: TestContext) => {
|
|
71
|
+
order.push('third');
|
|
72
|
+
return ctx;
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const pipeline = pipe(...hooks);
|
|
77
|
+
await pipeline({ code: 'test' });
|
|
78
|
+
|
|
79
|
+
expect(order).toEqual(['first', 'second', 'third']);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('hooks can modify context', async () => {
|
|
83
|
+
const hooks = [
|
|
84
|
+
(ctx: TestContext) => ({ ...ctx, code: ctx.code + ' modified1' }),
|
|
85
|
+
(ctx: TestContext) => ({ ...ctx, code: ctx.code + ' modified2' }),
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const pipeline = pipe(...hooks);
|
|
89
|
+
const result = await pipeline({ code: 'original' });
|
|
90
|
+
|
|
91
|
+
expect(result.code).toBe('original modified1 modified2');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('preprocess hook pattern', () => {
|
|
96
|
+
test('preprocess transforms source before parsing', () => {
|
|
97
|
+
const preprocessors = [
|
|
98
|
+
(source: string, _filename: string) => source.replace(/WARNING/g, 'CAUTION'),
|
|
99
|
+
(source: string, _filename: string) => source.replace(/NOTE/g, 'TIP'),
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
let source = 'This is a WARNING and a NOTE';
|
|
103
|
+
for (const preprocess of preprocessors) {
|
|
104
|
+
source = preprocess(source, '/test.md');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
expect(source).toBe('This is a CAUTION and a TIP');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('preprocess receives filename', () => {
|
|
111
|
+
const receivedFilenames: string[] = [];
|
|
112
|
+
const preprocessors = [
|
|
113
|
+
(source: string, filename: string) => {
|
|
114
|
+
receivedFilenames.push(filename);
|
|
115
|
+
return source;
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
let source = 'test';
|
|
120
|
+
for (const preprocess of preprocessors) {
|
|
121
|
+
source = preprocess(source, '/path/to/file.md');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
expect(receivedFilenames).toEqual(['/path/to/file.md']);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('conditional transforms with hooks', () => {
|
|
129
|
+
test('hooks can be combined with when()', async () => {
|
|
130
|
+
const userHook = (ctx: TestContext) => ({ ...ctx, code: ctx.code + ' [user]' });
|
|
131
|
+
const conditionalTransform = when<TestContext>(
|
|
132
|
+
(ctx) => ctx.config?.enabled ?? false,
|
|
133
|
+
(ctx) => ({ ...ctx, code: ctx.code + ' [built-in]' })
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const pipeline = pipe(userHook, conditionalTransform);
|
|
137
|
+
|
|
138
|
+
const result1 = await pipeline({
|
|
139
|
+
code: 'start',
|
|
140
|
+
config: { enabled: true },
|
|
141
|
+
});
|
|
142
|
+
expect(result1.code).toBe('start [user] [built-in]');
|
|
143
|
+
|
|
144
|
+
const result2 = await pipeline({
|
|
145
|
+
code: 'start',
|
|
146
|
+
config: { enabled: false },
|
|
147
|
+
});
|
|
148
|
+
expect(result2.code).toBe('start [user]');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('plugin enforce ordering simulation', () => {
|
|
153
|
+
test('pre plugins run before normal plugins', async () => {
|
|
154
|
+
const plugins: PluginLike[] = [
|
|
155
|
+
{ name: 'normal1', enforce: undefined, afterParse: (ctx) => ({ ...ctx, code: ctx.code + ' normal1' }) },
|
|
156
|
+
{ name: 'pre1', enforce: 'pre', afterParse: (ctx) => ({ ...ctx, code: ctx.code + ' pre1' }) },
|
|
157
|
+
{ name: 'post1', enforce: 'post', afterParse: (ctx) => ({ ...ctx, code: ctx.code + ' post1' }) },
|
|
158
|
+
{ name: 'pre2', enforce: 'pre', afterParse: (ctx) => ({ ...ctx, code: ctx.code + ' pre2' }) },
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
// Simulate collectHooks sorting
|
|
162
|
+
const sorted = [...plugins].sort((a, b) => {
|
|
163
|
+
const order: Record<string, number> = { pre: 0, undefined: 1, post: 2 };
|
|
164
|
+
const aOrder = order[String(a.enforce)] ?? 1;
|
|
165
|
+
const bOrder = order[String(b.enforce)] ?? 1;
|
|
166
|
+
return aOrder - bOrder;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const hooks = sorted.filter((p) => p.afterParse).map((p) => p.afterParse!);
|
|
170
|
+
const pipeline = pipe(...hooks);
|
|
171
|
+
const result = await pipeline({ code: 'start' });
|
|
172
|
+
|
|
173
|
+
// pre plugins first, then normal, then post
|
|
174
|
+
expect(result.code).toBe('start pre1 pre2 normal1 post1');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('realistic plugin examples', () => {
|
|
179
|
+
test('custom directive replacement plugin', async () => {
|
|
180
|
+
const customDirectivePlugin = {
|
|
181
|
+
name: 'custom-directive',
|
|
182
|
+
afterParse(ctx: TestContext) {
|
|
183
|
+
const newCode = ctx.code
|
|
184
|
+
.replace(/<Warning>/g, '<Aside type="caution">')
|
|
185
|
+
.replace(/<\/Warning>/g, '</Aside>');
|
|
186
|
+
return { ...ctx, code: newCode };
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const hooks = [customDirectivePlugin.afterParse];
|
|
191
|
+
const pipeline = pipe(...hooks);
|
|
192
|
+
const result = await pipeline({
|
|
193
|
+
code: '<Warning>Be careful!</Warning>',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(result.code).toBe('<Aside type="caution">Be careful!</Aside>');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('analytics plugin that doesn\'t modify code', async () => {
|
|
200
|
+
const processedFiles: string[] = [];
|
|
201
|
+
const analyticsPlugin = {
|
|
202
|
+
name: 'analytics',
|
|
203
|
+
beforeOutput(ctx: TestContext) {
|
|
204
|
+
if (ctx.filename) {
|
|
205
|
+
processedFiles.push(ctx.filename);
|
|
206
|
+
}
|
|
207
|
+
return ctx; // Return unchanged
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const hooks = [analyticsPlugin.beforeOutput];
|
|
212
|
+
const pipeline = pipe(...hooks);
|
|
213
|
+
const result = await pipeline({
|
|
214
|
+
code: 'original code',
|
|
215
|
+
filename: '/path/to/file.md',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(result.code).toBe('original code');
|
|
219
|
+
expect(processedFiles).toEqual(['/path/to/file.md']);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('code wrapper plugin', async () => {
|
|
223
|
+
const wrapperPlugin = {
|
|
224
|
+
name: 'wrapper',
|
|
225
|
+
beforeOutput(ctx: TestContext) {
|
|
226
|
+
const wrapped = `/* Generated by Xmdx */\n${ctx.code}`;
|
|
227
|
+
return { ...ctx, code: wrapped };
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const hooks = [wrapperPlugin.beforeOutput];
|
|
232
|
+
const pipeline = pipe(...hooks);
|
|
233
|
+
const result = await pipeline({ code: 'export default function() {}' });
|
|
234
|
+
|
|
235
|
+
expect(result.code).toContain('/* Generated by Xmdx */');
|
|
236
|
+
expect(result.code).toContain('export default function() {}');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('TransformContext structure', () => {
|
|
242
|
+
test('context contains all required properties', async () => {
|
|
243
|
+
let receivedCtx: TransformContext | null = null;
|
|
244
|
+
|
|
245
|
+
const inspectorHook = (ctx: TransformContext) => {
|
|
246
|
+
receivedCtx = ctx;
|
|
247
|
+
return ctx;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const pipeline = pipe(inspectorHook);
|
|
251
|
+
await pipeline({
|
|
252
|
+
code: '<div>Hello</div>',
|
|
253
|
+
source: '# Hello',
|
|
254
|
+
filename: '/test.md',
|
|
255
|
+
frontmatter: { title: 'Test' },
|
|
256
|
+
headings: [{ text: 'Hello', depth: 1, slug: 'hello' }],
|
|
257
|
+
config: {
|
|
258
|
+
expressiveCode: null,
|
|
259
|
+
starlightComponents: true,
|
|
260
|
+
shiki: null,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(receivedCtx).toHaveProperty('code');
|
|
265
|
+
expect(receivedCtx).toHaveProperty('source');
|
|
266
|
+
expect(receivedCtx).toHaveProperty('filename');
|
|
267
|
+
expect(receivedCtx).toHaveProperty('frontmatter');
|
|
268
|
+
expect(receivedCtx).toHaveProperty('headings');
|
|
269
|
+
expect(receivedCtx).toHaveProperty('config');
|
|
270
|
+
expect(receivedCtx!.config).toHaveProperty('expressiveCode');
|
|
271
|
+
expect(receivedCtx!.config).toHaveProperty('starlightComponents');
|
|
272
|
+
expect(receivedCtx!.config).toHaveProperty('shiki');
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Astro Xmdx presets for common UI libraries.
|
|
3
|
+
* @module presets
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ComponentLibrary } from 'xmdx/registry';
|
|
7
|
+
import { starlightLibrary, expressiveCodeLibrary, astroLibrary } from 'xmdx/registry';
|
|
8
|
+
import type { MdxImportHandlingOptions } from '../types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Preset configuration.
|
|
12
|
+
*/
|
|
13
|
+
export interface PresetConfig {
|
|
14
|
+
/** Component libraries to register */
|
|
15
|
+
libraries: ComponentLibrary[];
|
|
16
|
+
/** ExpressiveCode configuration */
|
|
17
|
+
expressiveCode?: boolean | { enabled: boolean; componentName?: string; importSource?: string };
|
|
18
|
+
/** Starlight components configuration */
|
|
19
|
+
starlightComponents?: boolean | { enabled: boolean; importSource?: string };
|
|
20
|
+
/** MDX import handling configuration */
|
|
21
|
+
mdx?: MdxImportHandlingOptions;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default import patterns allowed for Starlight projects.
|
|
26
|
+
* These patterns won't trigger fallback to @mdx-js/mdx.
|
|
27
|
+
*/
|
|
28
|
+
export const STARLIGHT_DEFAULT_ALLOW_IMPORTS = [
|
|
29
|
+
// Starlight components and subpackages
|
|
30
|
+
'@astrojs/starlight/components',
|
|
31
|
+
'@astrojs/starlight/*',
|
|
32
|
+
// Astro virtual modules
|
|
33
|
+
'astro:*',
|
|
34
|
+
// Local project components (common Astro convention)
|
|
35
|
+
'~/components/*',
|
|
36
|
+
// Common image imports
|
|
37
|
+
'*.svg',
|
|
38
|
+
'*.png',
|
|
39
|
+
'*.jpg',
|
|
40
|
+
'*.jpeg',
|
|
41
|
+
'*.gif',
|
|
42
|
+
'*.webp',
|
|
43
|
+
'*.avif',
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Options for the Starlight preset.
|
|
48
|
+
*/
|
|
49
|
+
export interface StarlightPresetOptions {
|
|
50
|
+
/** Enable ExpressiveCode integration (default: true) */
|
|
51
|
+
expressiveCode?: boolean;
|
|
52
|
+
/** Strip Starlight imports - registry handles injection. @default true */
|
|
53
|
+
stripStarlightImports?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Additional import patterns to allow beyond the defaults.
|
|
56
|
+
* Patterns matching these won't trigger fallback to @mdx-js/mdx.
|
|
57
|
+
* Supports glob patterns (e.g., '~/components/*').
|
|
58
|
+
* @example ['~/components/*', '../components/*', 'my-package']
|
|
59
|
+
*/
|
|
60
|
+
allowImports?: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates a Starlight preset with Starlight components and optionally ExpressiveCode.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* import xmdx from 'astro-xmdx';
|
|
68
|
+
* import { starlightPreset } from 'astro-xmdx/presets';
|
|
69
|
+
*
|
|
70
|
+
* export default defineConfig({
|
|
71
|
+
* integrations: [
|
|
72
|
+
* xmdx({
|
|
73
|
+
* presets: [starlightPreset()],
|
|
74
|
+
* })
|
|
75
|
+
* ]
|
|
76
|
+
* });
|
|
77
|
+
*/
|
|
78
|
+
export function starlightPreset(options: StarlightPresetOptions = {}): PresetConfig {
|
|
79
|
+
const { expressiveCode = true, stripStarlightImports = true, allowImports = [] } = options;
|
|
80
|
+
|
|
81
|
+
const libraries = [astroLibrary, starlightLibrary];
|
|
82
|
+
if (expressiveCode) {
|
|
83
|
+
libraries.push(expressiveCodeLibrary);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Merge default patterns with user-provided patterns
|
|
87
|
+
const mergedAllowImports = [
|
|
88
|
+
...STARLIGHT_DEFAULT_ALLOW_IMPORTS,
|
|
89
|
+
...allowImports,
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
libraries,
|
|
94
|
+
starlightComponents: true,
|
|
95
|
+
expressiveCode: expressiveCode ? { enabled: true } : false,
|
|
96
|
+
mdx: stripStarlightImports
|
|
97
|
+
? { allowImports: mergedAllowImports, ignoreCodeFences: true }
|
|
98
|
+
: undefined,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Options for the ExpressiveCode preset.
|
|
104
|
+
*/
|
|
105
|
+
export interface ExpressiveCodePresetOptions {
|
|
106
|
+
/** Component name to use (default: 'Code') */
|
|
107
|
+
componentName?: string;
|
|
108
|
+
/** Import source (default: 'astro-expressive-code/components') */
|
|
109
|
+
importSource?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates an ExpressiveCode preset for syntax highlighting.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* import xmdx from 'astro-xmdx';
|
|
117
|
+
* import { expressiveCodePreset } from 'astro-xmdx/presets';
|
|
118
|
+
*
|
|
119
|
+
* export default defineConfig({
|
|
120
|
+
* integrations: [
|
|
121
|
+
* xmdx({
|
|
122
|
+
* presets: [expressiveCodePreset()],
|
|
123
|
+
* })
|
|
124
|
+
* ]
|
|
125
|
+
* });
|
|
126
|
+
*/
|
|
127
|
+
export function expressiveCodePreset(options: ExpressiveCodePresetOptions = {}): PresetConfig {
|
|
128
|
+
const {
|
|
129
|
+
componentName = 'Code',
|
|
130
|
+
importSource = 'astro-expressive-code/components',
|
|
131
|
+
} = options;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
libraries: [astroLibrary, expressiveCodeLibrary],
|
|
135
|
+
expressiveCode: {
|
|
136
|
+
enabled: true,
|
|
137
|
+
componentName,
|
|
138
|
+
importSource,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Creates a base Astro preset with core components.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* import xmdx from 'astro-xmdx';
|
|
148
|
+
* import { astroPreset } from 'astro-xmdx/presets';
|
|
149
|
+
*
|
|
150
|
+
* export default defineConfig({
|
|
151
|
+
* integrations: [
|
|
152
|
+
* xmdx({
|
|
153
|
+
* presets: [astroPreset()],
|
|
154
|
+
* })
|
|
155
|
+
* ]
|
|
156
|
+
* });
|
|
157
|
+
*/
|
|
158
|
+
export function astroPreset(): PresetConfig {
|
|
159
|
+
return {
|
|
160
|
+
libraries: [astroLibrary],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Merges multiple presets into a single configuration.
|
|
166
|
+
* Later presets override earlier ones for conflicting options.
|
|
167
|
+
*/
|
|
168
|
+
export function mergePresets(presets: PresetConfig[]): PresetConfig {
|
|
169
|
+
if (!presets || presets.length === 0) {
|
|
170
|
+
return { libraries: [astroLibrary] };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const merged: PresetConfig = {
|
|
174
|
+
libraries: [],
|
|
175
|
+
expressiveCode: false,
|
|
176
|
+
starlightComponents: false,
|
|
177
|
+
mdx: undefined,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const libraryIds = new Set<string>();
|
|
181
|
+
|
|
182
|
+
for (const preset of presets) {
|
|
183
|
+
// Merge libraries (deduplicate by id)
|
|
184
|
+
if (Array.isArray(preset.libraries)) {
|
|
185
|
+
for (const lib of preset.libraries) {
|
|
186
|
+
if (!libraryIds.has(lib.id)) {
|
|
187
|
+
libraryIds.add(lib.id);
|
|
188
|
+
merged.libraries.push(lib);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Merge expressiveCode (last wins)
|
|
194
|
+
if (preset.expressiveCode !== undefined) {
|
|
195
|
+
merged.expressiveCode = preset.expressiveCode;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Merge starlightComponents (last wins)
|
|
199
|
+
if (preset.starlightComponents !== undefined) {
|
|
200
|
+
merged.starlightComponents = preset.starlightComponents;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Merge mdx (combine allowImports, last wins for others)
|
|
204
|
+
if (preset.mdx) {
|
|
205
|
+
if (!merged.mdx) {
|
|
206
|
+
merged.mdx = { ...preset.mdx };
|
|
207
|
+
} else {
|
|
208
|
+
const existingAllows = merged.mdx.allowImports ?? [];
|
|
209
|
+
const newAllows = preset.mdx.allowImports ?? [];
|
|
210
|
+
merged.mdx = {
|
|
211
|
+
...merged.mdx,
|
|
212
|
+
...preset.mdx,
|
|
213
|
+
allowImports: [...new Set([...existingAllows, ...newAllows])],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Ensure at least astroLibrary is present
|
|
220
|
+
if (merged.libraries.length === 0) {
|
|
221
|
+
merged.libraries.push(astroLibrary);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return merged;
|
|
225
|
+
}
|