This is page 4 of 5. Use http://codebase.md/marianfoo/mcp-sap-docs?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .cursor
│ └── rules
│ ├── 00-overview.mdc
│ ├── 10-search-stack.mdc
│ ├── 20-tools-and-apis.mdc
│ ├── 30-tests-and-output.mdc
│ ├── 40-deploy.mdc
│ ├── 50-metadata-config.mdc
│ ├── 60-adding-github-sources.mdc
│ ├── 70-tool-usage-guide.mdc
│ └── 80-abap-integration.mdc
├── .cursorignore
├── .gitattributes
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── config.yml
│ │ ├── missing-documentation.yml
│ │ └── new-documentation-source.yml
│ └── workflows
│ ├── deploy-mcp-sap-docs.yml
│ ├── test-pr.yml
│ └── update-submodules.yml
├── .gitignore
├── .gitmodules
├── .npmignore
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── docs
│ ├── ABAP-INTEGRATION-SUMMARY.md
│ ├── ABAP-MULTI-VERSION-INTEGRATION.md
│ ├── ABAP-STANDARD-INTEGRATION.md
│ ├── ABAP-USAGE-GUIDE.md
│ ├── ARCHITECTURE.md
│ ├── COMMUNITY-SEARCH-IMPLEMENTATION.md
│ ├── CONTENT-SIZE-LIMITS.md
│ ├── CURSOR-SETUP.md
│ ├── DEV.md
│ ├── FTS5-IMPLEMENTATION-COMPLETE.md
│ ├── LLM-FRIENDLY-IMPROVEMENTS.md
│ ├── METADATA-CONSOLIDATION.md
│ ├── TEST-SEARCH.md
│ └── TESTS.md
├── ecosystem.config.cjs
├── index.html
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── REMOTE_SETUP.md
├── scripts
│ ├── build-fts.ts
│ ├── build-index.ts
│ ├── check-version.js
│ └── summarize-src.js
├── server.json
├── setup.sh
├── src
│ ├── global.d.ts
│ ├── http-server.ts
│ ├── lib
│ │ ├── BaseServerHandler.ts
│ │ ├── communityBestMatch.ts
│ │ ├── config.ts
│ │ ├── localDocs.ts
│ │ ├── logger.ts
│ │ ├── metadata.ts
│ │ ├── sapHelp.ts
│ │ ├── search.ts
│ │ ├── searchDb.ts
│ │ ├── truncate.ts
│ │ ├── types.ts
│ │ └── url-generation
│ │ ├── abap.ts
│ │ ├── BaseUrlGenerator.ts
│ │ ├── cap.ts
│ │ ├── cloud-sdk.ts
│ │ ├── dsag.ts
│ │ ├── GenericUrlGenerator.ts
│ │ ├── index.ts
│ │ ├── README.md
│ │ ├── sapui5.ts
│ │ ├── utils.ts
│ │ └── wdi5.ts
│ ├── metadata.json
│ ├── server.ts
│ └── streamable-http-server.ts
├── test
│ ├── _utils
│ │ ├── httpClient.js
│ │ └── parseResults.js
│ ├── community-search.ts
│ ├── comprehensive-url-generation.test.ts
│ ├── performance
│ │ └── README.md
│ ├── prompts.test.ts
│ ├── quick-url-test.ts
│ ├── README.md
│ ├── tools
│ │ ├── run-tests.js
│ │ ├── sap_docs_search
│ │ │ ├── search-cap-docs.js
│ │ │ ├── search-cloud-sdk-ai.js
│ │ │ ├── search-cloud-sdk-js.js
│ │ │ └── search-sapui5-docs.js
│ │ ├── search-url-verification.js
│ │ ├── search.generic.spec.js
│ │ └── search.smoke.js
│ ├── url-status.ts
│ └── validate-urls.ts
├── test-community-search.js
├── test-search-interactive.ts
├── test-search.http
├── test-search.ts
├── tsconfig.json
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/src/metadata.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "version": 1,
3 | "updated_at": "2025-01-14",
4 | "description": "Centralized configuration for SAP Docs MCP search system",
5 |
6 | "sources": [
7 | {
8 | "id": "sapui5",
9 | "type": "documentation",
10 | "lang": "en",
11 | "boost": 0.1,
12 | "tags": ["ui5", "frontend", "javascript"],
13 | "description": "SAPUI5 framework documentation",
14 | "libraryId": "/sapui5",
15 | "sourcePath": "sapui5-docs/docs",
16 | "baseUrl": "https://ui5.sap.com",
17 | "pathPattern": "/#/topic/{file}",
18 | "anchorStyle": "custom"
19 | },
20 | {
21 | "id": "cap",
22 | "type": "documentation",
23 | "lang": "en",
24 | "boost": 0.1,
25 | "tags": ["backend", "nodejs", "java", "cds"],
26 | "description": "SAP Cloud Application Programming model",
27 | "libraryId": "/cap",
28 | "sourcePath": "cap-docs",
29 | "baseUrl": "https://cap.cloud.sap",
30 | "pathPattern": "/docs/{file}",
31 | "anchorStyle": "docsify"
32 | },
33 | {
34 | "id": "openui5-api",
35 | "type": "api",
36 | "lang": "en",
37 | "boost": 0.1,
38 | "tags": ["api", "controls", "ui5"],
39 | "description": "OpenUI5 API documentation",
40 | "libraryId": "/openui5-api",
41 | "sourcePath": "openui5/src",
42 | "baseUrl": "https://sdk.openui5.org",
43 | "pathPattern": "/#/api/{file}",
44 | "anchorStyle": "custom"
45 | },
46 | {
47 | "id": "openui5-samples",
48 | "type": "samples",
49 | "lang": "en",
50 | "boost": 0.05,
51 | "tags": ["samples", "examples", "ui5"],
52 | "description": "OpenUI5 code samples",
53 | "libraryId": "/openui5-samples",
54 | "sourcePath": "openui5/src",
55 | "baseUrl": "https://sdk.openui5.org",
56 | "pathPattern": "/entity/{file}",
57 | "anchorStyle": "custom"
58 | },
59 | {
60 | "id": "wdi5",
61 | "type": "documentation",
62 | "lang": "en",
63 | "boost": 0.05,
64 | "tags": ["testing", "e2e", "webdriver"],
65 | "description": "wdi5 testing framework",
66 | "libraryId": "/wdi5",
67 | "sourcePath": "wdi5/docs",
68 | "baseUrl": "https://ui5-community.github.io/wdi5",
69 | "pathPattern": "#{file}",
70 | "anchorStyle": "docsify"
71 | },
72 | {
73 | "id": "ui5-tooling",
74 | "type": "documentation",
75 | "lang": "en",
76 | "boost": 0.0,
77 | "tags": ["tooling", "build", "cli"],
78 | "description": "UI5 Tooling documentation",
79 | "libraryId": "/ui5-tooling",
80 | "sourcePath": "ui5-tooling/docs",
81 | "baseUrl": "https://sap.github.io/ui5-tooling/v4",
82 | "pathPattern": "/pages/{file}",
83 | "anchorStyle": "github"
84 | },
85 | {
86 | "id": "cloud-mta-build-tool",
87 | "type": "documentation",
88 | "lang": "en",
89 | "boost": 0.0,
90 | "tags": ["mta", "build", "deployment"],
91 | "description": "Cloud MTA Build Tool",
92 | "libraryId": "/cloud-mta-build-tool",
93 | "sourcePath": "cloud-mta-build-tool/docs/docs",
94 | "baseUrl": "https://sap.github.io/cloud-mta-build-tool",
95 | "pathPattern": "/{file}",
96 | "anchorStyle": "github"
97 | },
98 | {
99 | "id": "ui5-webcomponents",
100 | "type": "documentation",
101 | "lang": "en",
102 | "boost": 0.0,
103 | "tags": ["webcomponents", "ui5"],
104 | "description": "UI5 Web Components",
105 | "libraryId": "/ui5-webcomponents",
106 | "sourcePath": "ui5-webcomponents/docs",
107 | "baseUrl": "https://sap.github.io/ui5-webcomponents/docs",
108 | "pathPattern": "/{file}",
109 | "anchorStyle": "github"
110 | },
111 | {
112 | "id": "cloud-sdk-js",
113 | "type": "documentation",
114 | "lang": "en",
115 | "boost": 0.05,
116 | "tags": ["sdk", "javascript", "cloud"],
117 | "description": "SAP Cloud SDK for JavaScript",
118 | "libraryId": "/cloud-sdk-js",
119 | "sourcePath": "cloud-sdk/docs-js",
120 | "baseUrl": "https://sap.github.io/cloud-sdk/docs/js",
121 | "pathPattern": "/{file}",
122 | "anchorStyle": "github"
123 | },
124 | {
125 | "id": "cloud-sdk-java",
126 | "type": "documentation",
127 | "lang": "en",
128 | "boost": 0.05,
129 | "tags": ["sdk", "java", "cloud"],
130 | "description": "SAP Cloud SDK for Java",
131 | "libraryId": "/cloud-sdk-java",
132 | "sourcePath": "cloud-sdk/docs-java",
133 | "baseUrl": "https://sap.github.io/cloud-sdk/docs/java",
134 | "pathPattern": "/{file}",
135 | "anchorStyle": "github"
136 | },
137 | {
138 | "id": "cloud-sdk-ai-js",
139 | "type": "documentation",
140 | "lang": "en",
141 | "boost": 0.05,
142 | "tags": ["ai", "sdk", "javascript"],
143 | "description": "SAP Cloud SDK AI for JavaScript",
144 | "libraryId": "/cloud-sdk-ai-js",
145 | "sourcePath": "cloud-sdk-ai/docs-js",
146 | "baseUrl": "https://sap.github.io/ai-sdk/docs/js",
147 | "pathPattern": "/{file}",
148 | "anchorStyle": "github"
149 | },
150 | {
151 | "id": "cloud-sdk-ai-java",
152 | "type": "documentation",
153 | "lang": "en",
154 | "boost": 0.05,
155 | "tags": ["ai", "sdk", "java"],
156 | "description": "SAP Cloud SDK AI for Java",
157 | "libraryId": "/cloud-sdk-ai-java",
158 | "sourcePath": "cloud-sdk-ai/docs-java",
159 | "baseUrl": "https://sap.github.io/ai-sdk/docs/java",
160 | "pathPattern": "/{file}",
161 | "anchorStyle": "github"
162 | },
163 | {
164 | "id": "ui5-typescript",
165 | "type": "documentation",
166 | "lang": "en",
167 | "boost": 0.1,
168 | "tags": ["ui5", "typescript", "types", "frontend"],
169 | "description": "UI5 TypeScript",
170 | "libraryId": "/ui5-typescript",
171 | "sourcePath": "ui5-typescript",
172 | "baseUrl": "https://github.com/UI5/typescript/blob/gh-pages",
173 | "pathPattern": "/{file}",
174 | "anchorStyle": "github"
175 | },
176 | {
177 | "id": "ui5-cc-spreadsheetimporter",
178 | "type": "documentation",
179 | "lang": "en",
180 | "boost": 0.05,
181 | "tags": ["ui5", "spreadsheet", "importer", "custom-control"],
182 | "description": "UI5 CC Spreadsheet Importer",
183 | "libraryId": "/ui5-cc-spreadsheetimporter",
184 | "sourcePath": "ui5-cc-spreadsheetimporter/docs",
185 | "baseUrl": "https://docs.spreadsheet-importer.com",
186 | "pathPattern": "/pages/{file}/",
187 | "anchorStyle": "github"
188 | },
189 | {
190 | "id": "abap-cheat-sheets",
191 | "type": "documentation",
192 | "lang": "en",
193 | "boost": 0.05,
194 | "tags": ["abap", "syntax", "cheat-sheets", "examples", "backend"],
195 | "description": "ABAP Cheat Sheets",
196 | "libraryId": "/abap-cheat-sheets",
197 | "sourcePath": "abap-cheat-sheets",
198 | "baseUrl": "https://github.com/SAP-samples/abap-cheat-sheets/blob/main",
199 | "pathPattern": "/{file}",
200 | "anchorStyle": "github"
201 | },
202 | {
203 | "id": "sap-styleguides",
204 | "type": "documentation",
205 | "lang": "en",
206 | "boost": 0.06,
207 | "tags": ["abap", "clean-code", "style-guide", "best-practices", "code-review"],
208 | "description": "SAP Style Guides",
209 | "libraryId": "/sap-styleguides",
210 | "sourcePath": "sap-styleguides",
211 | "baseUrl": "https://github.com/SAP/styleguides/blob/main",
212 | "pathPattern": "/{file}",
213 | "anchorStyle": "github"
214 | },
215 | {
216 | "id": "dsag-abap-leitfaden",
217 | "type": "documentation",
218 | "lang": "de",
219 | "boost": 0.05,
220 | "tags": ["abap", "leitfaden", "best-practices", "german", "dsag", "clean-core"],
221 | "description": "DSAG ABAP Leitfaden",
222 | "libraryId": "/dsag-abap-leitfaden",
223 | "sourcePath": "dsag-abap-leitfaden/docs",
224 | "baseUrl": "https://1dsag.github.io/ABAP-Leitfaden",
225 | "pathPattern": "/{file}/",
226 | "anchorStyle": "github"
227 | },
228 | {
229 | "id": "abap-fiori-showcase",
230 | "type": "documentation",
231 | "lang": "en",
232 | "boost": 0.08,
233 | "tags": ["fiori-elements", "abap", "rap", "annotations", "odata-v4", "showcase"],
234 | "description": "ABAP Platform Fiori Feature Showcase",
235 | "libraryId": "/abap-fiori-showcase",
236 | "sourcePath": "abap-fiori-showcase",
237 | "baseUrl": "https://github.com/SAP-samples/abap-platform-fiori-feature-showcase/blob/main",
238 | "pathPattern": "/{file}",
239 | "anchorStyle": "github"
240 | },
241 | {
242 | "id": "cap-fiori-showcase",
243 | "type": "documentation",
244 | "lang": "en",
245 | "boost": 0.08,
246 | "tags": ["fiori-elements", "cap", "annotations", "odata-v4", "showcase", "nodejs"],
247 | "description": "CAP Fiori Elements Feature Showcase",
248 | "libraryId": "/cap-fiori-showcase",
249 | "sourcePath": "cap-fiori-showcase",
250 | "baseUrl": "https://github.com/SAP-samples/fiori-elements-feature-showcase/blob/main",
251 | "pathPattern": "/{file}",
252 | "anchorStyle": "github"
253 | },
254 | {
255 | "id": "abap-docs-758",
256 | "type": "documentation",
257 | "lang": "en",
258 | "boost": 0.05,
259 | "tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.58"],
260 | "description": "Official ABAP Keyword Documentation (7.58)",
261 | "libraryId": "/abap-docs-758",
262 | "sourcePath": "abap-docs/docs/7.58/md",
263 | "baseUrl": "https://help.sap.com/doc/abapdocu_758_index_htm/7.58/en-US",
264 | "pathPattern": "/{file}",
265 | "anchorStyle": "sap-help"
266 | },
267 | {
268 | "id": "abap-docs-757",
269 | "type": "documentation",
270 | "lang": "en",
271 | "boost": 0.02,
272 | "tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.57"],
273 | "description": "Official ABAP Keyword Documentation (7.57)",
274 | "libraryId": "/abap-docs-757",
275 | "sourcePath": "abap-docs/docs/7.57/md",
276 | "baseUrl": "https://help.sap.com/doc/abapdocu_757_index_htm/7.57/en-US",
277 | "pathPattern": "/{file}",
278 | "anchorStyle": "sap-help"
279 | },
280 | {
281 | "id": "abap-docs-756",
282 | "type": "documentation",
283 | "lang": "en",
284 | "boost": 0.01,
285 | "tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.56"],
286 | "description": "Official ABAP Keyword Documentation (7.56)",
287 | "libraryId": "/abap-docs-756",
288 | "sourcePath": "abap-docs/docs/7.56/md",
289 | "baseUrl": "https://help.sap.com/doc/abapdocu_756_index_htm/7.56/en-US",
290 | "pathPattern": "/{file}",
291 | "anchorStyle": "sap-help"
292 | },
293 | {
294 | "id": "abap-docs-755",
295 | "type": "documentation",
296 | "lang": "en",
297 | "boost": 0.01,
298 | "tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.55"],
299 | "description": "Official ABAP Keyword Documentation (7.55)",
300 | "libraryId": "/abap-docs-755",
301 | "sourcePath": "abap-docs/docs/7.55/md",
302 | "baseUrl": "https://help.sap.com/doc/abapdocu_755_index_htm/7.55/en-US",
303 | "pathPattern": "/{file}",
304 | "anchorStyle": "sap-help"
305 | },
306 | {
307 | "id": "abap-docs-754",
308 | "type": "documentation",
309 | "lang": "en",
310 | "boost": 0.01,
311 | "tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.54"],
312 | "description": "Official ABAP Keyword Documentation (7.54)",
313 | "libraryId": "/abap-docs-754",
314 | "sourcePath": "abap-docs/docs/7.54/md",
315 | "baseUrl": "https://help.sap.com/doc/abapdocu_754_index_htm/7.54/en-US",
316 | "pathPattern": "/{file}",
317 | "anchorStyle": "sap-help"
318 | },
319 | {
320 | "id": "abap-docs-753",
321 | "type": "documentation",
322 | "lang": "en",
323 | "boost": 0.01,
324 | "tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.53"],
325 | "description": "Official ABAP Keyword Documentation (7.53)",
326 | "libraryId": "/abap-docs-753",
327 | "sourcePath": "abap-docs/docs/7.53/md",
328 | "baseUrl": "https://help.sap.com/doc/abapdocu_753_index_htm/7.53/en-US",
329 | "pathPattern": "/{file}",
330 | "anchorStyle": "sap-help"
331 | },
332 | {
333 | "id": "abap-docs-752",
334 | "type": "documentation",
335 | "lang": "en",
336 | "boost": 0.01,
337 | "tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.52"],
338 | "description": "Official ABAP Keyword Documentation (7.52)",
339 | "libraryId": "/abap-docs-752",
340 | "sourcePath": "abap-docs/docs/7.52/md",
341 | "baseUrl": "https://help.sap.com/doc/abapdocu_752_index_htm/7.52/en-US",
342 | "pathPattern": "/{file}",
343 | "anchorStyle": "sap-help"
344 | },
345 | {
346 | "id": "abap-docs-latest",
347 | "type": "documentation",
348 | "lang": "en",
349 | "boost": 1.0,
350 | "tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "latest"],
351 | "description": "Official ABAP Keyword Documentation (Latest)",
352 | "libraryId": "/abap-docs-latest",
353 | "sourcePath": "abap-docs/docs/latest/md",
354 | "baseUrl": "https://help.sap.com/doc/abapdocu_latest_index_htm/latest/en-US",
355 | "pathPattern": "/{file}",
356 | "anchorStyle": "sap-help"
357 | }
358 | ],
359 |
360 | "synonyms": [
361 | { "from": "ui5", "to": ["sapui5", "openui5"] },
362 | { "from": "button", "to": ["btn", "sap.m.Button"] },
363 | { "from": "table", "to": ["sap.m.Table", "sap.ui.table.Table"] },
364 | { "from": "wizard", "to": ["sap.m.Wizard"] },
365 | { "from": "testing", "to": ["wdi5", "e2e", "automation"] },
366 | { "from": "cds", "to": ["cap", "cloud application programming"] },
367 | { "from": "odata", "to": ["rest", "api", "service"] },
368 | { "from": "typescript", "to": ["ts", "types", "type definitions"] },
369 | { "from": "spreadsheet", "to": ["excel", "csv", "import", "upload"] },
370 | { "from": "abap", "to": ["advanced business application programming", "sap programming"] },
371 | { "from": "clean-code", "to": ["clean code", "best practices", "style guide"] },
372 | { "from": "style-guide", "to": ["styleguide", "coding standards", "best practices"] },
373 | { "from": "leitfaden", "to": ["guidelines", "guide", "best practices"] },
374 | { "from": "clean-core", "to": ["clean core", "extensibility", "cloud development"] },
375 | { "from": "fiori-elements", "to": ["fiori elements", "annotations", "odata"] },
376 | { "from": "rap", "to": ["restful application programming", "abap restful"] },
377 | { "from": "annotations", "to": ["ui5 annotations", "odata annotations", "fiori annotations"] }
378 | ],
379 |
380 | "acronyms": {
381 | "CAP": ["Cloud Application Programming", "cds"],
382 | "CDS": ["Core Data Services", "cap"],
383 | "UI5": ["sapui5", "openui5"],
384 | "BTP": ["Business Technology Platform", "cloud"],
385 | "MTA": ["Multi-Target Application"],
386 | "CQL": ["CDS Query Language", "query"],
387 | "OData": ["Open Data Protocol", "rest", "api"],
388 | "TS": ["TypeScript", "types"],
389 | "ABAP": ["Advanced Business Application Programming", "sap programming"],
390 | "DSAG": ["Deutschsprachige SAP-Anwendergruppe", "german sap user group"],
391 | "RAP": ["RESTful Application Programming", "abap restful", "business object"]
392 | },
393 |
394 | "contextBoosts": {
395 | "SAP Cloud SDK": {
396 | "/cloud-sdk-ai-js": 1.0,
397 | "/cloud-sdk-ai-java": 1.0,
398 | "/cloud-sdk-js": 0.8,
399 | "/cloud-sdk-java": 0.8,
400 | "/cap": 0.2
401 | },
402 | "UI5": {
403 | "/sapui5": 0.9,
404 | "/openui5-api": 0.9,
405 | "/openui5-samples": 0.9,
406 | "/ui5-typescript": 0.8
407 | },
408 | "wdi5": {
409 | "/wdi5": 1.0,
410 | "/openui5-api": 0.4,
411 | "/openui5-samples": 0.4,
412 | "/sapui5": 0.4
413 | },
414 | "UI5 Web Components": {
415 | "/ui5-webcomponents": 1.0
416 | },
417 | "UI5 Tooling": {
418 | "/ui5-tooling": 1.0
419 | },
420 | "Cloud MTA Build Tool": {
421 | "/cloud-mta-build-tool": 1.0
422 | },
423 | "CAP": {
424 | "/cap": 1.0,
425 | "/cap-fiori-showcase": 0.9,
426 | "/sapui5": 0.2
427 | },
428 | "TypeScript": {
429 | "/ui5-typescript": 1.0,
430 | "/sapui5": 0.4,
431 | "/openui5-api": 0.4
432 | },
433 | "UI5 CC Spreadsheet Importer": {
434 | "/ui5-cc-spreadsheetimporter": 1.0,
435 | "/sapui5": 0.3,
436 | "/openui5-api": 0.3
437 | },
438 | "ABAP": {
439 | "/abap-docs-latest": 1.0,
440 | "/abap-cheat-sheets": 0.8,
441 | "/sap-styleguides": 0.7,
442 | "/dsag-abap-leitfaden": 0.6,
443 | "/abap-docs-758": 0.05,
444 | "/abap-docs-757": 0.02,
445 | "/abap-docs-756": 0.01,
446 | "/abap-docs-755": 0.01,
447 | "/abap-docs-754": 0.01,
448 | "/abap-docs-753": 0.01,
449 | "/abap-docs-752": 0.01,
450 | "/cap": 0.2
451 | },
452 | "Clean Code": {
453 | "/sap-styleguides": 1.0,
454 | "/dsag-abap-leitfaden": 0.8,
455 | "/abap-cheat-sheets": 0.4
456 | },
457 | "DSAG": {
458 | "/dsag-abap-leitfaden": 1.0,
459 | "/abap-cheat-sheets": 0.5,
460 | "/sap-styleguides": 0.3
461 | },
462 | "Fiori Elements": {
463 | "/abap-fiori-showcase": 1.0,
464 | "/cap-fiori-showcase": 1.0,
465 | "/sapui5": 0.6,
466 | "/cap": 0.5
467 | },
468 | "RAP": {
469 | "/abap-fiori-showcase": 1.0,
470 | "/abap-cheat-sheets": 0.6,
471 | "/cap": 0.3
472 | },
473 | "7.58": {
474 | "/abap-docs-758": 2.0,
475 | "/abap-docs-latest": 0.3
476 | },
477 | "7.57": {
478 | "/abap-docs-757": 2.0,
479 | "/abap-docs-latest": 0.3
480 | },
481 | "7.56": {
482 | "/abap-docs-756": 2.0,
483 | "/abap-docs-latest": 0.3
484 | },
485 | "7.55": {
486 | "/abap-docs-755": 2.0,
487 | "/abap-docs-latest": 0.3
488 | },
489 | "7.54": {
490 | "/abap-docs-754": 2.0,
491 | "/abap-docs-latest": 0.3
492 | },
493 | "7.53": {
494 | "/abap-docs-753": 2.0,
495 | "/abap-docs-latest": 0.3
496 | },
497 | "7.52": {
498 | "/abap-docs-752": 2.0,
499 | "/abap-docs-latest": 0.3
500 | },
501 | "latest": {
502 | "/abap-docs-latest": 1.5,
503 | "/abap-docs-758": 0.1
504 | }
505 | },
506 |
507 | "libraryMappings": {
508 | "sapui5": "sapui5",
509 | "cap": "cap",
510 | "cloud-sdk-js": "cloud-sdk-js",
511 | "cloud-sdk-ai-js": "cloud-sdk-ai-js",
512 | "openui5-api": "sapui5",
513 | "openui5-samples": "sapui5",
514 | "wdi5": "wdi5",
515 | "ui5-tooling": "ui5-tooling",
516 | "cloud-mta-build-tool": "cloud-mta-build-tool",
517 | "ui5-webcomponents": "ui5-webcomponents",
518 | "cloud-sdk-java": "cloud-sdk-java",
519 | "cloud-sdk-ai-java": "cloud-sdk-ai-java",
520 | "ui5-typescript": "ui5-typescript",
521 | "ui5-cc-spreadsheetimporter": "ui5-cc-spreadsheetimporter",
522 | "abap-cheat-sheets": "abap-cheat-sheets",
523 | "sap-styleguides": "sap-styleguides",
524 | "dsag-abap-leitfaden": "dsag-abap-leitfaden",
525 | "abap-fiori-showcase": "abap-fiori-showcase",
526 | "cap-fiori-showcase": "cap-fiori-showcase",
527 | "abap-docs-758": "abap-docs",
528 | "abap-docs-757": "abap-docs",
529 | "abap-docs-756": "abap-docs",
530 | "abap-docs-755": "abap-docs",
531 | "abap-docs-754": "abap-docs",
532 | "abap-docs-753": "abap-docs",
533 | "abap-docs-752": "abap-docs",
534 | "abap-docs-latest": "abap-docs"
535 | },
536 |
537 | "contextEmojis": {
538 | "CAP": "🏗️",
539 | "wdi5": "🧪",
540 | "UI5": "🎨",
541 | "UI5 Web Components": "🕹️",
542 | "SAP Cloud SDK": "🌐",
543 | "UI5 Tooling": "🔧",
544 | "Cloud MTA Build Tool": "🚢",
545 | "TypeScript": "📝",
546 | "UI5 CC Spreadsheet Importer": "📊",
547 | "ABAP": "💻",
548 | "Clean Code": "✨",
549 | "DSAG": "🇩🇪",
550 | "Fiori Elements": "📱",
551 | "RAP": "⚡",
552 | "MIXED": "🔀"
553 | }
554 | }
555 |
```
--------------------------------------------------------------------------------
/test/comprehensive-url-generation.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Comprehensive URL Generation Test Suite
3 | *
4 | * This test suite validates the URL generation system for SAP documentation sources.
5 | * It tests both the main generateDocumentationUrl function and individual generator classes
6 | * for 10+ different documentation sources including CAP, Cloud SDK, UI5, wdi5, etc.
7 | *
8 | * Key Features:
9 | * - Reads from real source files when available (automatic path mapping)
10 | * - Falls back to test data when source files don't exist
11 | * - Uses real configuration from metadata.json (no hardcoded configs)
12 | * - Comprehensive coverage of all URL generation patterns
13 | * - Debug mode available with DEBUG_TESTS=true environment variable
14 | *
15 | * Running Tests:
16 | * - npm run test:url-generation # Run URL generation tests
17 | * - npm run test:url-generation:debug # Run with debug output
18 | * - DEBUG_TESTS=true npx vitest run test/comprehensive-url-generation.test.ts
19 | *
20 | * Architecture:
21 | * The system uses an abstract BaseUrlGenerator class with source-specific implementations
22 | * for different documentation platforms. Each generator handles its own URL patterns,
23 | * frontmatter parsing, and path transformations.
24 | */
25 |
26 | import { describe, it, expect } from 'vitest';
27 | import {
28 | generateDocumentationUrl,
29 | CloudSdkUrlGenerator,
30 | SapUi5UrlGenerator,
31 | CapUrlGenerator,
32 | Wdi5UrlGenerator,
33 | DsagUrlGenerator,
34 | GenericUrlGenerator
35 | } from '../src/lib/url-generation/index.js';
36 | import { AbapUrlGenerator, generateAbapUrl } from '../src/lib/url-generation/abap.js';
37 | import { DocUrlConfig, getDocUrlConfig } from '../src/lib/metadata.js';
38 |
39 | describe('Comprehensive URL Generation System', () => {
40 |
41 | /**
42 | * Retrieves URL configuration from metadata.json for a given library
43 | * @param libraryId - The library identifier (e.g., '/cloud-sdk-js')
44 | * @returns Configuration object with baseUrl, pathPattern, and anchorStyle
45 | * @throws Error if no configuration is found
46 | */
47 | function getConfigForLibrary(libraryId: string): DocUrlConfig {
48 | const config = getDocUrlConfig(libraryId);
49 | if (!config) {
50 | throw new Error(`No configuration found for library: ${libraryId}`);
51 | }
52 | return config;
53 | }
54 |
55 | /**
56 | * Maps libraryId + relFile to actual source file path in the filesystem
57 | * Handles different repository structures and path transformations
58 | * @param libraryId - The library identifier
59 | * @param relFile - The relative file path within the library
60 | * @returns Full path to the actual source file
61 | * @throws Error if no path mapping exists for the library
62 | */
63 | function getSourceFilePath(libraryId: string, relFile: string): string {
64 | const pathMappings: Record<string, { basePath: string; transform?: (relFile: string) => string }> = {
65 | '/cap': { basePath: 'sources/cap-docs' },
66 | '/cloud-mta-build-tool': { basePath: 'sources/cloud-mta-build-tool' },
67 | '/cloud-sdk-js': { basePath: 'sources/cloud-sdk/docs-js' },
68 | '/cloud-sdk-ai-js': { basePath: 'sources/cloud-sdk-ai/docs-js' },
69 | '/openui5-api': {
70 | basePath: 'sources/openui5',
71 | transform: (relFile) => {
72 | // Transform src/sap/m/Button.js → src/sap.m/src/sap/m/Button.js
73 | const match = relFile.match(/^src\/sap\/([^\/]+)\/(.+)$/);
74 | if (match) {
75 | const [, module, file] = match;
76 | return `src/sap.${module}/src/sap/${module}/${file}`;
77 | }
78 | return relFile;
79 | }
80 | },
81 | '/openui5-samples': { basePath: 'sources/openui5' },
82 | '/sapui5': { basePath: 'sources/sapui5-docs/docs' },
83 | '/ui5-tooling': { basePath: 'sources/ui5-tooling/docs' },
84 | '/ui5-webcomponents': { basePath: 'sources/ui5-webcomponents/docs' },
85 | '/wdi5': { basePath: 'sources/wdi5/docs' },
86 | '/ui5-typescript': { basePath: 'sources/ui5-typescript' },
87 | '/ui5-cc-spreadsheetimporter': { basePath: 'sources/ui5-cc-spreadsheetimporter/docs' },
88 | '/abap-cheat-sheets': { basePath: 'sources/abap-cheat-sheets' },
89 | '/sap-styleguides': { basePath: 'sources/sap-styleguides' },
90 | '/dsag-abap-leitfaden': { basePath: 'sources/dsag-abap-leitfaden/docs' },
91 | '/abap-fiori-showcase': { basePath: 'sources/abap-fiori-showcase' },
92 | '/cap-fiori-showcase': { basePath: 'sources/cap-fiori-showcase' }
93 | };
94 |
95 | const mapping = pathMappings[libraryId];
96 | if (!mapping) {
97 | throw new Error(`No source path mapping found for library: ${libraryId}`);
98 | }
99 |
100 | const transformedRelFile = mapping.transform ? mapping.transform(relFile) : relFile;
101 | return `${mapping.basePath}/${transformedRelFile}`;
102 | }
103 |
104 | /**
105 | * Reads file content from actual source files with graceful fallback
106 | * @param libraryId - The library identifier (e.g., '/cloud-sdk-js')
107 | * @param relFile - The relative file path within the library
108 | * @returns File content as string, or null if file doesn't exist
109 | */
110 | function readFileContent(libraryId: string, relFile: string): string | null {
111 | const fs = require('fs');
112 | const path = require('path');
113 |
114 | try {
115 | const sourceFilePath = getSourceFilePath(libraryId, relFile);
116 | const fullPath = path.resolve(sourceFilePath);
117 | return fs.readFileSync(fullPath, 'utf8');
118 | } catch (error: any) {
119 | console.warn(`Could not read file for ${libraryId}/${relFile}:`, error.message);
120 | // Return null to trigger fallback to test data
121 | return null;
122 | }
123 | }
124 |
125 | /**
126 | * Test cases for comprehensive URL generation testing
127 | *
128 | * Each test case defines:
129 | * - name: Human-readable test description
130 | * - libraryId: Library identifier from metadata.json
131 | * - relFile: Relative file path within the library (used for path mapping)
132 | * - expectedUrl: Expected generated URL for validation
133 | * - frontmatter: Fallback YAML frontmatter (used when real file not found)
134 | * - content: Fallback content (used when real file not found)
135 | *
136 | * The system will attempt to read real source files first, falling back to
137 | * the provided frontmatter/content if the file doesn't exist.
138 | */
139 | const testCases = [
140 | {
141 | name: 'CAP - CDS Log Documentation',
142 | libraryId: '/cap',
143 | relFile: 'node.js/cds-log.md',
144 | expectedUrl: 'https://cap.cloud.sap/docs/#/node.js/cds-log',
145 | frontmatter: '---\nid: cds-log\ntitle: Logging\n---\n',
146 | content: '# Logging\n\nCAP provides structured logging capabilities...'
147 | },
148 | {
149 | name: 'Cloud MTA Build Tool - Download Page',
150 | libraryId: '/cloud-mta-build-tool',
151 | relFile: 'docs/download.md',
152 | expectedUrl: 'https://sap.github.io/cloud-mta-build-tool/download',
153 | frontmatter: '',
154 | content: '\nYou can install the Cloud MTA Build Tool...'
155 | },
156 | {
157 | name: 'Cloud SDK JS - Kubernetes Migration',
158 | libraryId: '/cloud-sdk-js',
159 | relFile: 'environments/migrate-sdk-application-from-btp-cf-to-kubernetes.mdx',
160 | expectedUrl: 'https://sap.github.io/cloud-sdk/docs/js/environments/kubernetes',
161 | frontmatter: '---\nid: kubernetes\ntitle: Migrate your App from SAP BTP CF to Kubernetes\n---\n',
162 | content: '# Migrate a Cloud Foundry Application to a Kubernetes Cluster\n\nThis guide details...'
163 | },
164 | {
165 | name: 'Cloud SDK AI JS - Orchestration',
166 | libraryId: '/cloud-sdk-ai-js',
167 | relFile: 'langchain/orchestration.mdx',
168 | expectedUrl: 'https://sap.github.io/ai-sdk/docs/js/langchain/orchestration',
169 | frontmatter: '---\nid: orchestration\ntitle: Orchestration Integration\n---\n',
170 | content: '# Orchestration Integration\n\nThe @sap-ai-sdk/langchain packages provides...'
171 | },
172 | {
173 | name: 'OpenUI5 API - Button Control',
174 | libraryId: '/openui5-api',
175 | relFile: 'src/sap/m/Button.js',
176 | expectedUrl: 'https://sdk.openui5.org/#/api/sap.m.Button',
177 | frontmatter: '',
178 | content: 'sap.ui.define([\n "./library",\n "sap/ui/core/Control",\n // Button control implementation'
179 | },
180 | {
181 | name: 'OpenUI5 Samples - ButtonWithBadge',
182 | libraryId: '/openui5-samples',
183 | relFile: 'src/sap.m/test/sap/m/demokit/sample/ButtonWithBadge/Component.js',
184 | expectedUrl: 'https://sdk.openui5.org/entity/sap.m.Button/sample/sap.m.sample.ButtonWithBadge',
185 | frontmatter: '',
186 | content: 'sap.ui.define([\n "sap/ui/core/UIComponent"\n], function (UIComponent) {\n // Sample implementation'
187 | },
188 | {
189 | name: 'SAPUI5 - Multi-Selection Navigation',
190 | libraryId: '/sapui5',
191 | relFile: '06_SAP_Fiori_Elements/multi-selection-for-intent-based-navigation-640cabf.md',
192 | expectedUrl: 'https://ui5.sap.com/#/topic/640cabfd35c3469aacf31be28924d50d',
193 | frontmatter: '---\nid: 640cabfd35c3469aacf31be28924d50d\ntopic: 640cabfd35c3469aacf31be28924d50d\ntitle: Multi-Selection for Intent-Based Navigation\n---\n',
194 | content: '# Multi-Selection for Intent-Based Navigation\n\nThis feature allows...'
195 | },
196 | {
197 | name: 'UI5 Tooling - Builder Documentation',
198 | libraryId: '/ui5-tooling',
199 | relFile: 'pages/Builder.md',
200 | expectedUrl: 'https://sap.github.io/ui5-tooling/v4/pages/Builder#ui5-builder',
201 | frontmatter: '',
202 | content: '# UI5 Builder\n\nThe UI5 Builder module takes care of building your project...'
203 | },
204 | {
205 | name: 'UI5 Web Components - Configuration',
206 | libraryId: '/ui5-webcomponents',
207 | relFile: '2-advanced/01-configuration.md',
208 | expectedUrl: 'https://sap.github.io/ui5-webcomponents/docs/01-configuration#configuration',
209 | frontmatter: '',
210 | content: '# Configuration\n\nThis section explains how you can configure UI5 Web Components...'
211 | },
212 | {
213 | name: 'wdi5 - Locators Documentation',
214 | libraryId: '/wdi5',
215 | relFile: 'locators.md',
216 | expectedUrl: 'https://ui5-community.github.io/wdi5/#/locators',
217 | frontmatter: '---\nid: locators\ntitle: Locators\n---\n',
218 | content: '# Locators\n\nwdi5 provides various locators for UI5 controls...'
219 | },
220 | {
221 | name: 'UI5 TypeScript - FAQ Documentation',
222 | libraryId: '/ui5-typescript',
223 | relFile: 'faq.md',
224 | expectedUrl: 'https://github.com/UI5/typescript/blob/gh-pages/faq#faq---frequently-asked-questions-for-the-ui5-type-definitions',
225 | frontmatter: '',
226 | content: '# FAQ - Frequently Asked Questions for the UI5 Type Definitions\n\nWhile the [main page](README.md) answers the high-level questions...'
227 | },
228 | {
229 | name: 'UI5 CC Spreadsheet Importer - Checks Documentation',
230 | libraryId: '/ui5-cc-spreadsheetimporter',
231 | relFile: 'pages/Checks.md',
232 | expectedUrl: 'https://docs.spreadsheet-importer.com/pages/Checks/#error-types',
233 | frontmatter: '',
234 | content: '## Error Types\n\nThe following types of errors are handled by the UI5 Spreadsheet Upload Control...'
235 | },
236 | {
237 | name: 'ABAP Cheat Sheets - Internal Tables',
238 | libraryId: '/abap-cheat-sheets',
239 | relFile: '01_Internal_Tables.md',
240 | expectedUrl: 'https://github.com/SAP-samples/abap-cheat-sheets/blob/main/01_Internal_Tables#internal-tables',
241 | frontmatter: '',
242 | content: '# Internal Tables\n\nThis cheat sheet contains a selection of syntax examples and notes on internal tables...'
243 | },
244 | {
245 | name: 'SAP Style Guides - Clean ABAP',
246 | libraryId: '/sap-styleguides',
247 | relFile: 'clean-abap/CleanABAP.md',
248 | expectedUrl: 'https://github.com/SAP/styleguides/blob/main/CleanABAP#clean-abap',
249 | frontmatter: '',
250 | content: '# Clean ABAP\n\n> [**中文**](CleanABAP_zh.md)\n\nThis style guide presents the essentials of clean ABAP...'
251 | },
252 | {
253 | name: 'DSAG ABAP Leitfaden - Clean Core',
254 | libraryId: '/dsag-abap-leitfaden',
255 | relFile: 'clean-core/what-is-clean-core.md',
256 | expectedUrl: 'https://1dsag.github.io/ABAP-Leitfaden/clean-core/what-is-clean-core/#was-ist-clean-core',
257 | frontmatter: '',
258 | content: '# Was ist Clean Core?\n\nClean Core ist ein Konzept von SAP, das darauf abzielt...'
259 | },
260 | {
261 | name: 'ABAP Platform Fiori Feature Showcase - General Features',
262 | libraryId: '/abap-fiori-showcase',
263 | relFile: '01_general_features.md',
264 | expectedUrl: 'https://github.com/SAP-samples/abap-platform-fiori-feature-showcase/blob/main/01_general_features#general-features',
265 | frontmatter: '',
266 | content: '# General Features\n\nThis section describes the features that are generally used throughout...'
267 | },
268 | {
269 | name: 'CAP Fiori Elements Feature Showcase - README',
270 | libraryId: '/cap-fiori-showcase',
271 | relFile: 'README.md',
272 | expectedUrl: 'https://github.com/SAP-samples/fiori-elements-feature-showcase/blob/main/README#sap-fiori-elements-for-odata-v4-feature-showcase',
273 | frontmatter: '',
274 | content: '# SAP Fiori Elements for OData V4 Feature Showcase\n\nThis app showcases different features of SAP Fiori elements...'
275 | }
276 | // Note: Some sources like CAP, Cloud SDK AI, wdi5, etc. may need different file mappings
277 | // or fallback to mock content if actual files don't exist in expected locations
278 | ];
279 |
280 | describe('Main URL Generation Function', () => {
281 | testCases.forEach(({ name, libraryId, relFile, expectedUrl, frontmatter, content }) => {
282 | it(`should generate correct URL for ${name}`, () => {
283 | // Step 1: Get configuration from metadata.json
284 | const config = getConfigForLibrary(libraryId);
285 |
286 | // Step 2: Try to read from actual source file first, fallback to test data
287 | let fileContent = readFileContent(libraryId, relFile);
288 | let contentSource = 'real file';
289 |
290 | if (!fileContent) {
291 | // Fallback to hardcoded test data when real file is not available
292 | fileContent = frontmatter ? `${frontmatter}\n${content}` : content;
293 | contentSource = 'test data';
294 | }
295 |
296 | // For debugging: log which content source was used
297 | if (process.env.DEBUG_TESTS === 'true') {
298 | console.log(`\n[${name}] Using ${contentSource}`);
299 | console.log(`File path: ${libraryId}/${relFile}`);
300 | console.log(`Content preview: ${fileContent.slice(0, 100)}...`);
301 | }
302 |
303 | // Step 3: Generate URL using the URL generation system
304 | const result = generateDocumentationUrl(libraryId, relFile, fileContent, config);
305 |
306 | // Step 4: Validate the result
307 | expect(result).toBe(expectedUrl);
308 | });
309 | });
310 | });
311 |
312 | describe('Individual Generator Classes', () => {
313 |
314 | describe('CloudSdkUrlGenerator', () => {
315 | it('should generate URLs using frontmatter ID', () => {
316 | const config = getConfigForLibrary('/cloud-sdk-js');
317 | const generator = new CloudSdkUrlGenerator('/cloud-sdk-js', config);
318 | const content = '---\nid: kubernetes\n---\n# Migration Guide';
319 |
320 | const result = generator.generateUrl({
321 | libraryId: '/cloud-sdk-js',
322 | relFile: 'environments/migrate.mdx',
323 | content,
324 | config
325 | });
326 |
327 | expect(result).toBe('https://sap.github.io/cloud-sdk/docs/js/environments/kubernetes');
328 | });
329 |
330 | it('should handle AI SDK variants differently', () => {
331 | const config = getConfigForLibrary('/cloud-sdk-ai-js');
332 | const generator = new CloudSdkUrlGenerator('/cloud-sdk-ai-js', config);
333 | const content = '---\nid: orchestration\n---\n# Orchestration';
334 |
335 | const result = generator.generateUrl({
336 | libraryId: '/cloud-sdk-ai-js',
337 | relFile: 'langchain/orchestration.mdx',
338 | content,
339 | config
340 | });
341 |
342 | expect(result).toBe('https://sap.github.io/ai-sdk/docs/js/langchain/orchestration');
343 | });
344 | });
345 |
346 | describe('SapUi5UrlGenerator', () => {
347 | it('should generate topic-based URLs for SAPUI5', () => {
348 | const config = getConfigForLibrary('/sapui5');
349 | const generator = new SapUi5UrlGenerator('/sapui5', config);
350 | const content = '---\nid: 123e4567-e89b-12d3-a456-426614174000\n---\n# Topic Content';
351 |
352 | const result = generator.generateUrl({
353 | libraryId: '/sapui5',
354 | relFile: 'docs/topic.md',
355 | content,
356 | config
357 | });
358 |
359 | expect(result).toBe('https://ui5.sap.com/#/topic/123e4567-e89b-12d3-a456-426614174000');
360 | });
361 |
362 | it('should generate API URLs for OpenUI5 controls', () => {
363 | const config = getConfigForLibrary('/openui5-api');
364 | const generator = new SapUi5UrlGenerator('/openui5-api', config);
365 | const content = 'sap.ui.define([\n "sap/m/Button"\n], function(Button) {';
366 |
367 | const result = generator.generateUrl({
368 | libraryId: '/openui5-api',
369 | relFile: 'src/sap/m/Button.js',
370 | content,
371 | config
372 | });
373 |
374 | expect(result).toBe('https://sdk.openui5.org/#/api/sap.m.Button');
375 | });
376 | });
377 |
378 | describe('CapUrlGenerator', () => {
379 | it('should generate docsify-style URLs', () => {
380 | const config = getConfigForLibrary('/cap');
381 | const generator = new CapUrlGenerator('/cap', config);
382 | const content = '---\nid: getting-started\n---\n# Getting Started';
383 |
384 | const result = generator.generateUrl({
385 | libraryId: '/cap',
386 | relFile: 'guides/getting-started.md',
387 | content,
388 | config
389 | });
390 |
391 | expect(result).toBe('https://cap.cloud.sap/docs/#/guides/getting-started');
392 | });
393 |
394 | it('should handle CDS-specific sections', () => {
395 | const config = getConfigForLibrary('/cap');
396 | const generator = new CapUrlGenerator('/cap', config);
397 | const content = '---\nslug: cds-types\n---\n# CDS Types';
398 |
399 | const result = generator.generateUrl({
400 | libraryId: '/cap',
401 | relFile: 'cds/types.md',
402 | content,
403 | config
404 | });
405 |
406 | expect(result).toBe('https://cap.cloud.sap/docs/#/cds/cds-types');
407 | });
408 | });
409 |
410 | describe('Wdi5UrlGenerator', () => {
411 | it('should generate docsify-style URLs for wdi5', () => {
412 | const config = getConfigForLibrary('/wdi5');
413 | const generator = new Wdi5UrlGenerator('/wdi5', config);
414 | const content = '---\nid: locators\n---\n# Locators';
415 |
416 | const result = generator.generateUrl({
417 | libraryId: '/wdi5',
418 | relFile: 'locators.md',
419 | content,
420 | config
421 | });
422 |
423 | expect(result).toBe('https://ui5-community.github.io/wdi5/#/locators');
424 | });
425 |
426 | it('should handle configuration-specific sections', () => {
427 | const config = getConfigForLibrary('/wdi5');
428 | const generator = new Wdi5UrlGenerator('/wdi5', config);
429 | const content = '---\nid: basic-config\n---\n# Basic Configuration';
430 |
431 | const result = generator.generateUrl({
432 | libraryId: '/wdi5',
433 | relFile: 'configuration/basic.md',
434 | content,
435 | config
436 | });
437 |
438 | expect(result).toBe('https://ui5-community.github.io/wdi5/#/configuration/basic-config');
439 | });
440 | });
441 |
442 | describe('DsagUrlGenerator', () => {
443 | it('should generate GitHub Pages URLs with path transformation', () => {
444 | const config = getConfigForLibrary('/dsag-abap-leitfaden');
445 | const generator = new DsagUrlGenerator('/dsag-abap-leitfaden', config);
446 | const content = '# Was ist Clean Core?\n\nClean Core ist ein Konzept von SAP...';
447 |
448 | const result = generator.generateUrl({
449 | libraryId: '/dsag-abap-leitfaden',
450 | relFile: 'clean-core/what-is-clean-core.md',
451 | content,
452 | config
453 | });
454 |
455 | expect(result).toBe('https://1dsag.github.io/ABAP-Leitfaden/clean-core/what-is-clean-core/#was-ist-clean-core');
456 | });
457 |
458 | it('should handle root-level documentation', () => {
459 | const config = getConfigForLibrary('/dsag-abap-leitfaden');
460 | const generator = new DsagUrlGenerator('/dsag-abap-leitfaden', config);
461 | const content = '# ABAP Leitfaden\n\nDer DSAG ABAP Leitfaden...';
462 |
463 | const result = generator.generateUrl({
464 | libraryId: '/dsag-abap-leitfaden',
465 | relFile: 'README.md',
466 | content,
467 | config
468 | });
469 |
470 | expect(result).toBe('https://1dsag.github.io/ABAP-Leitfaden/README/#abap-leitfaden');
471 | });
472 | });
473 |
474 | describe('GenericUrlGenerator', () => {
475 | it('should handle generic sources with frontmatter', () => {
476 | const config = getConfigForLibrary('/ui5-tooling'); // Use a real generic source
477 | const generator = new GenericUrlGenerator('/ui5-tooling', config);
478 | const content = '---\nid: test-doc\n---\n# Test Document';
479 |
480 | const result = generator.generateUrl({
481 | libraryId: '/ui5-tooling',
482 | relFile: 'pages/test.md',
483 | content,
484 | config
485 | });
486 |
487 | expect(result).toBe('https://sap.github.io/ui5-tooling/v4/pages/test-doc#test-document');
488 | });
489 |
490 | it('should fallback to filename when no frontmatter', () => {
491 | const config = getConfigForLibrary('/ui5-tooling'); // Use a real generic source
492 | const generator = new GenericUrlGenerator('/ui5-tooling', config);
493 | const content = '# Test Document\n\nSome content...';
494 |
495 | const result = generator.generateUrl({
496 | libraryId: '/ui5-tooling',
497 | relFile: 'pages/test.md',
498 | content,
499 | config
500 | });
501 |
502 | expect(result).toBe('https://sap.github.io/ui5-tooling/v4/pages/test#test-document');
503 | });
504 | });
505 |
506 | describe('AbapUrlGenerator', () => {
507 | it('should generate correct cloud URLs for latest version', () => {
508 | // Mock config for ABAP documentation
509 | const config: DocUrlConfig = {
510 | baseUrl: 'https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US',
511 | pathPattern: '/latest/en-US/{filename}',
512 | anchorStyle: 'lowercase-with-dashes'
513 | };
514 |
515 | const generator = new AbapUrlGenerator('/abap-docs-latest', config);
516 |
517 | const result = generator.generateSourceSpecificUrl({
518 | libraryId: '/abap-docs-latest',
519 | relFile: 'abeninline_declarations.md',
520 | content: '# Inline Declarations',
521 | config
522 | });
523 |
524 | expect(result).toBe('https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/abeninline_declarations.html');
525 | });
526 |
527 | it('should generate correct cloud URLs for version 9.16', () => {
528 | const config: DocUrlConfig = {
529 | baseUrl: 'https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US',
530 | pathPattern: '/9.16/en-US/{filename}',
531 | anchorStyle: 'lowercase-with-dashes'
532 | };
533 |
534 | const generator = new AbapUrlGenerator('/abap-docs-916', config);
535 |
536 | const result = generator.generateSourceSpecificUrl({
537 | libraryId: '/abap-docs-916',
538 | relFile: 'md/abapselect.md',
539 | content: '# SELECT Statement',
540 | config
541 | });
542 |
543 | expect(result).toBe('https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/abapselect.html');
544 | });
545 |
546 | it('should generate correct cloud URLs for S/4HANA 2025 version 8.10', () => {
547 | const config: DocUrlConfig = {
548 | baseUrl: 'https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US',
549 | pathPattern: '/8.10/en-US/{filename}',
550 | anchorStyle: 'lowercase-with-dashes'
551 | };
552 |
553 | const generator = new AbapUrlGenerator('/abap-docs-810', config);
554 |
555 | const result = generator.generateSourceSpecificUrl({
556 | libraryId: '/abap-docs-810',
557 | relFile: 'abaploop.md',
558 | content: '# LOOP Statement',
559 | config
560 | });
561 |
562 | expect(result).toBe('https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/abaploop.html');
563 | });
564 |
565 | it('should generate correct legacy URLs for version 7.58', () => {
566 | const config: DocUrlConfig = {
567 | baseUrl: 'https://help.sap.com/doc/abapdocu_758_index_htm/7.58/en-US',
568 | pathPattern: '/7.58/en-US/{filename}',
569 | anchorStyle: 'lowercase-with-dashes'
570 | };
571 |
572 | const generator = new AbapUrlGenerator('/abap-docs-758', config);
573 |
574 | const result = generator.generateSourceSpecificUrl({
575 | libraryId: '/abap-docs-758',
576 | relFile: 'md/abapdata.md',
577 | content: '# DATA Statement',
578 | config
579 | });
580 |
581 | expect(result).toBe('https://help.sap.com/doc/abapdocu_758_index_htm/7.58/en-US/abapdata.html');
582 | });
583 |
584 | it('should handle anchors correctly', () => {
585 | const config: DocUrlConfig = {
586 | baseUrl: 'https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US',
587 | pathPattern: '/latest/en-US/{filename}',
588 | anchorStyle: 'lowercase-with-dashes'
589 | };
590 |
591 | const generator = new AbapUrlGenerator('/abap-docs-latest', config);
592 |
593 | const result = generator.generateSourceSpecificUrl({
594 | libraryId: '/abap-docs-latest',
595 | relFile: 'abeninline_declarations.md',
596 | content: '# Inline Declarations',
597 | config,
598 | anchor: 'syntax'
599 | });
600 |
601 | expect(result).toBe('https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/abeninline_declarations.html#syntax');
602 | });
603 |
604 | it('should correctly extract version from library ID', () => {
605 | const testCases = [
606 | { libraryId: '/abap-docs-758', expected: '7.58' },
607 | { libraryId: '/abap-docs-latest', expected: 'latest' },
608 | { libraryId: '/abap-docs-916', expected: '9.16' },
609 | { libraryId: '/abap-docs-810', expected: '8.10' }
610 | ];
611 |
612 | testCases.forEach(({ libraryId, expected }) => {
613 | const config: DocUrlConfig = {
614 | baseUrl: 'https://example.com',
615 | pathPattern: `/${expected}/en-US/{filename}`,
616 | anchorStyle: 'lowercase-with-dashes'
617 | };
618 |
619 | const generator = new AbapUrlGenerator(libraryId, config);
620 |
621 | // Test the version extraction by checking the generated URL
622 | const result = generator.generateSourceSpecificUrl({
623 | libraryId,
624 | relFile: 'test.md',
625 | content: '# Test',
626 | config
627 | });
628 |
629 | if (expected === 'latest' || parseFloat(expected) >= 9.1 || parseFloat(expected) >= 8.1) {
630 | expect(result).toContain('abapdocu_cp_index_htm/CLOUD');
631 | } else {
632 | const versionCode = expected.replace('.', '');
633 | expect(result).toContain(`abapdocu_${versionCode}_index_htm/${expected}`);
634 | }
635 | });
636 | });
637 |
638 | it('should use .html extension instead of .htm for file extension', () => {
639 | const config: DocUrlConfig = {
640 | baseUrl: 'https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US',
641 | pathPattern: '/latest/en-US/{filename}',
642 | anchorStyle: 'lowercase-with-dashes'
643 | };
644 |
645 | const generator = new AbapUrlGenerator('/abap-docs-latest', config);
646 |
647 | const result = generator.generateSourceSpecificUrl({
648 | libraryId: '/abap-docs-latest',
649 | relFile: 'abeninline_declarations.md',
650 | content: '# Inline Declarations',
651 | config
652 | });
653 |
654 | // Should use .html file extension (not .htm file extension)
655 | expect(result).toContain('abeninline_declarations.html');
656 | expect(result).toMatch(/\.html$/);
657 | expect(result).not.toMatch(/\.htm$/);
658 | });
659 |
660 | it('should point to latest cloud version instead of legacy 7.58 version', () => {
661 | const config: DocUrlConfig = {
662 | baseUrl: 'https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US',
663 | pathPattern: '/latest/en-US/{filename}',
664 | anchorStyle: 'lowercase-with-dashes'
665 | };
666 |
667 | const generator = new AbapUrlGenerator('/abap-docs-latest', config);
668 |
669 | const result = generator.generateSourceSpecificUrl({
670 | libraryId: '/abap-docs-latest',
671 | relFile: 'abeninline_declarations.md',
672 | content: '# Inline Declarations',
673 | config
674 | });
675 |
676 | // Should use the new cloud URL pattern instead of the old 7.58 pattern
677 | expect(result).toContain('abapdocu_cp_index_htm/CLOUD');
678 | expect(result).not.toContain('abapdocu_758_index_htm/7.58');
679 | expect(result).not.toContain('abapdocu_latest_index_htm/latest');
680 |
681 | // The full URL should match the expected cloud pattern
682 | expect(result).toBe('https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/abeninline_declarations.html');
683 | });
684 | });
685 | });
686 |
687 | describe('Error Handling', () => {
688 | it('should return null for missing config', () => {
689 | const result = generateDocumentationUrl('/unknown', 'file.md', 'content', null as any);
690 | expect(result).toBeNull();
691 | });
692 |
693 | it('should handle malformed frontmatter gracefully', () => {
694 | // Test with a non-existent library ID that will use the generic generator
695 | const config = getConfigForLibrary('/ui5-tooling'); // Use a real config for fallback testing
696 |
697 | const content = '---\ninvalid: yaml: content:\n---\n# Content';
698 | const result = generateDocumentationUrl('/ui5-tooling', 'test.md', content, config);
699 |
700 | expect(result).not.toBeNull();
701 | });
702 | });
703 |
704 | describe('URL Pattern Validation', () => {
705 | testCases.forEach(({ name, expectedUrl }) => {
706 | it(`should generate valid URL format for ${name}`, () => {
707 | expect(expectedUrl).toMatch(/^https?:\/\//);
708 | expect(() => new URL(expectedUrl)).not.toThrow();
709 | });
710 | });
711 | });
712 | });
713 |
```
--------------------------------------------------------------------------------
/src/lib/BaseServerHandler.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Base Server Handler - Shared functionality for MCP servers
3 | * Eliminates code duplication between stdio and HTTP server implementations
4 | *
5 | * IMPORTANT FOR LLMs/AI ASSISTANTS:
6 | * =================================
7 | * The function names in this MCP server may appear with different prefixes depending on your MCP client:
8 | * - Simple names: search, fetch, sap_community_search, sap_help_search, sap_help_get
9 | * - Prefixed names: mcp_sap-docs-remote_search, mcp_sap-docs-remote_fetch, etc.
10 | *
11 | * Try the simple names first, then the prefixed versions if they don't work.
12 | *
13 | * Note: sap_docs_search and sap_docs_get are legacy aliases for backward compatibility.
14 | */
15 |
16 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
17 | import {
18 | ListResourcesRequestSchema,
19 | ReadResourceRequestSchema,
20 | CallToolRequestSchema,
21 | ListToolsRequestSchema,
22 | ListPromptsRequestSchema,
23 | GetPromptRequestSchema
24 | } from "@modelcontextprotocol/sdk/types.js";
25 | import {
26 | searchLibraries,
27 | fetchLibraryDocumentation,
28 | listDocumentationResources,
29 | readDocumentationResource,
30 | searchCommunity
31 | } from "./localDocs.js";
32 | import { searchSapHelp, getSapHelpContent } from "./sapHelp.js";
33 |
34 | import { SearchResponse } from "./types.js";
35 | import { logger } from "./logger.js";
36 | import { search } from "./search.js";
37 | import { CONFIG } from "./config.js";
38 | import { loadMetadata, getDocUrlConfig } from "./metadata.js";
39 | import { generateDocumentationUrl, formatSearchResult } from "./url-generation/index.js";
40 |
41 | /**
42 | * Helper functions for creating structured JSON responses compatible with ChatGPT and all MCP clients
43 | */
44 |
45 | interface SearchResult {
46 | id: string;
47 | title: string;
48 | url: string;
49 | snippet?: string;
50 | score?: number;
51 | metadata?: Record<string, any>;
52 | }
53 |
54 | interface DocumentResult {
55 | id: string;
56 | title: string;
57 | text: string;
58 | url: string;
59 | metadata?: Record<string, any>;
60 | }
61 |
62 | /**
63 | * Create structured JSON response for search results (ChatGPT-compatible)
64 | */
65 | function createSearchResponse(results: SearchResult[]): any {
66 | // Clean the results to avoid JSON serialization issues in MCP protocol
67 | const cleanedResults = results.map(result => ({
68 | // ChatGPT requires: id, title, url (other fields optional)
69 | id: result.id,
70 | title: result.title ? result.title.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n') : result.title,
71 | url: result.url,
72 | // Additional fields for enhanced functionality
73 | snippet: result.snippet ? result.snippet.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n') : result.snippet,
74 | score: result.score,
75 | metadata: result.metadata
76 | }));
77 |
78 | // ChatGPT expects: { "results": [...] } in JSON-encoded text content
79 | return {
80 | content: [
81 | {
82 | type: "text",
83 | text: JSON.stringify({ results: cleanedResults })
84 | }
85 | ]
86 | };
87 | }
88 |
89 | /**
90 | * Create structured JSON response for document fetch (ChatGPT-compatible)
91 | */
92 | function createDocumentResponse(document: DocumentResult): any {
93 | // Clean the text content to avoid JSON serialization issues in MCP protocol
94 | const cleanedDocument = {
95 | // ChatGPT requires: id, title, text, url, metadata
96 | id: document.id,
97 | title: document.title,
98 | text: document.text
99 | .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control chars except \n, \r, \t
100 | .replace(/\r\n/g, '\n') // Normalize line endings
101 | .replace(/\r/g, '\n'), // Convert remaining \r to \n
102 | url: document.url,
103 | metadata: document.metadata
104 | };
105 |
106 | // ChatGPT expects document object as JSON-encoded text content
107 | return {
108 | content: [
109 | {
110 | type: "text",
111 | text: JSON.stringify(cleanedDocument)
112 | }
113 | ]
114 | };
115 | }
116 |
117 | /**
118 | * Create error response in structured JSON format
119 | */
120 | function createErrorResponse(error: string, requestId?: string): any {
121 | return {
122 | content: [
123 | {
124 | type: "text",
125 | text: JSON.stringify({
126 | error,
127 | requestId: requestId || 'unknown'
128 | })
129 | }
130 | ]
131 | };
132 | }
133 |
134 | export interface ServerConfig {
135 | name: string;
136 | description: string;
137 | version: string;
138 | }
139 |
140 | /**
141 | * Helper function to extract client metadata from request
142 | */
143 | function extractClientMetadata(request: any): Record<string, any> {
144 | const metadata: Record<string, any> = {};
145 |
146 | // Try to extract available metadata from the request
147 | if (request.meta) {
148 | metadata.meta = request.meta;
149 | }
150 |
151 | // Extract any client identification from headers or other sources
152 | if (request.headers) {
153 | metadata.headers = request.headers;
154 | }
155 |
156 | // Extract transport information if available
157 | if (request.transport) {
158 | metadata.transport = request.transport;
159 | }
160 |
161 | // Extract session or connection info
162 | if (request.id) {
163 | metadata.requestId = request.id;
164 | }
165 |
166 | return metadata;
167 | }
168 |
169 | /**
170 | * Base Server Handler Class
171 | * Provides shared functionality for all MCP server implementations
172 | */
173 | export class BaseServerHandler {
174 |
175 | /**
176 | * Configure server with shared resource and tool handlers
177 | */
178 | static configureServer(srv: Server): void {
179 | // Only setup resource handlers if resources capability is enabled
180 | // DISABLED: Resources capability causes 60,000+ resources which breaks Cursor
181 | // this.setupResourceHandlers(srv);
182 | this.setupToolHandlers(srv);
183 |
184 | const capabilities = (srv as unknown as { _capabilities?: { prompts?: object } })._capabilities;
185 | if (capabilities?.prompts) {
186 | this.setupPromptHandlers(srv);
187 | }
188 | }
189 |
190 | /**
191 | * Setup resource handlers (shared between all server types)
192 | */
193 | private static setupResourceHandlers(srv: Server): void {
194 | // List available resources
195 | srv.setRequestHandler(ListResourcesRequestSchema, async () => {
196 | const resources = await listDocumentationResources();
197 | return { resources };
198 | });
199 |
200 | // Read resource contents
201 | srv.setRequestHandler(ReadResourceRequestSchema, async (request) => {
202 | const { uri } = request.params;
203 | try {
204 | return await readDocumentationResource(uri);
205 | } catch (error: any) {
206 | return {
207 | contents: [{
208 | uri,
209 | mimeType: "text/plain",
210 | text: `Error reading resource: ${error.message}`
211 | }]
212 | };
213 | }
214 | });
215 | }
216 |
217 | /**
218 | * Setup tool handlers (shared between all server types)
219 | */
220 | private static setupToolHandlers(srv: Server): void {
221 | // List available tools
222 | srv.setRequestHandler(ListToolsRequestSchema, async () => {
223 | return {
224 | tools: [
225 | {
226 | name: "sap_community_search",
227 | description: `SEARCH SAP COMMUNITY: sap_community_search(query="search terms")
228 |
229 | FUNCTION NAME: sap_community_search (or mcp_sap-docs-remote_sap_community_search)
230 |
231 | FINDS: Blog posts, discussions, solutions from SAP Community
232 | INCLUDES: Engagement data (kudos), ranked by "Best Match"
233 |
234 | TYPICAL WORKFLOW:
235 | 1. sap_community_search(query="your problem + error code")
236 | 2. fetch(id="community-12345") for full posts
237 |
238 | BEST FOR TROUBLESHOOTING:
239 | • Include error codes: "415 error", "500 error"
240 | • Be specific: "CAP action binary upload 415"
241 | • Use real scenarios: "wizard implementation issues"`,
242 | inputSchema: {
243 | type: "object",
244 | properties: {
245 | query: {
246 | type: "string",
247 | description: "Search terms for SAP Community. Include error codes and specific technical details.",
248 | examples: [
249 | "CAP action parameter binary file upload 415 error",
250 | "wizard implementation best practices",
251 | "fiori elements authentication",
252 | "UI5 deployment issues",
253 | "wdi5 test automation problems"
254 | ]
255 | }
256 | },
257 | required: ["query"]
258 | }
259 | },
260 | {
261 | name: "sap_help_search",
262 | description: `SEARCH SAP HELP PORTAL: sap_help_search(query="product + topic")
263 |
264 | FUNCTION NAME: sap_help_search (or mcp_sap-docs-remote_sap_help_search)
265 |
266 | SEARCHES: Official SAP Help Portal (help.sap.com)
267 | COVERS: Product guides, implementation guides, technical documentation
268 |
269 | TYPICAL WORKFLOW:
270 | 1. sap_help_search(query="product name + configuration topic")
271 | 2. sap_help_get(result_id="sap-help-12345abc")
272 |
273 | BEST PRACTICES:
274 | • Include product names: "S/4HANA", "BTP", "Fiori"
275 | • Add specific tasks: "configuration", "setup", "deployment"
276 | • Use official SAP terminology`,
277 | inputSchema: {
278 | type: "object",
279 | properties: {
280 | query: {
281 | type: "string",
282 | description: "Search terms for SAP Help Portal. Include product names and specific topics.",
283 | examples: [
284 | "S/4HANA configuration",
285 | "Fiori Launchpad setup",
286 | "BTP integration",
287 | "ABAP development guide",
288 | "SAP Analytics Cloud setup"
289 | ]
290 | }
291 | },
292 | required: ["query"]
293 | }
294 | },
295 | {
296 | name: "sap_help_get",
297 | description: `GET SAP HELP PAGE: sap_help_get(result_id="sap-help-12345abc")
298 |
299 | FUNCTION NAME: sap_help_get (or mcp_sap-docs-remote_sap_help_get)
300 |
301 | RETRIEVES: Complete SAP Help Portal page content
302 | REQUIRES: Exact result_id from sap_help_search
303 |
304 | USAGE PATTERN:
305 | 1. Get ID from sap_help_search results
306 | 2. Use exact ID (don't modify the format)
307 | 3. Receive full page content + metadata`,
308 | inputSchema: {
309 | type: "object",
310 | properties: {
311 | result_id: {
312 | type: "string",
313 | description: "Exact ID from sap_help_search results. Copy the ID exactly as returned.",
314 | examples: [
315 | "sap-help-12345abc",
316 | "sap-help-98765def"
317 | ]
318 | }
319 | },
320 | required: ["result_id"]
321 | }
322 | },
323 | {
324 | name: "search",
325 | description: `SEARCH SAP DOCS: search(query="search terms")
326 |
327 | FUNCTION NAME: search
328 |
329 | COVERS: ABAP (all versions), UI5, CAP, wdi5, OpenUI5 APIs, Cloud SDK
330 | AUTO-DETECTS: ABAP versions from query (e.g. "LOOP 7.57", defaults to 7.58)
331 |
332 | TYPICAL WORKFLOW:
333 | 1. search(query="your search terms")
334 | 2. fetch(id="result_id_from_step_1")
335 |
336 | QUERY TIPS:
337 | • Be specific: "CAP action binary parameter" not just "CAP"
338 | • Include error codes: "415 error CAP action"
339 | • Use technical terms: "LargeBinary MediaType XMLHttpRequest"
340 | • For ABAP: Include version like "7.58" or "latest"`,
341 | inputSchema: {
342 | type: "object",
343 | properties: {
344 | query: {
345 | type: "string",
346 | description: "Search terms using natural language. Be specific and include technical terms.",
347 | examples: [
348 | "CAP binary data LargeBinary MediaType",
349 | "UI5 button properties",
350 | "wdi5 testing locators",
351 | "ABAP SELECT statements 7.58",
352 | "415 error CAP action parameter"
353 | ]
354 | }
355 | },
356 | required: ["query"]
357 | }
358 | },
359 | {
360 | name: "fetch",
361 | description: `GET SPECIFIC DOCS: fetch(id="result_id")
362 |
363 | FUNCTION NAME: fetch
364 |
365 | RETRIEVES: Full content from search results
366 | WORKS WITH: Document IDs returned by search
367 |
368 | ChatGPT COMPATIBLE:
369 | • Uses "id" parameter (required by ChatGPT)
370 | • Returns structured JSON content
371 | • Includes full document text and metadata`,
372 | inputSchema: {
373 | type: "object",
374 | properties: {
375 | id: {
376 | type: "string",
377 | description: "Unique document ID from search results. Use exact IDs returned by search.",
378 | examples: [
379 | "/cap/guides/domain-modeling",
380 | "/sapui5/controls/button-properties",
381 | "/openui5-api/sap/m/Button",
382 | "/abap-docs-758/inline-declarations",
383 | "community-12345"
384 | ]
385 | }
386 | },
387 | required: ["id"]
388 | }
389 | },
390 |
391 | ]
392 | };
393 | });
394 |
395 | // Handle tool execution
396 | srv.setRequestHandler(CallToolRequestSchema, async (request) => {
397 | const { name, arguments: args } = request.params;
398 | const clientMetadata = extractClientMetadata(request);
399 |
400 | if (name === "sap_docs_search" || name === "search") {
401 | const { query } = args as { query: string };
402 |
403 | // Enhanced logging with timing
404 | const timing = logger.logToolStart(name, query, clientMetadata);
405 |
406 | try {
407 | // Use hybrid search with reranking
408 | const results = await search(query, {
409 | k: CONFIG.RETURN_K
410 | });
411 |
412 | const topResults = results;
413 |
414 | if (topResults.length === 0) {
415 | logger.logToolSuccess(name, timing.requestId, timing.startTime, 0, { fallback: false });
416 | return createErrorResponse(
417 | `No results for "${query}". Try UI5 controls ("button", "table"), CAP topics ("actions", "binary"), testing ("wdi5", "e2e"), ABAP with versions ("SELECT 7.58"), or include error codes ("415 error").`,
418 | timing.requestId
419 | );
420 | }
421 |
422 | // Transform results to ChatGPT-compatible format with id, title, url
423 | const searchResults: SearchResult[] = topResults.map((r, index) => {
424 | // Extract library_id and topic from document ID
425 | const libraryIdMatch = r.id.match(/^(\/[^\/]+)/);
426 | const libraryId = libraryIdMatch ? libraryIdMatch[1] : (r.sourceId ? `/${r.sourceId}` : r.id);
427 | const topic = r.id.startsWith(libraryId) ? r.id.slice(libraryId.length + 1) : '';
428 |
429 | const config = getDocUrlConfig(libraryId);
430 | const docUrl = config ? generateDocumentationUrl(libraryId, '', r.text, config) : null;
431 |
432 | return {
433 | // ChatGPT-required format: id, title, url
434 | id: r.id, // Use full document ID as required by ChatGPT
435 | title: r.text.split('\n')[0] || r.id,
436 | url: docUrl || `#${r.id}`,
437 | // Additional fields for backward compatibility
438 | library_id: libraryId,
439 | topic: topic,
440 | snippet: r.text ? r.text.substring(0, CONFIG.EXCERPT_LENGTH_MAIN) + '...' : '',
441 | score: r.finalScore,
442 | metadata: {
443 | source: r.sourceId || 'sap-docs',
444 | library: libraryId,
445 | bm25Score: r.bm25,
446 | rank: index + 1
447 | }
448 | };
449 | });
450 |
451 | logger.logToolSuccess(name, timing.requestId, timing.startTime, topResults.length, { fallback: false });
452 |
453 | return createSearchResponse(searchResults);
454 | } catch (error) {
455 | logger.logToolError(name, timing.requestId, timing.startTime, error, false);
456 | logger.info('Attempting fallback to original search after hybrid search failure');
457 |
458 | // Fallback to original search
459 | try {
460 | const res: SearchResponse = await searchLibraries(query);
461 |
462 | if (!res.results.length) {
463 | logger.logToolSuccess(name, timing.requestId, timing.startTime, 0, { fallback: true });
464 | return createErrorResponse(
465 | res.error || `No fallback results for "${query}". Try UI5 controls ("button", "table"), CAP topics ("actions", "binary"), testing ("wdi5", "e2e"), ABAP with versions ("SELECT 7.58"), or include error codes.`,
466 | timing.requestId
467 | );
468 | }
469 |
470 | // Transform fallback results to structured format
471 | const fallbackResults: SearchResult[] = res.results.map((r, index) => ({
472 | id: r.id || `fallback-${index}`,
473 | title: r.title || 'SAP Documentation',
474 | url: r.url || `#${r.id}`,
475 | snippet: r.description ? r.description.substring(0, 200) + '...' : '',
476 | metadata: {
477 | source: 'fallback-search',
478 | rank: index + 1
479 | }
480 | }));
481 |
482 | logger.logToolSuccess(name, timing.requestId, timing.startTime, res.results.length, { fallback: true });
483 |
484 | return createSearchResponse(fallbackResults);
485 | } catch (fallbackError) {
486 | logger.logToolError(name, timing.requestId, timing.startTime, fallbackError, true);
487 | return createErrorResponse(
488 | `Search temporarily unavailable. Wait 30 seconds and retry, try sap_community_search instead, or use more specific search terms.`,
489 | timing.requestId
490 | );
491 | }
492 | }
493 | }
494 |
495 | if (name === "sap_community_search") {
496 | const { query } = args as { query: string };
497 |
498 | // Enhanced logging with timing
499 | const timing = logger.logToolStart(name, query, clientMetadata);
500 |
501 | try {
502 | const res: SearchResponse = await searchCommunity(query);
503 |
504 | if (!res.results.length) {
505 | logger.logToolSuccess(name, timing.requestId, timing.startTime, 0);
506 | return createErrorResponse(
507 | res.error || `No SAP Community posts found for "${query}". Try different keywords or check your connection.`,
508 | timing.requestId
509 | );
510 | }
511 |
512 | // Transform community search results to ChatGPT-compatible format
513 | const communityResults: SearchResult[] = res.results.map((r: any, index) => ({
514 | // ChatGPT-required format: id, title, url
515 | id: r.id || `community-${index}`,
516 | title: r.title || 'SAP Community Post',
517 | url: r.url || `#${r.id}`,
518 | // Additional fields for enhanced functionality
519 | library_id: r.library_id || `community-${index}`,
520 | topic: r.topic || '',
521 | snippet: r.snippet || (r.description ? r.description.substring(0, 200) + '...' : ''),
522 | score: r.score || 0,
523 | metadata: r.metadata || {
524 | source: 'sap-community',
525 | likes: r.likes,
526 | author: r.author,
527 | postTime: r.postTime,
528 | rank: index + 1
529 | }
530 | }));
531 |
532 | logger.logToolSuccess(name, timing.requestId, timing.startTime, res.results.length);
533 |
534 | return createSearchResponse(communityResults);
535 | } catch (error) {
536 | logger.logToolError(name, timing.requestId, timing.startTime, error);
537 | return createErrorResponse(
538 | `SAP Community search service temporarily unavailable. Please try again later.`,
539 | timing.requestId
540 | );
541 | }
542 | }
543 |
544 | if (name === "sap_docs_get" || name === "fetch") {
545 | // Handle both old format (library_id) and new ChatGPT format (id)
546 | const library_id = (args as any).library_id || (args as any).id;
547 | const topic = (args as any).topic || "";
548 |
549 | if (!library_id) {
550 | const timing = logger.logToolStart(name, 'missing_id', clientMetadata);
551 | logger.logToolError(name, timing.requestId, timing.startTime, new Error('Missing id parameter'));
552 | return createErrorResponse(
553 | `Missing required parameter: id. Please provide a document ID from search results.`,
554 | timing.requestId
555 | );
556 | }
557 |
558 | // Enhanced logging with timing
559 | const searchKey = library_id + (topic ? `/${topic}` : '');
560 | const timing = logger.logToolStart(name, searchKey, clientMetadata);
561 |
562 | try {
563 | const text = await fetchLibraryDocumentation(library_id, topic);
564 |
565 | if (!text) {
566 | logger.logToolSuccess(name, timing.requestId, timing.startTime, 0);
567 | return createErrorResponse(
568 | `Nothing found for ${library_id}`,
569 | timing.requestId
570 | );
571 | }
572 |
573 | // Transform document content to ChatGPT-compatible format
574 | const config = getDocUrlConfig(library_id);
575 | const docUrl = config ? generateDocumentationUrl(library_id, '', text, config) : null;
576 | const document: DocumentResult = {
577 | id: library_id,
578 | title: library_id.replace(/^\//, '').replace(/\//g, ' > ') + (topic ? ` (${topic})` : ''),
579 | text: text,
580 | url: docUrl || `#${library_id}`,
581 | metadata: {
582 | source: 'sap-docs',
583 | library: library_id,
584 | topic: topic || undefined,
585 | contentLength: text.length
586 | }
587 | };
588 |
589 | logger.logToolSuccess(name, timing.requestId, timing.startTime, 1, {
590 | contentLength: text.length,
591 | libraryId: library_id,
592 | topic: topic || undefined
593 | });
594 |
595 | return createDocumentResponse(document);
596 | } catch (error) {
597 | logger.logToolError(name, timing.requestId, timing.startTime, error);
598 | return createErrorResponse(
599 | `Error retrieving documentation for ${library_id}. Please try again later.`,
600 | timing.requestId
601 | );
602 | }
603 | }
604 |
605 | if (name === "sap_help_search") {
606 | const { query } = args as { query: string };
607 |
608 | // Enhanced logging with timing
609 | const timing = logger.logToolStart(name, query, clientMetadata);
610 |
611 | try {
612 | const res: SearchResponse = await searchSapHelp(query);
613 |
614 | if (!res.results.length) {
615 | logger.logToolSuccess(name, timing.requestId, timing.startTime, 0);
616 | return createErrorResponse(
617 | res.error || `No SAP Help results found for "${query}". Try different keywords or check your connection.`,
618 | timing.requestId
619 | );
620 | }
621 |
622 | // Transform SAP Help search results to ChatGPT-compatible format
623 | const helpResults: SearchResult[] = res.results.map((r, index) => ({
624 | // ChatGPT-required format: id, title, url
625 | id: r.id || `sap-help-${index}`,
626 | title: r.title || 'SAP Help Document',
627 | url: r.url || `#${r.id}`,
628 | // Additional fields for enhanced functionality
629 | snippet: r.description ? r.description.substring(0, 200) + '...' : '',
630 | metadata: {
631 | source: 'sap-help',
632 | totalSnippets: r.totalSnippets,
633 | rank: index + 1
634 | }
635 | }));
636 |
637 | logger.logToolSuccess(name, timing.requestId, timing.startTime, res.results.length);
638 |
639 | return createSearchResponse(helpResults);
640 | } catch (error) {
641 | logger.logToolError(name, timing.requestId, timing.startTime, error);
642 | return createErrorResponse(
643 | `SAP Help search service temporarily unavailable. Please try again later.`,
644 | timing.requestId
645 | );
646 | }
647 | }
648 |
649 | if (name === "sap_help_get") {
650 | const { result_id } = args as { result_id: string };
651 |
652 | // Enhanced logging with timing
653 | const timing = logger.logToolStart(name, result_id, clientMetadata);
654 |
655 | try {
656 | const content = await getSapHelpContent(result_id);
657 |
658 | // Transform SAP Help content to structured format
659 | const document: DocumentResult = {
660 | id: result_id,
661 | title: `SAP Help Document (${result_id})`,
662 | text: content,
663 | url: `https://help.sap.com/#${result_id}`,
664 | metadata: {
665 | source: 'sap-help',
666 | resultId: result_id,
667 | contentLength: content.length
668 | }
669 | };
670 |
671 | logger.logToolSuccess(name, timing.requestId, timing.startTime, 1, {
672 | contentLength: content.length,
673 | resultId: result_id
674 | });
675 |
676 | return createDocumentResponse(document);
677 | } catch (error) {
678 | logger.logToolError(name, timing.requestId, timing.startTime, error);
679 | return createErrorResponse(
680 | `Error retrieving SAP Help content. Please try again later.`,
681 | timing.requestId
682 | );
683 | }
684 | }
685 |
686 |
687 |
688 | throw new Error(`Unknown tool: ${name}`);
689 | });
690 | }
691 |
692 | /**
693 | * Setup prompt handlers (shared between all server types)
694 | */
695 | private static setupPromptHandlers(srv: Server): void {
696 | // List available prompts
697 | srv.setRequestHandler(ListPromptsRequestSchema, async () => {
698 | return {
699 | prompts: [
700 | {
701 | name: "sap_search_help",
702 | title: "SAP Documentation Search Helper",
703 | description: "Helps users construct effective search queries for SAP documentation",
704 | arguments: [
705 | {
706 | name: "domain",
707 | description: "SAP domain (UI5, CAP, ABAP, etc.)",
708 | required: false
709 | },
710 | {
711 | name: "context",
712 | description: "Specific context or technology area",
713 | required: false
714 | }
715 | ]
716 | },
717 | {
718 | name: "sap_troubleshoot",
719 | title: "SAP Issue Troubleshooting Guide",
720 | description: "Guides users through troubleshooting common SAP development issues",
721 | arguments: [
722 | {
723 | name: "error_message",
724 | description: "Error message or symptom description",
725 | required: false
726 | },
727 | {
728 | name: "technology",
729 | description: "SAP technology stack (UI5, CAP, ABAP, etc.)",
730 | required: false
731 | }
732 | ]
733 | }
734 | ]
735 | };
736 | });
737 |
738 | // Get specific prompt
739 | srv.setRequestHandler(GetPromptRequestSchema, async (request) => {
740 | const { name, arguments: args } = request.params;
741 |
742 | switch (name) {
743 | case "sap_search_help":
744 | const domain = args?.domain || "general SAP";
745 | const context = args?.context || "development";
746 |
747 | return {
748 | description: `Search helper for ${domain} documentation`,
749 | messages: [
750 | {
751 | role: "user",
752 | content: {
753 | type: "text",
754 | text: `I need help searching ${domain} documentation for ${context}. What search terms should I use to find the most relevant results?
755 |
756 | Here are some tips for effective SAP documentation searches:
757 |
758 | **For UI5/Frontend:**
759 | - Include specific control names (e.g., "Table", "Button", "ObjectPage")
760 | - Mention UI5 version if relevant
761 | - Use terms like "properties", "events", "aggregations"
762 |
763 | **For CAP/Backend:**
764 | - Include CDS concepts (e.g., "entity", "service", "annotation")
765 | - Mention specific features (e.g., "authentication", "authorization", "events")
766 | - Use terms like "deployment", "configuration"
767 |
768 | **For ABAP:**
769 | - Include version number (e.g., "7.58", "latest")
770 | - Use specific statement types (e.g., "SELECT", "LOOP", "MODIFY")
771 | - Include object types (e.g., "class", "method", "interface")
772 |
773 | **General Tips:**
774 | - Be specific rather than broad
775 | - Include error codes if troubleshooting
776 | - Use technical terms rather than business descriptions
777 | - Combine multiple related terms
778 |
779 | What specific topic are you looking for help with?`
780 | }
781 | }
782 | ]
783 | };
784 |
785 | case "sap_troubleshoot":
786 | const errorMessage = args?.error_message || "an issue";
787 | const technology = args?.technology || "SAP";
788 |
789 | return {
790 | description: `Troubleshooting guide for ${technology}`,
791 | messages: [
792 | {
793 | role: "user",
794 | content: {
795 | type: "text",
796 | text: `I'm experiencing ${errorMessage} with ${technology}. Let me help you troubleshoot this systematically.
797 |
798 | **Step 1: Information Gathering**
799 | - What is the exact error message or symptom?
800 | - When does this occur (during development, runtime, deployment)?
801 | - What were you trying to accomplish?
802 | - What technology stack are you using?
803 |
804 | **Step 2: Initial Search Strategy**
805 | Let me search the SAP documentation for similar issues:
806 |
807 | **For UI5 Issues:**
808 | - Search for the exact error message
809 | - Include control or component names
810 | - Look for browser console errors
811 |
812 | **For CAP Issues:**
813 | - Check service definitions and annotations
814 | - Look for deployment configuration
815 | - Verify database connections
816 |
817 | **For ABAP Issues:**
818 | - Include ABAP version in search
819 | - Look for syntax or runtime errors
820 | - Check object dependencies
821 |
822 | **Step 3: Common Solutions**
823 | Based on the issue type, I'll search for:
824 | - Official SAP documentation
825 | - Community discussions
826 | - Code examples and samples
827 |
828 | Please provide more details about your specific issue, and I'll search for relevant solutions.`
829 | }
830 | }
831 | ]
832 | };
833 |
834 | default:
835 | throw new Error(`Unknown prompt: ${name}`);
836 | }
837 | });
838 | }
839 |
840 | /**
841 | * Initialize metadata system (shared initialization logic)
842 | */
843 | static initializeMetadata(): void {
844 | logger.info('Initializing BM25 search system...');
845 | try {
846 | loadMetadata();
847 | logger.info('Search system ready with metadata');
848 | } catch (error) {
849 | logger.warn('Metadata loading failed, using defaults', { error: String(error) });
850 | logger.info('Search system ready');
851 | }
852 | }
853 | }
854 |
```
--------------------------------------------------------------------------------
/scripts/build-index.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Build pipeline step 1: Creates dist/data/index.json (bundle of all docs from submodules)
2 | import fg from "fast-glob";
3 | import fs from "fs/promises";
4 | import path, { join } from "path";
5 | import matter from "gray-matter";
6 |
7 | interface DocEntry {
8 | id: string; // "/sapui5/<rel-path>", "/cap/<rel-path>", "/openui5-api/<rel-path>", or "/openui5-samples/<rel-path>"
9 | title: string;
10 | description: string;
11 | snippetCount: number;
12 | relFile: string; // path relative to sources/…
13 | type?: "markdown" | "jsdoc" | "sample" | "markdown-section"; // type of documentation
14 | controlName?: string; // extracted UI5 control name (e.g., "Wizard", "Button")
15 | namespace?: string; // UI5 namespace (e.g., "sap.m", "sap.f")
16 | keywords?: string[]; // searchable keywords and tags
17 | properties?: string[]; // control properties for API docs
18 | events?: string[]; // control events for API docs
19 | aggregations?: string[]; // control aggregations for API docs
20 | parentDocument?: string; // for sections, the ID of the parent document
21 | sectionStartLine?: number; // for sections, the line number where the section starts
22 | headingLevel?: number; // for sections, the heading level (2=##, 3=###, 4=####)
23 | }
24 |
25 | interface LibraryBundle {
26 | id: string; // "/sapui5" | "/cap" | "/openui5-api" | "/openui5-samples"
27 | name: string; // "SAPUI5", "CAP", "OpenUI5 API", "OpenUI5 Samples"
28 | description: string;
29 | docs: DocEntry[];
30 | }
31 |
32 | interface SourceConfig {
33 | repoName: string;
34 | absDir: string;
35 | id: string;
36 | name: string;
37 | description: string;
38 | filePattern: string;
39 | exclude?: string;
40 | type: "markdown" | "jsdoc" | "sample";
41 | }
42 |
43 | const SOURCES: SourceConfig[] = [
44 | {
45 | repoName: "sapui5-docs",
46 | absDir: join("sources", "sapui5-docs", "docs"),
47 | id: "/sapui5",
48 | name: "SAPUI5",
49 | description: "Official SAPUI5 Markdown documentation",
50 | filePattern: "**/*.md",
51 | type: "markdown" as const
52 | },
53 | {
54 | repoName: "cap-docs",
55 | absDir: join("sources", "cap-docs"),
56 | id: "/cap",
57 | name: "SAP Cloud Application Programming Model (CAP)",
58 | description: "CAP (Capire) reference & guides",
59 | filePattern: "**/*.md",
60 | type: "markdown" as const
61 | },
62 | {
63 | repoName: "openui5",
64 | absDir: join("sources", "openui5", "src"),
65 | id: "/openui5-api",
66 | name: "OpenUI5 API",
67 | description: "OpenUI5 Control API documentation and JSDoc",
68 | filePattern: "**/src/**/*.js",
69 | exclude: "**/test/**/*",
70 | type: "jsdoc" as const
71 | },
72 | {
73 | repoName: "openui5",
74 | absDir: join("sources", "openui5", "src"),
75 | id: "/openui5-samples",
76 | name: "OpenUI5 Samples",
77 | description: "OpenUI5 demokit sample applications and code examples",
78 | filePattern: "**/demokit/sample/**/*.{js,xml,json,html}",
79 | type: "sample" as const
80 | },
81 | {
82 | repoName: "wdi5",
83 | absDir: join("sources", "wdi5", "docs"),
84 | id: "/wdi5",
85 | name: "wdi5",
86 | description: "wdi5 end-to-end test framework documentation",
87 | filePattern: "**/*.md",
88 | type: "markdown" as const
89 | },
90 | {
91 | repoName: "ui5-tooling",
92 | absDir: join("sources", "ui5-tooling", "docs"),
93 | id: "/ui5-tooling",
94 | name: "UI5 Tooling ",
95 | description: "UI5 Tooling documentation",
96 | filePattern: "**/*.md",
97 | type: "markdown" as const
98 | },
99 | {
100 | repoName: "cloud-mta-build-tool",
101 | absDir: join("sources", "cloud-mta-build-tool", "docs", "docs"),
102 | id: "/cloud-mta-build-tool",
103 | name: "Cloud MTA Build Tool",
104 | description: "Cloud MTA Build Tool documentation",
105 | filePattern: "**/*.md",
106 | type: "markdown" as const
107 | },
108 | {
109 | repoName: "ui5-webcomponents",
110 | absDir: join("sources", "ui5-webcomponents", "docs"),
111 | id: "/ui5-webcomponents",
112 | name: "UI5 Web Components",
113 | description: "UI5 Web Components documentation",
114 | filePattern: "**/*.md",
115 | type: "markdown" as const
116 | },
117 | {
118 | repoName: "cloud-sdk",
119 | absDir: join("sources", "cloud-sdk", "docs-js"),
120 | id: "/cloud-sdk-js",
121 | name: "Cloud SDK (JavaScript)",
122 | description: "Cloud SDK (JavaScript) documentation",
123 | filePattern: "**/*.mdx",
124 | type: "markdown" as const
125 | },
126 | {
127 | repoName: "cloud-sdk",
128 | absDir: join("sources", "cloud-sdk", "docs-java"),
129 | id: "/cloud-sdk-java",
130 | name: "Cloud SDK (Java)",
131 | description: "Cloud SDK (Java) documentation",
132 | filePattern: "**/*.mdx",
133 | type: "markdown" as const
134 | },
135 | {
136 | repoName: "cloud-sdk-ai",
137 | absDir: join("sources", "cloud-sdk-ai", "docs-js"),
138 | id: "/cloud-sdk-ai-js",
139 | name: "Cloud SDK AI (JavaScript)",
140 | description: "Cloud SDK AI (JavaScript) documentation",
141 | filePattern: "**/*.mdx",
142 | type: "markdown" as const
143 | },
144 | {
145 | repoName: "cloud-sdk-ai",
146 | absDir: join("sources", "cloud-sdk-ai", "docs-java"),
147 | id: "/cloud-sdk-ai-java",
148 | name: "Cloud SDK AI (Java)",
149 | description: "Cloud SDK AI (Java) documentation",
150 | filePattern: "**/*.mdx",
151 | type: "markdown" as const
152 | },
153 | {
154 | repoName: "ui5-typescript",
155 | absDir: join("sources", "ui5-typescript"),
156 | id: "/ui5-typescript",
157 | name: "UI5 TypeScript",
158 | description: "Official entry point to anything TypeScript related for UI5",
159 | filePattern: "*.md",
160 | type: "markdown" as const
161 | },
162 | {
163 | repoName: "ui5-cc-spreadsheetimporter",
164 | absDir: join("sources", "ui5-cc-spreadsheetimporter", "docs"),
165 | id: "/ui5-cc-spreadsheetimporter",
166 | name: "UI5 CC Spreadsheet Importer",
167 | description: "UI5 Custom Control for importing spreadsheet data",
168 | filePattern: "**/*.md",
169 | type: "markdown" as const
170 | },
171 | {
172 | repoName: "abap-cheat-sheets",
173 | absDir: join("sources", "abap-cheat-sheets"),
174 | id: "/abap-cheat-sheets",
175 | name: "ABAP Cheat Sheets",
176 | description: "Comprehensive ABAP syntax examples and cheat sheets",
177 | filePattern: "*.md",
178 | type: "markdown" as const
179 | },
180 | {
181 | repoName: "sap-styleguides",
182 | absDir: join("sources", "sap-styleguides"),
183 | id: "/sap-styleguides",
184 | name: "SAP Style Guides",
185 | description: "SAP coding style guides and best practices including Clean ABAP",
186 | filePattern: "**/*.md",
187 | type: "markdown" as const
188 | },
189 | {
190 | repoName: "dsag-abap-leitfaden",
191 | absDir: join("sources", "dsag-abap-leitfaden", "docs"),
192 | id: "/dsag-abap-leitfaden",
193 | name: "DSAG ABAP Leitfaden",
194 | description: "German ABAP guidelines and best practices by DSAG",
195 | filePattern: "**/*.md",
196 | type: "markdown" as const
197 | },
198 | {
199 | repoName: "abap-fiori-showcase",
200 | absDir: join("sources", "abap-fiori-showcase"),
201 | id: "/abap-fiori-showcase",
202 | name: "ABAP Platform Fiori Feature Showcase",
203 | description: "Annotation-driven SAP Fiori Elements features for OData V4 using ABAP RAP",
204 | filePattern: "*.md",
205 | type: "markdown" as const
206 | },
207 | {
208 | repoName: "cap-fiori-showcase",
209 | absDir: join("sources", "cap-fiori-showcase"),
210 | id: "/cap-fiori-showcase",
211 | name: "CAP Fiori Elements Feature Showcase",
212 | description: "SAP Fiori Elements features and annotations showcase using CAP",
213 | filePattern: "*.md",
214 | type: "markdown" as const
215 | },
216 | {
217 | repoName: "abap-docs",
218 | absDir: join("sources", "abap-docs", "docs", "7.58", "md"),
219 | id: "/abap-docs-758",
220 | name: "ABAP Keyword Documentation (7.58)",
221 | description: "Official ABAP language reference and syntax documentation (version 7.58) - individual files optimized for LLM consumption",
222 | filePattern: "*.md",
223 | type: "markdown" as const
224 | },
225 | {
226 | repoName: "abap-docs",
227 | absDir: join("sources", "abap-docs", "docs", "7.57", "md"),
228 | id: "/abap-docs-757",
229 | name: "ABAP Keyword Documentation (7.57)",
230 | description: "Official ABAP language reference and syntax documentation (version 7.57) - individual files optimized for LLM consumption",
231 | filePattern: "*.md",
232 | type: "markdown" as const
233 | },
234 | {
235 | repoName: "abap-docs",
236 | absDir: join("sources", "abap-docs", "docs", "7.56", "md"),
237 | id: "/abap-docs-756",
238 | name: "ABAP Keyword Documentation (7.56)",
239 | description: "Official ABAP language reference and syntax documentation (version 7.56) - individual files optimized for LLM consumption",
240 | filePattern: "*.md",
241 | type: "markdown" as const
242 | },
243 | {
244 | repoName: "abap-docs",
245 | absDir: join("sources", "abap-docs", "docs", "7.55", "md"),
246 | id: "/abap-docs-755",
247 | name: "ABAP Keyword Documentation (7.55)",
248 | description: "Official ABAP language reference and syntax documentation (version 7.55) - individual files optimized for LLM consumption",
249 | filePattern: "*.md",
250 | type: "markdown" as const
251 | },
252 | {
253 | repoName: "abap-docs",
254 | absDir: join("sources", "abap-docs", "docs", "7.54", "md"),
255 | id: "/abap-docs-754",
256 | name: "ABAP Keyword Documentation (7.54)",
257 | description: "Official ABAP language reference and syntax documentation (version 7.54) - individual files optimized for LLM consumption",
258 | filePattern: "*.md",
259 | type: "markdown" as const
260 | },
261 | {
262 | repoName: "abap-docs",
263 | absDir: join("sources", "abap-docs", "docs", "7.53", "md"),
264 | id: "/abap-docs-753",
265 | name: "ABAP Keyword Documentation (7.53)",
266 | description: "Official ABAP language reference and syntax documentation (version 7.53) - individual files optimized for LLM consumption",
267 | filePattern: "*.md",
268 | type: "markdown" as const
269 | },
270 | {
271 | repoName: "abap-docs",
272 | absDir: join("sources", "abap-docs", "docs", "7.52", "md"),
273 | id: "/abap-docs-752",
274 | name: "ABAP Keyword Documentation (7.52)",
275 | description: "Official ABAP language reference and syntax documentation (version 7.52) - individual files optimized for LLM consumption",
276 | filePattern: "*.md",
277 | type: "markdown" as const
278 | },
279 | {
280 | repoName: "abap-docs",
281 | absDir: join("sources", "abap-docs", "docs", "latest", "md"),
282 | id: "/abap-docs-latest",
283 | name: "ABAP Keyword Documentation (Latest)",
284 | description: "Official ABAP language reference and syntax documentation (latest version) - individual files optimized for LLM consumption",
285 | filePattern: "*.md",
286 | type: "markdown" as const
287 | }
288 | ];
289 |
290 | // Extract meaningful content from ABAP documentation files
291 | function extractAbapContent(content: string, filename: string): { title: string; description: string; snippetCount: number } {
292 | const lines = content.split(/\r?\n/);
293 |
294 | // Skip attribution header (first few lines with "📖 Official SAP Documentation")
295 | let contentStart = 0;
296 | for (let i = 0; i < lines.length; i++) {
297 | if (lines[i].includes('📖 Official SAP Documentation') || lines[i].startsWith('> **📖')) {
298 | // Skip until we find the actual content (after attribution and separators)
299 | for (let j = i; j < lines.length; j++) {
300 | if (lines[j].trim() === '' || lines[j].includes('* * *') || lines[j].includes('---')) {
301 | continue;
302 | }
303 | if (!lines[j].startsWith('>')) {
304 | contentStart = j;
305 | break;
306 | }
307 | }
308 | break;
309 | }
310 | }
311 |
312 | // Find the actual title (first non-metadata heading)
313 | let title = filename.replace('.md', '').replace('aben', '');
314 | for (let i = contentStart; i < lines.length; i++) {
315 | const line = lines[i].trim();
316 | if (line && !line.startsWith('AS ABAP Release') && !line.startsWith('[ABAP -') && !line.startsWith('[![') && !line.includes('Mail Feedback')) {
317 | if (line.match(/^[A-Z][a-zA-Z\s]+$/)) {
318 | // Found a proper title (like "Inline Declarations")
319 | title = line;
320 | contentStart = i + 1;
321 | break;
322 | }
323 | }
324 | }
325 |
326 | // Extract meaningful description from content
327 | const contentLines = lines.slice(contentStart);
328 | const meaningfulLines = [];
329 |
330 | for (const line of contentLines) {
331 | const trimmed = line.trim();
332 |
333 | // Skip empty lines, separators, and navigation
334 | if (!trimmed || trimmed === '---' || trimmed === '* * *' || trimmed.startsWith('[ABAP -') || trimmed.includes('Mail Feedback')) {
335 | continue;
336 | }
337 |
338 | // Skip metadata lines
339 | if (trimmed.startsWith('AS ABAP Release') || trimmed.includes('©Copyright')) {
340 | continue;
341 | }
342 |
343 | // Stop at "Continue" or "Programming Guideline" sections
344 | if (trimmed.startsWith('Continue') || trimmed.startsWith('Programming Guideline')) {
345 | break;
346 | }
347 |
348 | meaningfulLines.push(trimmed);
349 |
350 | // Stop when we have enough content for a good description
351 | if (meaningfulLines.join(' ').length > 300) {
352 | break;
353 | }
354 | }
355 |
356 | // Build description from meaningful content
357 | let description = meaningfulLines.join(' ').trim();
358 |
359 | // If description is too short, add version info
360 | if (description.length < 50) {
361 | const versionMatch = filename.match(/abap-docs-(\d+)/);
362 | const version = versionMatch ? versionMatch[1] : '7.58';
363 | description = `${title} - ABAP ${version} language reference`;
364 | }
365 |
366 | // Extract ABAP-specific terms for better searchability
367 | const abapTerms: string[] = [];
368 | const descriptionLower = description.toLowerCase();
369 |
370 | // Common ABAP statement keywords
371 | const statements = ['data', 'final', 'field-symbol', 'select', 'loop', 'if', 'try', 'catch', 'class', 'method'];
372 | statements.forEach(stmt => {
373 | if (descriptionLower.includes(stmt)) {
374 | abapTerms.push(stmt);
375 | }
376 | });
377 |
378 | // Add statement context if found
379 | if (abapTerms.length > 0) {
380 | description += ` | Statements: ${abapTerms.join(', ')}`;
381 | }
382 |
383 | // Count code snippets (ABAP typically has fewer but more meaningful ones)
384 | const snippetCount = (content.match(/```/g)?.length || 0) / 2;
385 |
386 | return {
387 | title,
388 | description: description.substring(0, 400), // Allow longer descriptions for ABAP
389 | snippetCount
390 | };
391 | }
392 |
393 | // Extract information from sample files (JS, XML, JSON, HTML)
394 | function extractSampleInfo(content: string, filePath: string) {
395 | const fileName = path.basename(filePath);
396 | const fileExt = path.extname(filePath);
397 | const sampleDir = path.dirname(filePath);
398 |
399 | // Extract control name from the path (e.g., "Button", "Wizard", "Table")
400 | const pathParts = sampleDir.split('/');
401 | const sampleIndex = pathParts.findIndex(part => part === 'sample');
402 | const controlName = sampleIndex >= 0 && sampleIndex < pathParts.length - 1
403 | ? pathParts[sampleIndex + 1]
404 | : path.basename(sampleDir);
405 |
406 | let title = `${controlName} Sample - ${fileName}`;
407 | let description = `Sample implementation of ${controlName} control`;
408 | let snippetCount = 0;
409 |
410 | // Extract specific information based on file type
411 | if (fileExt === '.js') {
412 | // JavaScript sample files
413 | const jsContent = content.toLowerCase();
414 |
415 | // Look for common UI5 patterns
416 | if (jsContent.includes('controller')) {
417 | title = `${controlName} Sample Controller`;
418 | description = `Controller implementation for ${controlName} sample`;
419 | } else if (jsContent.includes('component')) {
420 | title = `${controlName} Sample Component`;
421 | description = `Component definition for ${controlName} sample`;
422 | }
423 |
424 | // Count meaningful code patterns
425 | const codePatterns = [
426 | /function\s*\(/g,
427 | /onPress\s*:/g,
428 | /on[A-Z][a-zA-Z]*\s*:/g,
429 | /\.attach[A-Z][a-zA-Z]*/g,
430 | /new\s+sap\./g
431 | ];
432 |
433 | snippetCount = codePatterns.reduce((count, pattern) => {
434 | return count + (content.match(pattern)?.length || 0);
435 | }, 0);
436 |
437 | } else if (fileExt === '.xml') {
438 | // XML view files
439 | title = `${controlName} Sample View`;
440 | description = `XML view implementation for ${controlName} sample`;
441 |
442 | // Count XML controls and bindings
443 | const xmlPatterns = [
444 | /<[a-zA-Z][^>]*>/g,
445 | /\{[^}]+\}/g, // bindings
446 | /press=/g,
447 | /text=/g
448 | ];
449 |
450 | snippetCount = xmlPatterns.reduce((count, pattern) => {
451 | return count + (content.match(pattern)?.length || 0);
452 | }, 0);
453 |
454 | } else if (fileExt === '.json') {
455 | // Manifest or model files
456 | if (fileName.includes('manifest')) {
457 | title = `${controlName} Sample Manifest`;
458 | description = `Application manifest for ${controlName} sample`;
459 | } else {
460 | title = `${controlName} Sample Data`;
461 | description = `Sample data model for ${controlName} control`;
462 | }
463 |
464 | try {
465 | const jsonObj = JSON.parse(content);
466 | snippetCount = Object.keys(jsonObj).length;
467 | } catch {
468 | snippetCount = 1;
469 | }
470 |
471 | } else if (fileExt === '.html') {
472 | // HTML files
473 | title = `${controlName} Sample HTML`;
474 | description = `HTML page for ${controlName} sample`;
475 |
476 | const htmlPatterns = [
477 | /<script[^>]*>/g,
478 | /<div[^>]*>/g,
479 | /data-sap-ui-/g
480 | ];
481 |
482 | snippetCount = htmlPatterns.reduce((count, pattern) => {
483 | return count + (content.match(pattern)?.length || 0);
484 | }, 0);
485 | }
486 |
487 | // Add library information from path
488 | const libraryMatch = filePath.match(/src\/([^\/]+)\/test/);
489 | if (libraryMatch) {
490 | const library = libraryMatch[1];
491 | description += ` (${library} library)`;
492 | }
493 |
494 | return {
495 | title,
496 | description,
497 | snippetCount: Math.max(1, snippetCount) // Ensure at least 1
498 | };
499 | }
500 |
501 | // Extract JSDoc information from JavaScript files with enhanced metadata
502 | function extractJSDocInfo(content: string, fileName: string) {
503 | const lines = content.split(/\r?\n/);
504 |
505 | // Try to find the main class/control definition
506 | const classMatch = content.match(/\.extend\s*\(\s*["']([^"']+)["']/);
507 | const fullControlName = classMatch ? classMatch[1] : path.basename(fileName, ".js");
508 |
509 | // Extract namespace and control name
510 | const namespaceMatch = fullControlName.match(/^(sap\.[^.]+)\.(.*)/);
511 | const namespace = namespaceMatch ? namespaceMatch[1] : '';
512 | const controlName = namespaceMatch ? namespaceMatch[2] : fullControlName;
513 |
514 | // Extract main class JSDoc comment
515 | const jsdocMatch = content.match(/\/\*\*\s*([\s\S]*?)\*\//);
516 | let description = "";
517 |
518 | if (jsdocMatch) {
519 | // Clean up JSDoc comment and extract description
520 | const jsdocContent = jsdocMatch[1]
521 | .split('\n')
522 | .map(line => line.replace(/^\s*\*\s?/, ''))
523 | .join('\n')
524 | .trim();
525 |
526 | // Extract the main description (everything before @tags)
527 | const firstAtIndex = jsdocContent.indexOf('@');
528 | description = firstAtIndex > -1
529 | ? jsdocContent.substring(0, firstAtIndex).trim()
530 | : jsdocContent;
531 |
532 | // Clean up common JSDoc patterns
533 | description = description
534 | .replace(/^\s*Constructor for a new.*$/m, '')
535 | .replace(/^\s*@param.*$/gm, '')
536 | .replace(/^\s*@.*$/gm, '')
537 | .replace(/\n\s*\n/g, '\n')
538 | .trim();
539 | }
540 |
541 | // Extract properties, events, aggregations with better parsing
542 | const properties: string[] = [];
543 | const events: string[] = [];
544 | const aggregations: string[] = [];
545 | const keywords: string[] = [];
546 |
547 | // Extract properties
548 | const propertiesSection = content.match(/properties\s*:\s*\{([\s\S]*?)\n\s*\}/);
549 | if (propertiesSection) {
550 | const propMatches = propertiesSection[1].matchAll(/(\w+)\s*:\s*\{/g);
551 | for (const match of propMatches) {
552 | properties.push(match[1]);
553 | }
554 | }
555 |
556 | // Extract events
557 | const eventsSection = content.match(/events\s*:\s*\{([\s\S]*?)\n\s*\}/);
558 | if (eventsSection) {
559 | const eventMatches = eventsSection[1].matchAll(/(\w+)\s*:\s*\{/g);
560 | for (const match of eventMatches) {
561 | events.push(match[1]);
562 | }
563 | }
564 |
565 | // Extract aggregations
566 | const aggregationsSection = content.match(/aggregations\s*:\s*\{([\s\S]*?)\n\s*\}/);
567 | if (aggregationsSection) {
568 | const aggMatches = aggregationsSection[1].matchAll(/(\w+)\s*:\s*\{/g);
569 | for (const match of aggMatches) {
570 | aggregations.push(match[1]);
571 | }
572 | }
573 |
574 | // Generate keywords based on control name and content
575 | keywords.push(controlName.toLowerCase());
576 | if (namespace) keywords.push(namespace);
577 | if (fullControlName !== controlName) keywords.push(fullControlName);
578 |
579 | // Add common UI5 control keywords based on control name
580 | const controlLower = controlName.toLowerCase();
581 | if (controlLower.includes('wizard')) keywords.push('wizard', 'step', 'multi-step', 'process');
582 | if (controlLower.includes('button')) keywords.push('button', 'click', 'press', 'action');
583 | if (controlLower.includes('table')) keywords.push('table', 'grid', 'data', 'row', 'column');
584 | if (controlLower.includes('dialog')) keywords.push('dialog', 'popup', 'modal', 'overlay');
585 | if (controlLower.includes('input')) keywords.push('input', 'field', 'text', 'form');
586 | if (controlLower.includes('list')) keywords.push('list', 'item', 'collection');
587 | if (controlLower.includes('panel')) keywords.push('panel', 'container', 'layout');
588 | if (controlLower.includes('page')) keywords.push('page', 'navigation', 'view');
589 |
590 | // Add property/event-based keywords
591 | if (properties.includes('text')) keywords.push('text');
592 | if (properties.includes('value')) keywords.push('value');
593 | if (events.includes('press')) keywords.push('press', 'click');
594 | if (events.includes('change')) keywords.push('change', 'update');
595 |
596 | // Count code blocks and property definitions
597 | const codeBlockCount = (content.match(/```/g)?.length || 0) / 2;
598 | const propertyCount = properties.length + events.length + aggregations.length;
599 |
600 | return {
601 | title: fullControlName,
602 | description: description || `OpenUI5 control: ${fullControlName}`,
603 | snippetCount: Math.max(1, codeBlockCount + Math.floor(propertyCount / 3)),
604 | controlName,
605 | namespace,
606 | keywords: [...new Set(keywords)],
607 | properties,
608 | events,
609 | aggregations
610 | };
611 | }
612 |
613 | function extractMarkdownSections(content: string, lines: string[], src: any, relFile: string, docs: DocEntry[]) {
614 | const sections: { title: string; content: string; startLine: number; level: number }[] = [];
615 | let currentSection: { title: string; content: string; startLine: number; level: number } | null = null;
616 |
617 | for (let i = 0; i < lines.length; i++) {
618 | const line = lines[i];
619 |
620 | // Check for headings (##, ###, ####)
621 | let headingLevel = 0;
622 | let headingText = '';
623 |
624 | if (line.startsWith('#### ')) {
625 | headingLevel = 4;
626 | headingText = line.slice(5).trim();
627 | } else if (line.startsWith('### ')) {
628 | headingLevel = 3;
629 | headingText = line.slice(4).trim();
630 | } else if (line.startsWith('## ')) {
631 | headingLevel = 2;
632 | headingText = line.slice(3).trim();
633 | }
634 |
635 | if (headingLevel > 0) {
636 | // Save previous section if it exists
637 | if (currentSection) {
638 | sections.push(currentSection);
639 | }
640 |
641 | // Start new section
642 | currentSection = {
643 | title: headingText,
644 | content: '',
645 | startLine: i,
646 | level: headingLevel
647 | };
648 | } else if (currentSection) {
649 | // Add content to current section
650 | currentSection.content += line + '\n';
651 | }
652 | }
653 |
654 | // Add the last section
655 | if (currentSection) {
656 | sections.push(currentSection);
657 | }
658 |
659 | // Create separate docs entries for meaningful sections
660 | for (const section of sections) {
661 | // Skip very short sections or those with placeholder titles
662 | if (section.content.trim().length < 100 || section.title.length < 3) {
663 | continue;
664 | }
665 |
666 | // Generate description from section content, including code blocks for better searchability
667 | const contentLines = section.content.split('\n').filter(l => l.trim() && !l.startsWith('#'));
668 |
669 | // Extract code blocks content for technical terms
670 | const codeBlocks = section.content.match(/```[\s\S]*?```/g) || [];
671 | const codeContent = codeBlocks
672 | .map(block => block.replace(/```[\w]*\n?/g, '').replace(/```/g, ''))
673 | .join(' ')
674 | .replace(/\s+/g, ' ')
675 | .trim();
676 |
677 | // Combine description with code content for better indexing
678 | let description = contentLines.slice(0, 3).join(' ').trim() || section.title;
679 |
680 | // Include important technical terms from code blocks (like annotation qualifiers)
681 | if (codeContent) {
682 | // Extract meaningful technical terms (identifiers, annotation qualifiers, etc.)
683 | const technicalTerms = (codeContent.match(/[@#]?\w+(?:\.\w+)*(?:#\w+)?/g) || [])
684 | .filter((term: string) => term.length > 3 && !['true', 'false', 'null', 'undefined', 'function', 'return'].includes(term.toLowerCase()))
685 | .slice(0, 10); // Limit to prevent bloating
686 |
687 | if (technicalTerms.length > 0) {
688 | description += ' ' + technicalTerms.join(' ');
689 | }
690 | }
691 |
692 | // Count code snippets in this section
693 | const snippetCount = (section.content.match(/```/g)?.length || 0) / 2;
694 |
695 | // Create section entry
696 | const sectionId = `${src.id}/${relFile.replace(/\.md$/, "")}#${section.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;
697 |
698 | docs.push({
699 | id: sectionId,
700 | title: section.title,
701 | description: description.substring(0, 300) + (description.length > 300 ? '...' : ''),
702 | snippetCount,
703 | relFile,
704 | type: 'markdown-section' as any,
705 | parentDocument: `${src.id}/${relFile.replace(/\.md$/, "")}`,
706 | sectionStartLine: section.startLine,
707 | headingLevel: section.level
708 | });
709 | }
710 | }
711 |
712 | async function main() {
713 | await fs.mkdir("dist/data", { recursive: true });
714 | const all: Record<string, LibraryBundle> = {};
715 |
716 | for (const src of SOURCES) {
717 | const patterns = [src.filePattern];
718 | if (src.exclude) {
719 | patterns.push(`!${src.exclude}`);
720 | }
721 | const files = await fg(patterns, { cwd: src.absDir, absolute: true });
722 |
723 | const docs: DocEntry[] = [];
724 |
725 | for (const absPath of files) {
726 | const rel = path.relative(src.absDir, absPath).replace(/\\/g, "/");
727 | const raw = await fs.readFile(absPath, "utf8");
728 |
729 |
730 | let title: string;
731 | let description: string;
732 | let snippetCount: number;
733 | let id: string;
734 |
735 | if (src.type === "markdown") {
736 | // Handle markdown files with error handling for malformed frontmatter
737 | let frontmatter, content;
738 | try {
739 | const parsed = matter(raw);
740 | frontmatter = parsed.data;
741 | content = parsed.content;
742 | } catch (yamlError: any) {
743 | console.warn(`YAML parsing failed for ${rel}, using fallback:`, yamlError?.message || yamlError);
744 | // Fallback: extract content without frontmatter
745 | const lines = raw.split('\n');
746 | const contentStartIndex = lines.findIndex((line, index) => line.trim() === '---' && index > 0) + 1;
747 | frontmatter = {};
748 | content = contentStartIndex > 0 ? lines.slice(contentStartIndex).join('\n') : raw;
749 | }
750 | const lines = content.split(/\r?\n/);
751 |
752 | // Use frontmatter for title and description (works for ABAP and other sources)
753 | title = frontmatter?.title ||
754 | lines.find((l) => l.startsWith("# "))?.slice(2).trim() ||
755 | path.basename(rel, ".md");
756 |
757 | // Enhanced description from frontmatter or content
758 | if (frontmatter?.description) {
759 | description = frontmatter.description;
760 | } else if (frontmatter?.synopsis && content.includes("{{ $frontmatter.synopsis }}")) {
761 | description = frontmatter.synopsis;
762 | } else {
763 | // Fallback to content extraction
764 | const rawDescription = lines.find((l) => l.trim() && !l.startsWith("#"))?.trim() || "";
765 | description = rawDescription;
766 | }
767 |
768 | snippetCount = (content.match(/```/g)?.length || 0) / 2;
769 |
770 | id = `${src.id}/${rel.replace(/\.md$/, "")}`;
771 |
772 | // Extract individual sections as separate entries for all markdown docs
773 | if (content.includes('##')) {
774 | extractMarkdownSections(content, lines, src, rel, docs);
775 | }
776 | } else if (src.type === "jsdoc") {
777 | // Handle JavaScript files with JSDoc
778 | const jsDocInfo = extractJSDocInfo(raw, path.basename(absPath));
779 | title = jsDocInfo.title;
780 | description = jsDocInfo.description;
781 | snippetCount = jsDocInfo.snippetCount;
782 | id = `${src.id}/${rel.replace(/\.js$/, "")}`;
783 |
784 | // Skip files that don't look like UI5 controls
785 | if (!raw.includes('.extend') || !raw.includes('metadata')) {
786 | continue;
787 | }
788 |
789 | docs.push({
790 | id,
791 | title,
792 | description,
793 | snippetCount,
794 | relFile: rel,
795 | type: src.type,
796 | controlName: jsDocInfo.controlName,
797 | namespace: jsDocInfo.namespace,
798 | keywords: jsDocInfo.keywords,
799 | properties: jsDocInfo.properties,
800 | events: jsDocInfo.events,
801 | aggregations: jsDocInfo.aggregations
802 | });
803 |
804 | } else if (src.type === "sample") {
805 | // Handle sample files (JS, XML, JSON, HTML)
806 | const sampleInfo = extractSampleInfo(raw, rel);
807 | title = sampleInfo.title;
808 | description = sampleInfo.description;
809 | snippetCount = sampleInfo.snippetCount;
810 | id = `${src.id}/${rel.replace(/\.(js|xml|json|html)$/, "")}`;
811 |
812 | // Skip empty files or non-meaningful samples
813 | if (raw.trim().length < 50) {
814 | continue;
815 | }
816 |
817 | // Extract control name from sample path for better searchability
818 | const pathParts = rel.split('/');
819 | const sampleIndex = pathParts.findIndex(part => part === 'sample');
820 | const controlName = sampleIndex >= 0 && sampleIndex < pathParts.length - 1
821 | ? pathParts[sampleIndex + 1]
822 | : path.basename(path.dirname(rel));
823 |
824 | // Generate sample keywords
825 | const keywords = [controlName.toLowerCase(), 'sample', 'example'];
826 | if (rel.includes('.xml')) keywords.push('view', 'xml');
827 | if (rel.includes('.js')) keywords.push('controller', 'javascript');
828 | if (rel.includes('.json')) keywords.push('model', 'data', 'configuration');
829 | if (rel.includes('manifest')) keywords.push('manifest', 'app');
830 |
831 | docs.push({
832 | id,
833 | title,
834 | description,
835 | snippetCount,
836 | relFile: rel,
837 | type: src.type,
838 | controlName,
839 | keywords: [...new Set(keywords)]
840 | });
841 |
842 | } else {
843 | continue; // Skip unknown file types
844 | }
845 |
846 | // For markdown files, still use the basic structure
847 | if (src.type === "markdown") {
848 |
849 | docs.push({
850 | id,
851 | title,
852 | description,
853 | snippetCount,
854 | relFile: rel,
855 | type: src.type
856 | });
857 | }
858 | }
859 |
860 | const bundle: LibraryBundle = {
861 | id: src.id,
862 | name: src.name,
863 | description: src.description,
864 | docs
865 | };
866 |
867 | all[src.id] = bundle;
868 | await fs.writeFile(
869 | path.join("dist", "data", `data${src.id}.json`.replace(/\//g, "_")),
870 | JSON.stringify(bundle, null, 2)
871 | );
872 | }
873 |
874 | await fs.writeFile("dist/data/index.json", JSON.stringify(all, null, 2));
875 | console.log("✅ Index built with", Object.keys(all).length, "libraries.");
876 | }
877 |
878 | main();
```