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,252 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import { resolveExpressiveCodeConfig, resolveStarlightConfig } from './config.js';
3
+ import {
4
+ createRegistry,
5
+ starlightLibrary,
6
+ expressiveCodeLibrary,
7
+ } from 'xmdx/registry';
8
+
9
+ const STARLIGHT_COMPONENTS = starlightLibrary.components.map((c) => c.name);
10
+ const STARLIGHT_COMPONENTS_MODULE = starlightLibrary.defaultModulePath;
11
+ const EXPRESSIVE_CODE_COMPONENT = expressiveCodeLibrary.components[0]?.name ?? 'Code';
12
+ const EXPRESSIVE_CODE_MODULE = expressiveCodeLibrary.defaultModulePath;
13
+
14
+ describe('resolveExpressiveCodeConfig', () => {
15
+ it('should return null for falsy values', () => {
16
+ expect(resolveExpressiveCodeConfig(null)).toBe(null);
17
+ expect(resolveExpressiveCodeConfig(undefined)).toBe(null);
18
+ expect(resolveExpressiveCodeConfig(false)).toBe(null);
19
+ });
20
+
21
+ it('should return default config for true', () => {
22
+ const result = resolveExpressiveCodeConfig(true);
23
+
24
+ expect(result).toEqual({
25
+ component: EXPRESSIVE_CODE_COMPONENT,
26
+ moduleId: EXPRESSIVE_CODE_MODULE,
27
+ });
28
+ });
29
+
30
+ it('should use defaults for empty object', () => {
31
+ const result = resolveExpressiveCodeConfig({});
32
+
33
+ expect(result).toEqual({
34
+ component: EXPRESSIVE_CODE_COMPONENT,
35
+ moduleId: EXPRESSIVE_CODE_MODULE,
36
+ });
37
+ });
38
+
39
+ it('should use custom component name', () => {
40
+ const result = resolveExpressiveCodeConfig({
41
+ component: 'CustomCode',
42
+ });
43
+
44
+ expect(result).toEqual({
45
+ component: 'CustomCode',
46
+ moduleId: EXPRESSIVE_CODE_MODULE,
47
+ });
48
+ });
49
+
50
+ it('should use custom module path', () => {
51
+ const result = resolveExpressiveCodeConfig({
52
+ module: 'my-custom-module',
53
+ });
54
+
55
+ expect(result).toEqual({
56
+ component: EXPRESSIVE_CODE_COMPONENT,
57
+ moduleId: 'my-custom-module',
58
+ });
59
+ });
60
+
61
+ it('should use both custom component and module', () => {
62
+ const result = resolveExpressiveCodeConfig({
63
+ component: 'MyCode',
64
+ module: 'my-module',
65
+ });
66
+
67
+ expect(result).toEqual({
68
+ component: 'MyCode',
69
+ moduleId: 'my-module',
70
+ });
71
+ });
72
+
73
+ it('should ignore empty string component', () => {
74
+ const result = resolveExpressiveCodeConfig({
75
+ component: '',
76
+ });
77
+
78
+ expect(result?.component).toBe(EXPRESSIVE_CODE_COMPONENT);
79
+ });
80
+
81
+ it('should ignore empty string module', () => {
82
+ const result = resolveExpressiveCodeConfig({
83
+ module: '',
84
+ });
85
+
86
+ expect(result?.moduleId).toBe(EXPRESSIVE_CODE_MODULE);
87
+ });
88
+ });
89
+
90
+ describe('resolveStarlightConfig', () => {
91
+ it('should return null for falsy values', () => {
92
+ expect(resolveStarlightConfig(null)).toBe(null);
93
+ expect(resolveStarlightConfig(undefined)).toBe(null);
94
+ expect(resolveStarlightConfig(false)).toBe(null);
95
+ });
96
+
97
+ it('should return default config for true', () => {
98
+ const result = resolveStarlightConfig(true);
99
+
100
+ expect(result).toEqual({
101
+ components: STARLIGHT_COMPONENTS,
102
+ moduleId: STARLIGHT_COMPONENTS_MODULE,
103
+ });
104
+ });
105
+
106
+ it('should use defaults for empty object', () => {
107
+ const result = resolveStarlightConfig({});
108
+
109
+ expect(result).toEqual({
110
+ components: STARLIGHT_COMPONENTS,
111
+ moduleId: STARLIGHT_COMPONENTS_MODULE,
112
+ });
113
+ });
114
+
115
+ it('should use custom components array', () => {
116
+ const customComponents = ['Aside', 'Tabs'];
117
+ const result = resolveStarlightConfig({
118
+ components: customComponents,
119
+ });
120
+
121
+ expect(result).toEqual({
122
+ components: customComponents,
123
+ moduleId: STARLIGHT_COMPONENTS_MODULE,
124
+ });
125
+ });
126
+
127
+ it('should use custom module path', () => {
128
+ const result = resolveStarlightConfig({
129
+ module: 'my-starlight-module',
130
+ });
131
+
132
+ expect(result).toEqual({
133
+ components: STARLIGHT_COMPONENTS,
134
+ moduleId: 'my-starlight-module',
135
+ });
136
+ });
137
+
138
+ it('should use both custom components and module', () => {
139
+ const customComponents = ['CustomAside'];
140
+ const result = resolveStarlightConfig({
141
+ components: customComponents,
142
+ module: 'custom-module',
143
+ });
144
+
145
+ expect(result).toEqual({
146
+ components: customComponents,
147
+ moduleId: 'custom-module',
148
+ });
149
+ });
150
+
151
+ it('should ignore empty string module', () => {
152
+ const result = resolveStarlightConfig({
153
+ module: '',
154
+ });
155
+
156
+ expect(result?.moduleId).toBe(STARLIGHT_COMPONENTS_MODULE);
157
+ });
158
+
159
+ it('should handle empty components array', () => {
160
+ const result = resolveStarlightConfig({
161
+ components: [],
162
+ });
163
+
164
+ expect(result?.components).toEqual([]);
165
+ });
166
+ });
167
+
168
+ describe('resolveExpressiveCodeConfig with registry', () => {
169
+ const registry = createRegistry([expressiveCodeLibrary]);
170
+
171
+ it('should return default config from registry for true', () => {
172
+ const result = resolveExpressiveCodeConfig(true, registry);
173
+
174
+ expect(result).toEqual({
175
+ component: 'Code',
176
+ moduleId: 'astro-expressive-code/components',
177
+ });
178
+ });
179
+
180
+ it('should use registry defaults for empty object', () => {
181
+ const result = resolveExpressiveCodeConfig({}, registry);
182
+
183
+ expect(result).toEqual({
184
+ component: 'Code',
185
+ moduleId: 'astro-expressive-code/components',
186
+ });
187
+ });
188
+
189
+ it('should allow custom overrides with registry', () => {
190
+ const result = resolveExpressiveCodeConfig({
191
+ component: 'CustomCode',
192
+ module: 'my-module',
193
+ }, registry);
194
+
195
+ expect(result).toEqual({
196
+ component: 'CustomCode',
197
+ moduleId: 'my-module',
198
+ });
199
+ });
200
+
201
+ it('should fall back to constants when registry has no matching components', () => {
202
+ const emptyRegistry = createRegistry([]);
203
+ const result = resolveExpressiveCodeConfig(true, emptyRegistry);
204
+
205
+ expect(result).toEqual({
206
+ component: EXPRESSIVE_CODE_COMPONENT,
207
+ moduleId: EXPRESSIVE_CODE_MODULE,
208
+ });
209
+ });
210
+ });
211
+
212
+ describe('resolveStarlightConfig with registry', () => {
213
+ const registry = createRegistry([starlightLibrary]);
214
+
215
+ it('should return default config from registry for true', () => {
216
+ const result = resolveStarlightConfig(true, registry);
217
+
218
+ expect(result?.moduleId).toBe('@astrojs/starlight/components');
219
+ expect(result?.components).toContain('Aside');
220
+ expect(result?.components).toContain('Tabs');
221
+ expect(result?.components).toContain('TabItem');
222
+ });
223
+
224
+ it('should use registry defaults for empty object', () => {
225
+ const result = resolveStarlightConfig({}, registry);
226
+
227
+ expect(result?.moduleId).toBe('@astrojs/starlight/components');
228
+ expect((result?.components.length ?? 0) > 0).toBe(true);
229
+ });
230
+
231
+ it('should allow custom overrides with registry', () => {
232
+ const result = resolveStarlightConfig({
233
+ components: ['CustomAside'],
234
+ module: 'my-module',
235
+ }, registry);
236
+
237
+ expect(result).toEqual({
238
+ components: ['CustomAside'],
239
+ moduleId: 'my-module',
240
+ });
241
+ });
242
+
243
+ it('should fall back to constants when registry has no matching components', () => {
244
+ const emptyRegistry = createRegistry([]);
245
+ const result = resolveStarlightConfig(true, emptyRegistry);
246
+
247
+ expect(result).toEqual({
248
+ components: STARLIGHT_COMPONENTS,
249
+ moduleId: STARLIGHT_COMPONENTS_MODULE,
250
+ });
251
+ });
252
+ });
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Configuration resolution utilities
3
+ * @module utils/config
4
+ */
5
+
6
+ import type { Registry } from 'xmdx/registry';
7
+ import { starlightLibrary, expressiveCodeLibrary } from 'xmdx/registry';
8
+
9
+ /**
10
+ * Resolved ExpressiveCode configuration.
11
+ */
12
+ export interface ExpressiveCodeConfig {
13
+ /** Component name (e.g., "Code" or "ExpressiveCode") */
14
+ component: string;
15
+ /** Module to import from */
16
+ moduleId: string;
17
+ }
18
+
19
+ /**
20
+ * ExpressiveCode user configuration input.
21
+ */
22
+ export interface ExpressiveCodeUserConfig {
23
+ /** Whether enabled */
24
+ enabled?: boolean;
25
+ /** Component name */
26
+ component?: string;
27
+ /** Module path */
28
+ module?: string;
29
+ }
30
+
31
+ /**
32
+ * Resolve ExpressiveCode configuration.
33
+ * Normalizes boolean/object config into consistent object format.
34
+ *
35
+ * @example
36
+ * resolveExpressiveCodeConfig(true)
37
+ * // => { component: "ExpressiveCode", moduleId: "astro-expressive-code/components" }
38
+ *
39
+ * resolveExpressiveCodeConfig({ component: "Code", module: "my-module" })
40
+ * // => { component: "Code", moduleId: "my-module" }
41
+ */
42
+ export function resolveExpressiveCodeConfig(
43
+ config: boolean | ExpressiveCodeUserConfig | null | undefined,
44
+ registry?: Registry
45
+ ): ExpressiveCodeConfig | null {
46
+ if (!config) return null;
47
+
48
+ // Get defaults from registry if available, otherwise use library preset
49
+ let defaultComponent = expressiveCodeLibrary.components[0]?.name ?? 'Code';
50
+ let defaultModuleId = expressiveCodeLibrary.defaultModulePath;
51
+
52
+ if (registry) {
53
+ const ecComponents = registry.getComponentsByModule(expressiveCodeLibrary.defaultModulePath);
54
+ if (ecComponents.length > 0 && ecComponents[0]) {
55
+ defaultComponent = ecComponents[0].name;
56
+ defaultModuleId = ecComponents[0].modulePath;
57
+ }
58
+ }
59
+
60
+ if (config === true) {
61
+ return {
62
+ component: defaultComponent,
63
+ moduleId: defaultModuleId,
64
+ };
65
+ }
66
+ if (typeof config === 'object') {
67
+ const component =
68
+ typeof config.component === 'string' && config.component.length > 0
69
+ ? config.component
70
+ : defaultComponent;
71
+ const moduleId =
72
+ typeof config.module === 'string' && config.module.length > 0
73
+ ? config.module
74
+ : defaultModuleId;
75
+ return { component, moduleId };
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Resolved Starlight configuration.
82
+ */
83
+ export interface StarlightConfig {
84
+ /** Component names to inject */
85
+ components: string[];
86
+ /** Module to import from */
87
+ moduleId: string;
88
+ }
89
+
90
+ /**
91
+ * Starlight user configuration input.
92
+ */
93
+ export interface StarlightUserConfig {
94
+ /** Component names */
95
+ components?: string[];
96
+ /** Module path */
97
+ module?: string;
98
+ }
99
+
100
+ /**
101
+ * Resolve Starlight configuration.
102
+ * Normalizes boolean/object config into consistent object format.
103
+ *
104
+ * @example
105
+ * resolveStarlightConfig(true)
106
+ * // => { components: ["Aside", "Tabs", ...], moduleId: "@astrojs/starlight/components" }
107
+ *
108
+ * resolveStarlightConfig({ components: ["Aside"], module: "my-module" })
109
+ * // => { components: ["Aside"], moduleId: "my-module" }
110
+ */
111
+ export function resolveStarlightConfig(
112
+ config: boolean | StarlightUserConfig | null | undefined,
113
+ registry?: Registry
114
+ ): StarlightConfig | null {
115
+ if (!config) return null;
116
+
117
+ // Get defaults from registry if available, otherwise use library preset
118
+ let defaultComponents = starlightLibrary.components.map((c) => c.name);
119
+ let defaultModuleId = starlightLibrary.defaultModulePath;
120
+
121
+ if (registry) {
122
+ const slComponents = registry.getComponentsByModule(starlightLibrary.defaultModulePath);
123
+ if (slComponents.length > 0 && slComponents[0]) {
124
+ defaultComponents = slComponents.map((c) => c.name);
125
+ defaultModuleId = slComponents[0].modulePath;
126
+ }
127
+ }
128
+
129
+ if (config === true) {
130
+ return {
131
+ components: defaultComponents,
132
+ moduleId: defaultModuleId,
133
+ };
134
+ }
135
+ if (typeof config === 'object') {
136
+ const components = Array.isArray(config.components)
137
+ ? config.components
138
+ : defaultComponents;
139
+ const moduleId =
140
+ typeof config.module === 'string' && config.module.length > 0
141
+ ? config.module
142
+ : defaultModuleId;
143
+ return { components, moduleId };
144
+ }
145
+ return null;
146
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Frontmatter detection and stripping utilities.
3
+ * @module utils/frontmatter
4
+ */
5
+
6
+ /**
7
+ * Robust frontmatter regex that handles:
8
+ * - Optional BOM at file start
9
+ * - Optional leading blank lines
10
+ * - Trailing spaces after `---`
11
+ * - Both `---` and `...` as closing markers (YAML spec)
12
+ */
13
+ const FRONTMATTER_REGEX =
14
+ /^\uFEFF?(?:\s*\r?\n)*---[ \t]*\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/;
15
+
16
+ /**
17
+ * Strip frontmatter from source, returning content only.
18
+ * If no frontmatter found, returns the original source.
19
+ */
20
+ export function stripFrontmatter(source: string): string {
21
+ const match = source.match(FRONTMATTER_REGEX);
22
+ if (!match) return source;
23
+ return source.slice(match[0].length);
24
+ }
25
+
26
+ /**
27
+ * Get the byte length of frontmatter (including delimiters).
28
+ * Returns 0 if no frontmatter found.
29
+ */
30
+ export function getFrontmatterLength(source: string): number {
31
+ const match = source.match(FRONTMATTER_REGEX);
32
+ return match ? match[0].length : 0;
33
+ }