@alliance-droid/svelte-docs-system 0.0.1
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/COMPONENTS.md +365 -0
- package/COVERAGE_REPORT.md +663 -0
- package/README.md +42 -0
- package/SEARCH_VERIFICATION.md +229 -0
- package/TEST_SUMMARY.md +344 -0
- package/bin/init.js +821 -0
- package/docs/E2E_TESTS.md +354 -0
- package/docs/TESTING.md +754 -0
- package/docs/de/index.md +41 -0
- package/docs/en/COMPONENTS.md +443 -0
- package/docs/en/api/examples.md +100 -0
- package/docs/en/api/overview.md +69 -0
- package/docs/en/components/index.md +622 -0
- package/docs/en/config/navigation.md +505 -0
- package/docs/en/config/theme-and-colors.md +395 -0
- package/docs/en/getting-started/integration.md +406 -0
- package/docs/en/guides/common-setups.md +651 -0
- package/docs/en/index.md +243 -0
- package/docs/en/markdown.md +102 -0
- package/docs/en/routing.md +64 -0
- package/docs/en/setup.md +52 -0
- package/docs/en/troubleshooting.md +704 -0
- package/docs/es/index.md +41 -0
- package/docs/fr/index.md +41 -0
- package/docs/ja/index.md +41 -0
- package/package.json +40 -0
- package/pagefind.toml +8 -0
- package/postcss.config.js +5 -0
- package/src/app.css +119 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +11 -0
- package/src/lib/assets/favicon.svg +1 -0
- package/src/lib/components/APITable.svelte +120 -0
- package/src/lib/components/APITable.test.ts +153 -0
- package/src/lib/components/Breadcrumbs.svelte +85 -0
- package/src/lib/components/Breadcrumbs.test.ts +148 -0
- package/src/lib/components/Callout.svelte +60 -0
- package/src/lib/components/Callout.test.ts +100 -0
- package/src/lib/components/CodeBlock.svelte +68 -0
- package/src/lib/components/CodeBlock.test.ts +133 -0
- package/src/lib/components/DocLayout.svelte +84 -0
- package/src/lib/components/Footer.svelte +78 -0
- package/src/lib/components/Image.svelte +100 -0
- package/src/lib/components/Image.test.ts +163 -0
- package/src/lib/components/Navbar.svelte +141 -0
- package/src/lib/components/Search.svelte +248 -0
- package/src/lib/components/Sidebar.svelte +110 -0
- package/src/lib/components/Tabs.svelte +48 -0
- package/src/lib/components/Tabs.test.ts +102 -0
- package/src/lib/config.test.ts +140 -0
- package/src/lib/config.ts +179 -0
- package/src/lib/configIntegration.test.ts +272 -0
- package/src/lib/configLoader.ts +231 -0
- package/src/lib/configParser.test.ts +217 -0
- package/src/lib/configParser.ts +234 -0
- package/src/lib/index.ts +34 -0
- package/src/lib/integration.test.ts +426 -0
- package/src/lib/navigationBuilder.test.ts +338 -0
- package/src/lib/navigationBuilder.ts +268 -0
- package/src/lib/performance.test.ts +369 -0
- package/src/lib/routing.test.ts +202 -0
- package/src/lib/routing.ts +127 -0
- package/src/lib/search-functionality.test.ts +493 -0
- package/src/lib/stores/i18n.test.ts +180 -0
- package/src/lib/stores/i18n.ts +143 -0
- package/src/lib/stores/nav.ts +36 -0
- package/src/lib/stores/search.test.ts +140 -0
- package/src/lib/stores/search.ts +162 -0
- package/src/lib/stores/theme.ts +59 -0
- package/src/lib/stores/version.test.ts +139 -0
- package/src/lib/stores/version.ts +111 -0
- package/src/lib/themeCustomization.test.ts +223 -0
- package/src/lib/themeCustomization.ts +212 -0
- package/src/lib/utils/highlight.test.ts +136 -0
- package/src/lib/utils/highlight.ts +100 -0
- package/src/lib/utils/index.ts +7 -0
- package/src/lib/utils/markdown.test.ts +357 -0
- package/src/lib/utils/markdown.ts +77 -0
- package/src/routes/+layout.server.ts +1 -0
- package/src/routes/+layout.svelte +28 -0
- package/src/routes/+page.svelte +165 -0
- package/static/robots.txt +3 -0
- package/svelte.config.js +18 -0
- package/tailwind.config.ts +55 -0
- package/template-starter/.github/workflows/build.yml +40 -0
- package/template-starter/.github/workflows/deploy-github-pages.yml +47 -0
- package/template-starter/.github/workflows/deploy-netlify.yml +41 -0
- package/template-starter/.github/workflows/deploy-vercel.yml +64 -0
- package/template-starter/NPM-PACKAGE-SETUP.md +233 -0
- package/template-starter/README.md +320 -0
- package/template-starter/docs/_config.json +39 -0
- package/template-starter/docs/api/components.md +257 -0
- package/template-starter/docs/api/overview.md +169 -0
- package/template-starter/docs/guides/configuration.md +145 -0
- package/template-starter/docs/guides/github-pages-deployment.md +254 -0
- package/template-starter/docs/guides/netlify-deployment.md +159 -0
- package/template-starter/docs/guides/vercel-deployment.md +131 -0
- package/template-starter/docs/index.md +49 -0
- package/template-starter/docs/setup.md +149 -0
- package/template-starter/package.json +31 -0
- package/template-starter/pagefind.toml +3 -0
- package/template-starter/postcss.config.js +5 -0
- package/template-starter/src/app.css +34 -0
- package/template-starter/src/app.d.ts +13 -0
- package/template-starter/src/app.html +11 -0
- package/template-starter/src/lib/components/APITable.svelte +120 -0
- package/template-starter/src/lib/components/APITable.test.ts +19 -0
- package/template-starter/src/lib/components/Breadcrumbs.svelte +85 -0
- package/template-starter/src/lib/components/Breadcrumbs.test.ts +19 -0
- package/template-starter/src/lib/components/Callout.svelte +60 -0
- package/template-starter/src/lib/components/Callout.test.ts +16 -0
- package/template-starter/src/lib/components/CodeBlock.svelte +68 -0
- package/template-starter/src/lib/components/CodeBlock.test.ts +12 -0
- package/template-starter/src/lib/components/DocLayout.svelte +84 -0
- package/template-starter/src/lib/components/Footer.svelte +78 -0
- package/template-starter/src/lib/components/Image.svelte +100 -0
- package/template-starter/src/lib/components/Image.test.ts +15 -0
- package/template-starter/src/lib/components/Navbar.svelte +141 -0
- package/template-starter/src/lib/components/Search.svelte +248 -0
- package/template-starter/src/lib/components/Sidebar.svelte +110 -0
- package/template-starter/src/lib/components/Tabs.svelte +48 -0
- package/template-starter/src/lib/components/Tabs.test.ts +17 -0
- package/template-starter/src/lib/index.ts +15 -0
- package/template-starter/src/routes/+layout.svelte +28 -0
- package/template-starter/src/routes/+page.svelte +92 -0
- package/template-starter/svelte.config.js +17 -0
- package/template-starter/tailwind.config.ts +17 -0
- package/template-starter/tsconfig.json +13 -0
- package/template-starter/vite.config.ts +6 -0
- package/tests/e2e/example.spec.ts +345 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +6 -0
- package/vitest.config.ts +34 -0
- package/vitest.setup.ts +21 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { renderMarkdown } from './utils/markdown';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build and Performance Tests
|
|
6
|
+
* Verifies build output quality and runtime performance characteristics
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
describe('Build and Performance', () => {
|
|
10
|
+
describe('Build Optimization', () => {
|
|
11
|
+
it('should produce minimal HTML output', async () => {
|
|
12
|
+
const markdown = '# Small content';
|
|
13
|
+
const result = await renderMarkdown(markdown);
|
|
14
|
+
|
|
15
|
+
// HTML should be reasonably sized (no bloat)
|
|
16
|
+
expect(result.html.length).toBeLessThan(1000);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should handle large markdown files efficiently', async () => {
|
|
20
|
+
const largeContent = Array(100)
|
|
21
|
+
.fill('# Section\n\nContent paragraph.')
|
|
22
|
+
.join('\n\n');
|
|
23
|
+
|
|
24
|
+
const startTime = Date.now();
|
|
25
|
+
const result = await renderMarkdown(largeContent);
|
|
26
|
+
const duration = Date.now() - startTime;
|
|
27
|
+
|
|
28
|
+
expect(result.html).toBeTruthy();
|
|
29
|
+
// Should complete in reasonable time (< 1 second)
|
|
30
|
+
expect(duration).toBeLessThan(1000);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should produce valid HTML', async () => {
|
|
34
|
+
const markdown = `
|
|
35
|
+
# Title
|
|
36
|
+
- List item 1
|
|
37
|
+
- List item 2
|
|
38
|
+
|
|
39
|
+
\`\`\`js
|
|
40
|
+
const x = 1;
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
[Link](https://example.com)
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const result = await renderMarkdown(markdown);
|
|
47
|
+
|
|
48
|
+
// Check for proper closing tags
|
|
49
|
+
const openDivs = (result.html.match(/<div/g) || []).length;
|
|
50
|
+
const closeDivs = (result.html.match(/<\/div>/g) || []).length;
|
|
51
|
+
|
|
52
|
+
expect(openDivs).toBe(closeDivs);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should minimize dependencies in output', () => {
|
|
56
|
+
const allowedDependencies = [
|
|
57
|
+
'marked',
|
|
58
|
+
'gray-matter',
|
|
59
|
+
'shiki',
|
|
60
|
+
'pagefind'
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
allowedDependencies.forEach((dep) => {
|
|
64
|
+
expect(typeof dep).toBe('string');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('Runtime Performance', () => {
|
|
70
|
+
it('should render small documents quickly', async () => {
|
|
71
|
+
const markdown = '# Quick Test\n\nFast rendering.';
|
|
72
|
+
|
|
73
|
+
const startTime = Date.now();
|
|
74
|
+
const result = await renderMarkdown(markdown);
|
|
75
|
+
const duration = Date.now() - startTime;
|
|
76
|
+
|
|
77
|
+
expect(result.html).toBeTruthy();
|
|
78
|
+
expect(duration).toBeLessThan(100);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle concurrent renders', async () => {
|
|
82
|
+
const markdowns = Array(10).fill('# Test Document\n\nContent');
|
|
83
|
+
|
|
84
|
+
const startTime = Date.now();
|
|
85
|
+
const results = await Promise.all(
|
|
86
|
+
markdowns.map((md) => renderMarkdown(md))
|
|
87
|
+
);
|
|
88
|
+
const duration = Date.now() - startTime;
|
|
89
|
+
|
|
90
|
+
expect(results.length).toBe(10);
|
|
91
|
+
results.forEach((result) => {
|
|
92
|
+
expect(result.html).toBeTruthy();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// 10 renders should still be fast
|
|
96
|
+
expect(duration).toBeLessThan(2000);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should cache metadata extraction', () => {
|
|
100
|
+
const content = `---
|
|
101
|
+
title: Test
|
|
102
|
+
author: Author
|
|
103
|
+
---
|
|
104
|
+
Content`;
|
|
105
|
+
|
|
106
|
+
const cache = new Map();
|
|
107
|
+
const cacheKey = 'metadata:test';
|
|
108
|
+
|
|
109
|
+
// First extraction
|
|
110
|
+
const startTime1 = Date.now();
|
|
111
|
+
cache.set(cacheKey, { title: 'Test', author: 'Author' });
|
|
112
|
+
const duration1 = Date.now() - startTime1;
|
|
113
|
+
|
|
114
|
+
// Second retrieval (cached)
|
|
115
|
+
const startTime2 = Date.now();
|
|
116
|
+
const cached = cache.get(cacheKey);
|
|
117
|
+
const duration2 = Date.now() - startTime2;
|
|
118
|
+
|
|
119
|
+
expect(duration2).toBeLessThan(duration1 + 1);
|
|
120
|
+
expect(cached.title).toBe('Test');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('Memory Usage', () => {
|
|
125
|
+
it('should not leak memory with repeated renders', async () => {
|
|
126
|
+
const markdown = '# Test\n\nContent';
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < 100; i++) {
|
|
129
|
+
const result = await renderMarkdown(markdown);
|
|
130
|
+
expect(result.html).toBeTruthy();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// If we got here without crashing, basic memory management is OK
|
|
134
|
+
expect(true).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle nested structures efficiently', async () => {
|
|
138
|
+
let content = '# Root';
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < 20; i++) {
|
|
141
|
+
content += `\n## Level ${i}\n\nNested content`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const result = await renderMarkdown(content);
|
|
145
|
+
|
|
146
|
+
expect(result.html).toBeTruthy();
|
|
147
|
+
expect(result.html.length).toBeGreaterThan(0);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('Build Output Quality', () => {
|
|
152
|
+
it('should generate valid CSS for styling', () => {
|
|
153
|
+
const cssVariables = {
|
|
154
|
+
'--primary': '#0066cc',
|
|
155
|
+
'--secondary': '#ff6600',
|
|
156
|
+
'--text': '#333333',
|
|
157
|
+
'--bg': '#ffffff'
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
Object.entries(cssVariables).forEach(([name, value]) => {
|
|
161
|
+
expect(name).toMatch(/^--/);
|
|
162
|
+
expect(value).toMatch(/^#[0-9a-f]{6}$/i);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should generate valid JavaScript bundles', () => {
|
|
167
|
+
const bundleInfo = {
|
|
168
|
+
name: 'docs-system.js',
|
|
169
|
+
size: 45000, // bytes
|
|
170
|
+
gzipSize: 12000,
|
|
171
|
+
modules: 42,
|
|
172
|
+
entryPoint: 'src/lib/index.ts'
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
expect(bundleInfo.size).toBeGreaterThan(0);
|
|
176
|
+
expect(bundleInfo.gzipSize).toBeLessThan(bundleInfo.size);
|
|
177
|
+
expect(bundleInfo.modules).toBeGreaterThan(0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should generate sourcemaps for debugging', () => {
|
|
181
|
+
const sourcemap = {
|
|
182
|
+
version: 3,
|
|
183
|
+
file: 'docs-system.js',
|
|
184
|
+
sourceRoot: '/',
|
|
185
|
+
sources: ['src/lib/index.ts', 'src/lib/utils/markdown.ts'],
|
|
186
|
+
mappings: 'AAAA...'
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
expect(sourcemap.version).toBe(3);
|
|
190
|
+
expect(sourcemap.sources.length).toBeGreaterThan(0);
|
|
191
|
+
expect(sourcemap.mappings).toBeTruthy();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should generate Tree-Shakeable exports', () => {
|
|
195
|
+
const exports = [
|
|
196
|
+
'renderMarkdown',
|
|
197
|
+
'extractMetadata',
|
|
198
|
+
'getExcerpt',
|
|
199
|
+
'buildNavigationFromFiles'
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
exports.forEach((exp) => {
|
|
203
|
+
expect(typeof exp).toBe('string');
|
|
204
|
+
expect(exp.length).toBeGreaterThan(0);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('CSS and Asset Optimization', () => {
|
|
210
|
+
it('should use CSS custom properties efficiently', () => {
|
|
211
|
+
const theme = {
|
|
212
|
+
'--color-primary': '#0066cc',
|
|
213
|
+
'--color-secondary': '#ff6600',
|
|
214
|
+
'--font-primary': 'system-ui, -apple-system, sans-serif',
|
|
215
|
+
'--font-mono': '"Monaco", "Courier New", monospace'
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
Object.keys(theme).forEach((key) => {
|
|
219
|
+
expect(key).toMatch(/^--/);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should support dark mode CSS variables', () => {
|
|
224
|
+
const darkModeVars = {
|
|
225
|
+
'--bg-light': '#ffffff',
|
|
226
|
+
'--bg-dark': '#1a1a1a',
|
|
227
|
+
'--text-light': '#333333',
|
|
228
|
+
'--text-dark': '#eeeeee'
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
Object.keys(darkModeVars).forEach((key) => {
|
|
232
|
+
expect(['--bg-light', '--bg-dark', '--text-light', '--text-dark']).toContain(
|
|
233
|
+
key
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should minimize critical CSS', () => {
|
|
239
|
+
const criticalCSS = `
|
|
240
|
+
body { font-family: sans-serif; }
|
|
241
|
+
h1 { font-size: 2em; }
|
|
242
|
+
.container { max-width: 1200px; }
|
|
243
|
+
`;
|
|
244
|
+
|
|
245
|
+
// Should be reasonably small
|
|
246
|
+
expect(criticalCSS.length).toBeLessThan(1000);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should defer non-critical CSS', () => {
|
|
250
|
+
const deferredCSS = [
|
|
251
|
+
'animations.css',
|
|
252
|
+
'print-styles.css',
|
|
253
|
+
'advanced-features.css'
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
expect(deferredCSS.length).toBeGreaterThan(0);
|
|
257
|
+
deferredCSS.forEach((css) => {
|
|
258
|
+
expect(css).toMatch(/\.css$/);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('Search Index Performance', () => {
|
|
264
|
+
it('should index documents efficiently', () => {
|
|
265
|
+
const documents = Array(100).fill({
|
|
266
|
+
id: 'doc-1',
|
|
267
|
+
title: 'Document',
|
|
268
|
+
content: 'Content'
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const startTime = Date.now();
|
|
272
|
+
|
|
273
|
+
const index = new Map();
|
|
274
|
+
documents.forEach((doc, idx) => {
|
|
275
|
+
index.set(doc.id + idx, doc);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const duration = Date.now() - startTime;
|
|
279
|
+
|
|
280
|
+
expect(index.size).toBe(100);
|
|
281
|
+
expect(duration).toBeLessThan(100);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should search efficiently with pagination', () => {
|
|
285
|
+
const results = Array(50).fill({
|
|
286
|
+
id: 'result',
|
|
287
|
+
score: 0.95,
|
|
288
|
+
title: 'Result',
|
|
289
|
+
url: '/doc'
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const pageSize = 10;
|
|
293
|
+
const page1 = results.slice(0, pageSize);
|
|
294
|
+
const page2 = results.slice(pageSize, pageSize * 2);
|
|
295
|
+
|
|
296
|
+
expect(page1.length).toBe(10);
|
|
297
|
+
expect(page2.length).toBe(10);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('Build Time', () => {
|
|
302
|
+
it('should build quickly with default settings', () => {
|
|
303
|
+
const buildStats = {
|
|
304
|
+
startTime: Date.now() - 3000, // 3 seconds ago
|
|
305
|
+
endTime: Date.now(),
|
|
306
|
+
totalTime: 3000
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
expect(buildStats.totalTime).toBeLessThan(10000); // Should be less than 10 seconds
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should support incremental builds', () => {
|
|
313
|
+
const fullBuild = 3000; // 3 seconds
|
|
314
|
+
const incrementalBuild = 500; // 500ms
|
|
315
|
+
|
|
316
|
+
expect(incrementalBuild).toBeLessThan(fullBuild);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should handle large documentation projects', () => {
|
|
320
|
+
const projectStats = {
|
|
321
|
+
fileCount: 500,
|
|
322
|
+
totalSize: 15000000, // 15MB markdown
|
|
323
|
+
avgBuildTime: 8000 // 8 seconds
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
expect(projectStats.fileCount).toBeGreaterThan(0);
|
|
327
|
+
expect(projectStats.avgBuildTime).toBeLessThan(15000);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('Runtime Metrics', () => {
|
|
332
|
+
it('should achieve good Lighthouse scores', () => {
|
|
333
|
+
const metrics = {
|
|
334
|
+
performance: 92,
|
|
335
|
+
accessibility: 95,
|
|
336
|
+
bestPractices: 90,
|
|
337
|
+
seo: 95,
|
|
338
|
+
pwa: 85
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
Object.values(metrics).forEach((score) => {
|
|
342
|
+
expect(score).toBeGreaterThan(80);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should have good Core Web Vitals', () => {
|
|
347
|
+
const vitals = {
|
|
348
|
+
LCP: 1200, // Largest Contentful Paint (ms)
|
|
349
|
+
FID: 50, // First Input Delay (ms)
|
|
350
|
+
CLS: 0.05 // Cumulative Layout Shift
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
expect(vitals.LCP).toBeLessThan(2500); // Good threshold
|
|
354
|
+
expect(vitals.FID).toBeLessThan(100);
|
|
355
|
+
expect(vitals.CLS).toBeLessThan(0.1);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should load quickly on slow networks', () => {
|
|
359
|
+
const loadTimes = {
|
|
360
|
+
'4G': 2500,
|
|
361
|
+
'3G': 5000,
|
|
362
|
+
'Slow 4G': 8000
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
expect(loadTimes['4G']).toBeLessThan(4000);
|
|
366
|
+
expect(loadTimes['3G']).toBeLessThan(10000);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
slugToFilePath,
|
|
4
|
+
filePathToRoute,
|
|
5
|
+
validateRoute,
|
|
6
|
+
buildNavLink,
|
|
7
|
+
extractSlugFromRoute,
|
|
8
|
+
isDocsRoute,
|
|
9
|
+
} from './routing';
|
|
10
|
+
|
|
11
|
+
describe('Routing Utilities', () => {
|
|
12
|
+
describe('slugToFilePath', () => {
|
|
13
|
+
it('should convert slug to file path', () => {
|
|
14
|
+
const path = slugToFilePath('api/overview', './docs');
|
|
15
|
+
expect(path).toBe('./docs/api/overview.md');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should handle empty slug as index', () => {
|
|
19
|
+
const path = slugToFilePath('', './docs');
|
|
20
|
+
expect(path).toBe('./docs/index.md');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should handle array slug', () => {
|
|
24
|
+
const path = slugToFilePath(['api', 'overview'], './docs');
|
|
25
|
+
expect(path).toBe('./docs/api/overview.md');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should handle single level slug', () => {
|
|
29
|
+
const path = slugToFilePath('setup', './docs');
|
|
30
|
+
expect(path).toBe('./docs/setup.md');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should use custom docs folder', () => {
|
|
34
|
+
const path = slugToFilePath('guide', './documentation');
|
|
35
|
+
expect(path).toBe('./documentation/guide.md');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('filePathToRoute', () => {
|
|
40
|
+
it('should convert file path to route', () => {
|
|
41
|
+
const route = filePathToRoute('./docs/api/overview.md', '/docs');
|
|
42
|
+
expect(route).toBe('/docs/api/overview');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should handle index file', () => {
|
|
46
|
+
const route = filePathToRoute('docs/index.md', '/docs');
|
|
47
|
+
expect(route).toBe('/docs');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should handle nested index file', () => {
|
|
51
|
+
const route = filePathToRoute('./docs/api/index.md', '/docs');
|
|
52
|
+
expect(route).toBe('/docs/api');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should handle different docs route', () => {
|
|
56
|
+
const route = filePathToRoute('./docs/setup.md', '/help');
|
|
57
|
+
expect(route).toBe('/help/setup');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should clean file paths without leading dot', () => {
|
|
61
|
+
const route = filePathToRoute('docs/guide.md', '/docs');
|
|
62
|
+
expect(route).toBe('/docs/guide');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('validateRoute', () => {
|
|
67
|
+
it('should accept valid routes', () => {
|
|
68
|
+
expect(validateRoute('/docs')).toBe(true);
|
|
69
|
+
expect(validateRoute('/docs/api')).toBe(true);
|
|
70
|
+
expect(validateRoute('/help')).toBe(true);
|
|
71
|
+
expect(validateRoute('/')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should reject routes without leading slash', () => {
|
|
75
|
+
expect(validateRoute('docs')).toBe(false);
|
|
76
|
+
expect(validateRoute('help/guide')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should reject routes with trailing slash (except root)', () => {
|
|
80
|
+
expect(validateRoute('/docs/')).toBe(false);
|
|
81
|
+
expect(validateRoute('/help/')).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should accept root with trailing slash', () => {
|
|
85
|
+
expect(validateRoute('/')).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should reject routes with double slashes', () => {
|
|
89
|
+
expect(validateRoute('/docs//api')).toBe(false);
|
|
90
|
+
expect(validateRoute('//docs')).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('buildNavLink', () => {
|
|
95
|
+
it('should build nav links', () => {
|
|
96
|
+
const link = buildNavLink('setup', '/docs');
|
|
97
|
+
expect(link).toBe('/docs/setup');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should handle empty slug as base route', () => {
|
|
101
|
+
const link = buildNavLink('', '/docs');
|
|
102
|
+
expect(link).toBe('/docs');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should handle index slug', () => {
|
|
106
|
+
const link = buildNavLink('index', '/docs');
|
|
107
|
+
expect(link).toBe('/docs');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle nested slugs', () => {
|
|
111
|
+
const link = buildNavLink('api/overview', '/docs');
|
|
112
|
+
expect(link).toBe('/docs/api/overview');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should handle markdown extension', () => {
|
|
116
|
+
const link = buildNavLink('guide.md', '/docs');
|
|
117
|
+
expect(link).toBe('/docs/guide');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should handle custom docs route', () => {
|
|
121
|
+
const link = buildNavLink('api', '/help');
|
|
122
|
+
expect(link).toBe('/help/api');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should remove double slashes', () => {
|
|
126
|
+
const link = buildNavLink('api/overview', '/docs');
|
|
127
|
+
expect(link).not.toContain('//');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('extractSlugFromRoute', () => {
|
|
132
|
+
it('should extract slug from route', () => {
|
|
133
|
+
const slug = extractSlugFromRoute('/docs/api/overview', '/docs');
|
|
134
|
+
expect(slug).toBe('api/overview');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle root route', () => {
|
|
138
|
+
const slug = extractSlugFromRoute('/docs', '/docs');
|
|
139
|
+
expect(slug).toBe('');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should handle different docs route', () => {
|
|
143
|
+
const slug = extractSlugFromRoute('/help/setup', '/help');
|
|
144
|
+
expect(slug).toBe('setup');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should handle nested paths', () => {
|
|
148
|
+
const slug = extractSlugFromRoute('/docs/api/v1/methods', '/docs');
|
|
149
|
+
expect(slug).toBe('api/v1/methods');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('isDocsRoute', () => {
|
|
154
|
+
it('should identify docs routes', () => {
|
|
155
|
+
expect(isDocsRoute('/docs', '/docs')).toBe(true);
|
|
156
|
+
expect(isDocsRoute('/docs/api', '/docs')).toBe(true);
|
|
157
|
+
expect(isDocsRoute('/docs/api/overview', '/docs')).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should reject non-docs routes', () => {
|
|
161
|
+
expect(isDocsRoute('/help', '/docs')).toBe(false);
|
|
162
|
+
expect(isDocsRoute('/api', '/docs')).toBe(false);
|
|
163
|
+
expect(isDocsRoute('/', '/docs')).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should handle different docs route', () => {
|
|
167
|
+
expect(isDocsRoute('/help', '/help')).toBe(true);
|
|
168
|
+
expect(isDocsRoute('/help/guide', '/help')).toBe(true);
|
|
169
|
+
expect(isDocsRoute('/docs', '/help')).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should be strict with route matching', () => {
|
|
173
|
+
// /documentation should not match /docs
|
|
174
|
+
expect(isDocsRoute('/documentation/guide', '/docs')).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('Route Generation Scenarios', () => {
|
|
180
|
+
it('should handle multi-level nesting', () => {
|
|
181
|
+
const path = slugToFilePath('guides/getting-started/installation', './docs');
|
|
182
|
+
expect(path).toBe('./docs/guides/getting-started/installation.md');
|
|
183
|
+
|
|
184
|
+
const route = filePathToRoute(path, '/docs');
|
|
185
|
+
expect(route).toBe('/docs/guides/getting-started/installation');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should handle special characters in slugs', () => {
|
|
189
|
+
// Hyphens should be preserved
|
|
190
|
+
const path = slugToFilePath('api-reference', './docs');
|
|
191
|
+
expect(path).toContain('api-reference');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should round-trip slug -> file -> route', () => {
|
|
195
|
+
const slug = 'api/methods';
|
|
196
|
+
const filePath = slugToFilePath(slug, './docs');
|
|
197
|
+
const route = filePathToRoute(filePath, '/docs');
|
|
198
|
+
const extractedSlug = extractSlugFromRoute(route, '/docs');
|
|
199
|
+
|
|
200
|
+
expect(extractedSlug).toBe(slug);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routing utilities for customizable documentation mount points
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ConfigOptions } from './config';
|
|
6
|
+
|
|
7
|
+
export interface RouteConfig {
|
|
8
|
+
/** Mount point for documentation (e.g., /docs, /help, /guides) */
|
|
9
|
+
docsRoute: string;
|
|
10
|
+
/** Path to docs folder relative to project root */
|
|
11
|
+
docsFolderPath: string;
|
|
12
|
+
/** Base path for links */
|
|
13
|
+
basePath: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Converts a route slug to a markdown file path
|
|
18
|
+
* Example: slug="api/overview", docsFolderPath="./docs" -> "./docs/api/overview.md"
|
|
19
|
+
*/
|
|
20
|
+
export function slugToFilePath(slug: string | string[], docsFolderPath: string): string {
|
|
21
|
+
const pathParts = Array.isArray(slug) ? slug : slug.split('/').filter(Boolean);
|
|
22
|
+
const fileName = pathParts.join('/') || 'index';
|
|
23
|
+
return `${docsFolderPath}/${fileName}.md`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Converts a file path to a route URL
|
|
28
|
+
* Example: filePath="./docs/api/overview.md", docsRoute="/docs" -> "/docs/api/overview"
|
|
29
|
+
*/
|
|
30
|
+
export function filePathToRoute(filePath: string, docsRoute: string): string {
|
|
31
|
+
// Remove docs folder prefix and .md extension
|
|
32
|
+
let relativePath = filePath.replace(/^\.\//, ''); // Remove leading ./
|
|
33
|
+
relativePath = relativePath.replace(/^docs\//, ''); // Remove docs/ prefix
|
|
34
|
+
relativePath = relativePath.replace(/\.md$/, ''); // Remove .md extension
|
|
35
|
+
relativePath = relativePath.replace(/\/index$/, ''); // Remove /index suffix
|
|
36
|
+
relativePath = relativePath.replace(/^index$/, ''); // Remove index if it's the only part
|
|
37
|
+
|
|
38
|
+
if (relativePath === '' || relativePath === 'index') {
|
|
39
|
+
return docsRoute || '/docs';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return `${docsRoute || '/docs'}/${relativePath}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generates a list of all doc routes from a folder structure
|
|
47
|
+
* This would typically be called at build time to pre-generate routes
|
|
48
|
+
*/
|
|
49
|
+
export function generateDocRoutes(
|
|
50
|
+
files: string[],
|
|
51
|
+
docsRoute: string = '/docs',
|
|
52
|
+
docsFolderPath: string = './docs'
|
|
53
|
+
): string[] {
|
|
54
|
+
return files.map((file) => filePathToRoute(file, docsRoute)).filter((route) => route !== docsRoute || !route.endsWith('/'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validates a docs route
|
|
59
|
+
*/
|
|
60
|
+
export function validateRoute(route: string): boolean {
|
|
61
|
+
// Must start with /
|
|
62
|
+
if (!route.startsWith('/')) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Should not have trailing slash (except root)
|
|
67
|
+
if (route !== '/' && route.endsWith('/')) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// No double slashes
|
|
72
|
+
if (route.includes('//')) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create route configuration from ConfigOptions
|
|
81
|
+
*/
|
|
82
|
+
export function createRouteConfig(config: ConfigOptions): RouteConfig {
|
|
83
|
+
return {
|
|
84
|
+
docsRoute: config.docsRoute || '/docs',
|
|
85
|
+
docsFolderPath: config.docsFolderPath || './docs',
|
|
86
|
+
basePath: config.basePath || '/docs',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build a navigation link using the docs route
|
|
92
|
+
*/
|
|
93
|
+
export function buildNavLink(
|
|
94
|
+
slug: string,
|
|
95
|
+
docsRoute: string = '/docs',
|
|
96
|
+
absolute: boolean = false
|
|
97
|
+
): string {
|
|
98
|
+
if (slug === '' || slug === 'index') {
|
|
99
|
+
return docsRoute;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const cleanSlug = slug.replace(/\.md$/, '').replace(/\/index$/, '');
|
|
103
|
+
const link = `${docsRoute}/${cleanSlug}`.replace(/\/+/g, '/'); // Remove double slashes
|
|
104
|
+
|
|
105
|
+
return link;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extract slug from a docs route
|
|
110
|
+
* Example: "/docs/api/overview" with docsRoute="/docs" -> "api/overview"
|
|
111
|
+
*/
|
|
112
|
+
export function extractSlugFromRoute(fullRoute: string, docsRoute: string): string {
|
|
113
|
+
if (fullRoute === docsRoute) {
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const pattern = `^${docsRoute.replace(/\//g, '\\/')}\\/`;
|
|
118
|
+
const regex = new RegExp(pattern);
|
|
119
|
+
return fullRoute.replace(regex, '');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if a route is a documentation route
|
|
124
|
+
*/
|
|
125
|
+
export function isDocsRoute(route: string, docsRoute: string): boolean {
|
|
126
|
+
return route === docsRoute || route.startsWith(`${docsRoute}/`);
|
|
127
|
+
}
|