@larkiny/astro-github-loader 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +675 -0
- package/dist/github.cleanup.d.ts +5 -0
- package/dist/github.cleanup.js +216 -0
- package/dist/github.constants.d.ts +24 -0
- package/dist/github.constants.js +24 -0
- package/dist/github.content.d.ts +138 -0
- package/dist/github.content.js +1016 -0
- package/dist/github.dryrun.d.ts +72 -0
- package/dist/github.dryrun.js +247 -0
- package/dist/github.link-transform.d.ts +77 -0
- package/dist/github.link-transform.js +321 -0
- package/dist/github.loader.d.ts +14 -0
- package/dist/github.loader.js +143 -0
- package/dist/github.loader.spec.d.ts +1 -0
- package/dist/github.loader.spec.js +96 -0
- package/dist/github.logger.d.ts +132 -0
- package/dist/github.logger.js +260 -0
- package/dist/github.sync.d.ts +5 -0
- package/dist/github.sync.js +292 -0
- package/dist/github.types.d.ts +315 -0
- package/dist/github.types.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/package.json +66 -0
- package/src/github.cleanup.ts +243 -0
- package/src/github.constants.ts +25 -0
- package/src/github.content.ts +1205 -0
- package/src/github.dryrun.ts +339 -0
- package/src/github.link-transform.ts +452 -0
- package/src/github.loader.spec.ts +106 -0
- package/src/github.loader.ts +189 -0
- package/src/github.logger.ts +324 -0
- package/src/github.types.ts +339 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { slug } from 'github-slugger';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { LinkMapping, LinkTransformContext, MatchedPattern, IncludePattern, PathMappingValue, EnhancedPathMapping } from './github.types.js';
|
|
4
|
+
import type { Logger } from './github.logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Represents an imported file with its content and metadata
|
|
8
|
+
*/
|
|
9
|
+
export interface ImportedFile {
|
|
10
|
+
/** Original source path in the repository */
|
|
11
|
+
sourcePath: string;
|
|
12
|
+
/** Target path where the file will be written */
|
|
13
|
+
targetPath: string;
|
|
14
|
+
/** File content */
|
|
15
|
+
content: string;
|
|
16
|
+
/** File ID for cross-referencing */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Context information for link transformations */
|
|
19
|
+
linkContext?: LinkTransformContext;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Context for global link transformation
|
|
24
|
+
*/
|
|
25
|
+
interface GlobalLinkContext {
|
|
26
|
+
/** Map from source paths to target paths for all imported files */
|
|
27
|
+
sourceToTargetMap: Map<string, string>;
|
|
28
|
+
/** Map from source paths to file IDs */
|
|
29
|
+
sourceToIdMap: Map<string, string>;
|
|
30
|
+
/** Base paths to strip from final URLs (e.g., "src/content/docs") */
|
|
31
|
+
stripPrefixes: string[];
|
|
32
|
+
/** Custom handlers for special link types */
|
|
33
|
+
customHandlers?: LinkHandler[];
|
|
34
|
+
/** Path mappings for common transformations */
|
|
35
|
+
linkMappings?: LinkMapping[];
|
|
36
|
+
/** Logger for debug output */
|
|
37
|
+
logger?: Logger;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Custom handler for specific link patterns
|
|
42
|
+
*/
|
|
43
|
+
export interface LinkHandler {
|
|
44
|
+
/** Test if this handler should process the link */
|
|
45
|
+
test: (link: string, context: LinkContext) => boolean;
|
|
46
|
+
/** Transform the link */
|
|
47
|
+
transform: (link: string, context: LinkContext) => string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Context for individual link transformation
|
|
52
|
+
*/
|
|
53
|
+
interface LinkContext {
|
|
54
|
+
/** The file containing the link */
|
|
55
|
+
currentFile: ImportedFile;
|
|
56
|
+
/** The original link text */
|
|
57
|
+
originalLink: string;
|
|
58
|
+
/** Any anchor/fragment in the link */
|
|
59
|
+
anchor: string;
|
|
60
|
+
/** Global context */
|
|
61
|
+
global: GlobalLinkContext;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract anchor fragment from a link
|
|
66
|
+
*/
|
|
67
|
+
function extractAnchor(link: string): { path: string; anchor: string } {
|
|
68
|
+
const anchorMatch = link.match(/#.*$/);
|
|
69
|
+
const anchor = anchorMatch ? anchorMatch[0] : '';
|
|
70
|
+
const path = link.replace(/#.*$/, '');
|
|
71
|
+
return { path, anchor };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if a link is external (should not be transformed)
|
|
76
|
+
* External links are left completely unchanged by all transformations
|
|
77
|
+
*/
|
|
78
|
+
function isExternalLink(link: string): boolean {
|
|
79
|
+
return (
|
|
80
|
+
// Common protocols
|
|
81
|
+
/^https?:\/\//.test(link) ||
|
|
82
|
+
/^mailto:/.test(link) ||
|
|
83
|
+
/^tel:/.test(link) ||
|
|
84
|
+
/^ftp:/.test(link) ||
|
|
85
|
+
/^ftps:\/\//.test(link) ||
|
|
86
|
+
|
|
87
|
+
// Any protocol with ://
|
|
88
|
+
link.includes('://') ||
|
|
89
|
+
|
|
90
|
+
// Anchor-only links (same page)
|
|
91
|
+
link.startsWith('#') ||
|
|
92
|
+
|
|
93
|
+
// Data URLs
|
|
94
|
+
/^data:/.test(link) ||
|
|
95
|
+
|
|
96
|
+
// File protocol
|
|
97
|
+
/^file:\/\//.test(link)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Normalize path separators and resolve relative paths
|
|
103
|
+
*/
|
|
104
|
+
function normalizePath(linkPath: string, currentFilePath: string, logger?: Logger): string {
|
|
105
|
+
logger?.debug(`[normalizePath] BEFORE: linkPath="${linkPath}", currentFilePath="${currentFilePath}"`);
|
|
106
|
+
|
|
107
|
+
// Handle relative paths
|
|
108
|
+
if (linkPath.startsWith('./') || linkPath.includes('../')) {
|
|
109
|
+
const currentDir = path.dirname(currentFilePath);
|
|
110
|
+
const resolved = path.posix.normalize(path.posix.join(currentDir, linkPath));
|
|
111
|
+
logger?.debug(`[normalizePath] RELATIVE PATH RESOLVED: "${linkPath}" -> "${resolved}" (currentDir: "${currentDir}")`);
|
|
112
|
+
return resolved;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Remove leading './'
|
|
116
|
+
if (linkPath.startsWith('./')) {
|
|
117
|
+
return linkPath.slice(2);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
logger?.debug(`[normalizePath] AFTER: "${linkPath}" (no changes)`);
|
|
121
|
+
return linkPath;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Apply link mappings to transform a URL
|
|
126
|
+
*/
|
|
127
|
+
function applyLinkMappings(
|
|
128
|
+
linkUrl: string,
|
|
129
|
+
linkMappings: LinkMapping[],
|
|
130
|
+
context: LinkContext
|
|
131
|
+
): string {
|
|
132
|
+
const { path: linkPath, anchor } = extractAnchor(linkUrl);
|
|
133
|
+
let transformedPath = linkPath;
|
|
134
|
+
|
|
135
|
+
for (const mapping of linkMappings) {
|
|
136
|
+
// Check if contextFilter allows this mapping to be applied
|
|
137
|
+
if (mapping.contextFilter && context.currentFile.linkContext) {
|
|
138
|
+
if (!mapping.contextFilter(context.currentFile.linkContext)) {
|
|
139
|
+
continue; // Skip this mapping
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Handle relative links automatically if enabled
|
|
144
|
+
if (mapping.relativeLinks && context.currentFile.linkContext) {
|
|
145
|
+
// Check if this is a relative link (doesn't start with /, http, etc.)
|
|
146
|
+
if (!linkPath.startsWith('/') && !isExternalLink(linkPath)) {
|
|
147
|
+
// Check if the link points to a known directory structure
|
|
148
|
+
const knownPaths = ['modules/', 'classes/', 'interfaces/', 'enums/'];
|
|
149
|
+
const isKnownPath = knownPaths.some(p => linkPath.startsWith(p));
|
|
150
|
+
|
|
151
|
+
if (isKnownPath) {
|
|
152
|
+
// Strip .md extension from the link path
|
|
153
|
+
const cleanLinkPath = linkPath.replace(/\.md$/, '');
|
|
154
|
+
|
|
155
|
+
// Convert relative path to absolute path using the target base
|
|
156
|
+
const targetBase = generateSiteUrl(context.currentFile.linkContext.basePath, context.global.stripPrefixes);
|
|
157
|
+
|
|
158
|
+
// Construct final URL with proper Starlight formatting
|
|
159
|
+
let finalUrl = targetBase.replace(/\/$/, '') + '/' + cleanLinkPath;
|
|
160
|
+
|
|
161
|
+
// Add trailing slash if it doesn't end with one and isn't empty
|
|
162
|
+
if (finalUrl && !finalUrl.endsWith('/')) {
|
|
163
|
+
finalUrl += '/';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
transformedPath = finalUrl;
|
|
167
|
+
return transformedPath + anchor;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let matched = false;
|
|
173
|
+
let replacement = '';
|
|
174
|
+
|
|
175
|
+
if (typeof mapping.pattern === 'string') {
|
|
176
|
+
// String pattern - exact match or contains
|
|
177
|
+
if (transformedPath.includes(mapping.pattern)) {
|
|
178
|
+
matched = true;
|
|
179
|
+
if (typeof mapping.replacement === 'string') {
|
|
180
|
+
replacement = transformedPath.replace(mapping.pattern, mapping.replacement);
|
|
181
|
+
} else {
|
|
182
|
+
replacement = mapping.replacement(transformedPath, anchor, context);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
// RegExp pattern
|
|
187
|
+
const match = transformedPath.match(mapping.pattern);
|
|
188
|
+
if (match) {
|
|
189
|
+
matched = true;
|
|
190
|
+
if (typeof mapping.replacement === 'string') {
|
|
191
|
+
replacement = transformedPath.replace(mapping.pattern, mapping.replacement);
|
|
192
|
+
} else {
|
|
193
|
+
replacement = mapping.replacement(transformedPath, anchor, context);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (matched) {
|
|
199
|
+
// Apply the transformation and continue with next mapping
|
|
200
|
+
transformedPath = replacement;
|
|
201
|
+
// Note: We continue applying other mappings to allow chaining
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return transformedPath + anchor;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Convert a target path to a site-compatible URL
|
|
210
|
+
*/
|
|
211
|
+
function generateSiteUrl(targetPath: string, stripPrefixes: string[]): string {
|
|
212
|
+
let url = targetPath;
|
|
213
|
+
|
|
214
|
+
// Strip configured prefixes
|
|
215
|
+
for (const prefix of stripPrefixes) {
|
|
216
|
+
if (url.startsWith(prefix)) {
|
|
217
|
+
url = url.slice(prefix.length);
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Remove leading slash if present
|
|
223
|
+
url = url.replace(/^\//, '');
|
|
224
|
+
|
|
225
|
+
// Remove file extension
|
|
226
|
+
url = url.replace(/\.(md|mdx)$/i, '');
|
|
227
|
+
|
|
228
|
+
// Handle index files - they should resolve to parent directory
|
|
229
|
+
if (url.endsWith('/index')) {
|
|
230
|
+
url = url.replace('/index', '');
|
|
231
|
+
} else if (url === 'index') {
|
|
232
|
+
url = '';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Split path into segments and slugify each
|
|
236
|
+
const segments = url.split('/').map(segment => segment ? slug(segment) : '');
|
|
237
|
+
|
|
238
|
+
// Reconstruct URL
|
|
239
|
+
url = segments.filter(s => s).join('/');
|
|
240
|
+
|
|
241
|
+
// Ensure leading slash
|
|
242
|
+
if (url && !url.startsWith('/')) {
|
|
243
|
+
url = '/' + url;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Add trailing slash for non-empty paths
|
|
247
|
+
if (url && !url.endsWith('/')) {
|
|
248
|
+
url = url + '/';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return url || '/';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Transform a single markdown link
|
|
256
|
+
*
|
|
257
|
+
* Processing order:
|
|
258
|
+
* 1. Skip external links (no transformation)
|
|
259
|
+
* 2. Normalize path relative to current file
|
|
260
|
+
* 3. Apply global path mappings to normalized path
|
|
261
|
+
* 4. Check if link targets imported file in sourceToTargetMap
|
|
262
|
+
* 5. Apply non-global path mappings if unresolved
|
|
263
|
+
* 6. Check custom handlers
|
|
264
|
+
*/
|
|
265
|
+
function transformLink(linkText: string, linkUrl: string, context: LinkContext): string {
|
|
266
|
+
// Skip external links FIRST - no transformations should ever be applied to them
|
|
267
|
+
if (isExternalLink(linkUrl)) {
|
|
268
|
+
return `[${linkText}](${linkUrl})`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const { path: linkPath, anchor } = extractAnchor(linkUrl);
|
|
272
|
+
|
|
273
|
+
// Normalize the link path relative to current file FIRST
|
|
274
|
+
const normalizedPath = normalizePath(linkPath, context.currentFile.sourcePath, context.global.logger);
|
|
275
|
+
|
|
276
|
+
// Apply global path mappings to the normalized path
|
|
277
|
+
let processedNormalizedPath = normalizedPath;
|
|
278
|
+
if (context.global.linkMappings) {
|
|
279
|
+
const globalMappings = context.global.linkMappings.filter(m => m.global);
|
|
280
|
+
if (globalMappings.length > 0) {
|
|
281
|
+
processedNormalizedPath = applyLinkMappings(normalizedPath + anchor, globalMappings, context);
|
|
282
|
+
// Extract path again after global mappings
|
|
283
|
+
const { path: newPath } = extractAnchor(processedNormalizedPath);
|
|
284
|
+
processedNormalizedPath = newPath;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Check if this links to an imported file
|
|
289
|
+
const targetPath = context.global.sourceToTargetMap.get(normalizedPath);
|
|
290
|
+
|
|
291
|
+
if (targetPath) {
|
|
292
|
+
// This is an internal link to an imported file
|
|
293
|
+
const siteUrl = generateSiteUrl(targetPath, context.global.stripPrefixes);
|
|
294
|
+
return `[${linkText}](${siteUrl}${anchor})`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Apply non-global path mappings to unresolved links
|
|
298
|
+
if (context.global.linkMappings) {
|
|
299
|
+
const nonGlobalMappings = context.global.linkMappings.filter(m => !m.global);
|
|
300
|
+
if (nonGlobalMappings.length > 0) {
|
|
301
|
+
const mappedUrl = applyLinkMappings(processedNormalizedPath + anchor, nonGlobalMappings, context);
|
|
302
|
+
if (mappedUrl !== (processedNormalizedPath + anchor)) {
|
|
303
|
+
return `[${linkText}](${mappedUrl})`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check custom handlers
|
|
309
|
+
if (context.global.customHandlers) {
|
|
310
|
+
for (const handler of context.global.customHandlers) {
|
|
311
|
+
const currentUrl = processedNormalizedPath + anchor;
|
|
312
|
+
if (handler.test(currentUrl, context)) {
|
|
313
|
+
const transformedUrl = handler.transform(currentUrl, context);
|
|
314
|
+
return `[${linkText}](${transformedUrl})`;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// No transformation needed - return processed URL
|
|
320
|
+
return `[${linkText}](${processedNormalizedPath + anchor})`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Global link transformation function
|
|
325
|
+
* Processes all imported files and resolves internal links
|
|
326
|
+
*/
|
|
327
|
+
export function globalLinkTransform(
|
|
328
|
+
importedFiles: ImportedFile[],
|
|
329
|
+
options: {
|
|
330
|
+
stripPrefixes: string[];
|
|
331
|
+
customHandlers?: LinkHandler[];
|
|
332
|
+
linkMappings?: LinkMapping[];
|
|
333
|
+
logger?: Logger;
|
|
334
|
+
}
|
|
335
|
+
): ImportedFile[] {
|
|
336
|
+
// Build global context
|
|
337
|
+
const sourceToTargetMap = new Map<string, string>();
|
|
338
|
+
const sourceToIdMap = new Map<string, string>();
|
|
339
|
+
|
|
340
|
+
for (const file of importedFiles) {
|
|
341
|
+
sourceToTargetMap.set(file.sourcePath, file.targetPath);
|
|
342
|
+
sourceToIdMap.set(file.sourcePath, file.id);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const globalContext: GlobalLinkContext = {
|
|
346
|
+
sourceToTargetMap,
|
|
347
|
+
sourceToIdMap,
|
|
348
|
+
stripPrefixes: options.stripPrefixes,
|
|
349
|
+
customHandlers: options.customHandlers,
|
|
350
|
+
linkMappings: options.linkMappings,
|
|
351
|
+
logger: options.logger,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Transform links in all files
|
|
355
|
+
const markdownLinkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
|
|
356
|
+
|
|
357
|
+
return importedFiles.map(file => ({
|
|
358
|
+
...file,
|
|
359
|
+
content: file.content.replace(markdownLinkRegex, (match, linkText, linkUrl) => {
|
|
360
|
+
const linkContext: LinkContext = {
|
|
361
|
+
currentFile: file,
|
|
362
|
+
originalLink: linkUrl,
|
|
363
|
+
anchor: extractAnchor(linkUrl).anchor,
|
|
364
|
+
global: globalContext,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
return transformLink(linkText, linkUrl, linkContext);
|
|
368
|
+
}),
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Infer cross-section path from basePath
|
|
375
|
+
* @param basePath - The base path from include pattern (e.g., 'src/content/docs/reference/api')
|
|
376
|
+
* @returns Inferred cross-section path (e.g., '/reference/api')
|
|
377
|
+
*/
|
|
378
|
+
function inferCrossSectionPath(basePath: string): string {
|
|
379
|
+
return basePath
|
|
380
|
+
.replace(/^src\/content\/docs/, '')
|
|
381
|
+
.replace(/\/$/, '') || '/';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Generate link mappings automatically from pathMappings in include patterns
|
|
386
|
+
* @param includes - Array of include patterns with pathMappings
|
|
387
|
+
* @param stripPrefixes - Prefixes to strip when generating URLs
|
|
388
|
+
* @returns Array of generated link mappings
|
|
389
|
+
*/
|
|
390
|
+
export function generateAutoLinkMappings(
|
|
391
|
+
includes: IncludePattern[],
|
|
392
|
+
stripPrefixes: string[] = []
|
|
393
|
+
): LinkMapping[] {
|
|
394
|
+
const linkMappings: LinkMapping[] = [];
|
|
395
|
+
|
|
396
|
+
for (const includePattern of includes) {
|
|
397
|
+
if (!includePattern.pathMappings) continue;
|
|
398
|
+
|
|
399
|
+
const inferredCrossSection = inferCrossSectionPath(includePattern.basePath);
|
|
400
|
+
|
|
401
|
+
for (const [sourcePath, mappingValue] of Object.entries(includePattern.pathMappings)) {
|
|
402
|
+
// Handle both string and enhanced object formats
|
|
403
|
+
const targetPath = typeof mappingValue === 'string' ? mappingValue : mappingValue.target;
|
|
404
|
+
const crossSectionPath = typeof mappingValue === 'object' && mappingValue.crossSectionPath
|
|
405
|
+
? mappingValue.crossSectionPath
|
|
406
|
+
: inferredCrossSection;
|
|
407
|
+
|
|
408
|
+
if (sourcePath.endsWith('/')) {
|
|
409
|
+
// Folder mapping - use regex with capture group
|
|
410
|
+
const sourcePattern = sourcePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
411
|
+
|
|
412
|
+
linkMappings.push({
|
|
413
|
+
pattern: new RegExp(`^${sourcePattern}(.+)$`),
|
|
414
|
+
replacement: (transformedPath: string, anchor: string, context: any) => {
|
|
415
|
+
const relativePath = transformedPath.replace(new RegExp(`^${sourcePattern}`), '');
|
|
416
|
+
let finalPath: string;
|
|
417
|
+
if (crossSectionPath && crossSectionPath !== '/') {
|
|
418
|
+
finalPath = targetPath === ''
|
|
419
|
+
? `${crossSectionPath}/${relativePath}`
|
|
420
|
+
: `${crossSectionPath}/${targetPath}${relativePath}`;
|
|
421
|
+
} else {
|
|
422
|
+
finalPath = targetPath === '' ? relativePath : `${targetPath}${relativePath}`;
|
|
423
|
+
}
|
|
424
|
+
return generateSiteUrl(finalPath, stripPrefixes);
|
|
425
|
+
},
|
|
426
|
+
global: true,
|
|
427
|
+
});
|
|
428
|
+
} else {
|
|
429
|
+
// File mapping - exact string match
|
|
430
|
+
const sourcePattern = sourcePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
431
|
+
|
|
432
|
+
linkMappings.push({
|
|
433
|
+
pattern: new RegExp(`^${sourcePattern}$`),
|
|
434
|
+
replacement: (transformedPath: string, anchor: string, context: any) => {
|
|
435
|
+
const finalPath = crossSectionPath && crossSectionPath !== '/'
|
|
436
|
+
? `${crossSectionPath}/${targetPath}`
|
|
437
|
+
: targetPath;
|
|
438
|
+
return generateSiteUrl(finalPath, stripPrefixes);
|
|
439
|
+
},
|
|
440
|
+
global: true,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return linkMappings;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Export types for use in configuration
|
|
451
|
+
*/
|
|
452
|
+
export type { LinkContext, GlobalLinkContext };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { beforeEach, describe, it, expect } from "vitest";
|
|
2
|
+
import { githubLoader } from "./github.loader.js";
|
|
3
|
+
import { globalLinkTransform, type ImportedFile } from "./github.link-transform.js";
|
|
4
|
+
import { createLogger, type ImportSummary } from "./github.logger.js";
|
|
5
|
+
import { Octokit } from "octokit";
|
|
6
|
+
|
|
7
|
+
const FIXTURES = [
|
|
8
|
+
{
|
|
9
|
+
owner: "awesome-algorand",
|
|
10
|
+
repo: "algokit-cli",
|
|
11
|
+
ref: "docs/starlight-preview",
|
|
12
|
+
path: ".devportal/starlight",
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
describe("githubLoader", () => {
|
|
16
|
+
let octokit: Octokit;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should work", async () => {
|
|
22
|
+
const result = githubLoader({ octokit, configs: FIXTURES });
|
|
23
|
+
|
|
24
|
+
console.log(result);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("context-aware link transformations", () => {
|
|
28
|
+
it("should handle relative links from API files with contextFilter", () => {
|
|
29
|
+
const testFiles: ImportedFile[] = [
|
|
30
|
+
{
|
|
31
|
+
id: "api-readme",
|
|
32
|
+
sourcePath: "docs/code/README.md",
|
|
33
|
+
targetPath: "src/content/docs/reference/algokit-utils-ts/api/README.md",
|
|
34
|
+
content: 'Check out the [modules](modules/) for more info.',
|
|
35
|
+
linkContext: {
|
|
36
|
+
sourcePath: "docs/code/README.md",
|
|
37
|
+
targetPath: "src/content/docs/reference/algokit-utils-ts/api/README.md",
|
|
38
|
+
basePath: "src/content/docs/reference/algokit-utils-ts/api",
|
|
39
|
+
pathMappings: { "docs/code/": "" }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "modules-index",
|
|
44
|
+
sourcePath: "docs/code/modules/index.md",
|
|
45
|
+
targetPath: "src/content/docs/reference/algokit-utils-ts/api/modules/index.md",
|
|
46
|
+
content: 'This is the modules index.',
|
|
47
|
+
linkContext: {
|
|
48
|
+
sourcePath: "docs/code/modules/index.md",
|
|
49
|
+
targetPath: "src/content/docs/reference/algokit-utils-ts/api/modules/index.md",
|
|
50
|
+
basePath: "src/content/docs/reference/algokit-utils-ts/api",
|
|
51
|
+
pathMappings: { "docs/code/": "" }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const result = globalLinkTransform(testFiles, {
|
|
57
|
+
stripPrefixes: ['src/content/docs'],
|
|
58
|
+
linkMappings: [
|
|
59
|
+
{
|
|
60
|
+
contextFilter: (context) => context.sourcePath.startsWith('docs/code/'),
|
|
61
|
+
relativeLinks: true,
|
|
62
|
+
pattern: /.*/,
|
|
63
|
+
replacement: '',
|
|
64
|
+
global: false
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// The relative link `modules/` should be transformed to `/reference/algokit-utils-ts/api/modules/`
|
|
70
|
+
expect(result[0].content).toContain('[modules](/reference/algokit-utils-ts/api/modules/)');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("logging system", () => {
|
|
75
|
+
it("should create logger with different levels", () => {
|
|
76
|
+
const silentLogger = createLogger('silent');
|
|
77
|
+
const defaultLogger = createLogger('default');
|
|
78
|
+
const verboseLogger = createLogger('verbose');
|
|
79
|
+
const debugLogger = createLogger('debug');
|
|
80
|
+
|
|
81
|
+
expect(silentLogger.getLevel()).toBe('silent');
|
|
82
|
+
expect(defaultLogger.getLevel()).toBe('default');
|
|
83
|
+
expect(verboseLogger.getLevel()).toBe('verbose');
|
|
84
|
+
expect(debugLogger.getLevel()).toBe('debug');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should format import summary correctly", () => {
|
|
88
|
+
const logger = createLogger('default');
|
|
89
|
+
const summary: ImportSummary = {
|
|
90
|
+
configName: 'Test Config',
|
|
91
|
+
repository: 'test/repo',
|
|
92
|
+
ref: 'main',
|
|
93
|
+
filesProcessed: 10,
|
|
94
|
+
filesUpdated: 5,
|
|
95
|
+
filesUnchanged: 5,
|
|
96
|
+
assetsDownloaded: 3,
|
|
97
|
+
assetsCached: 2,
|
|
98
|
+
duration: 1500,
|
|
99
|
+
status: 'success'
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// This test mainly verifies the types work correctly
|
|
103
|
+
expect(() => logger.logImportSummary(summary)).not.toThrow();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|