@larkiny/astro-github-loader 0.11.3 → 0.13.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/README.md +35 -55
- package/dist/github.assets.d.ts +70 -0
- package/dist/github.assets.js +253 -0
- package/dist/github.auth.js +13 -9
- package/dist/github.cleanup.d.ts +3 -2
- package/dist/github.cleanup.js +30 -23
- package/dist/github.constants.d.ts +0 -16
- package/dist/github.constants.js +0 -16
- package/dist/github.content.d.ts +5 -131
- package/dist/github.content.js +152 -794
- package/dist/github.dryrun.d.ts +9 -5
- package/dist/github.dryrun.js +49 -25
- package/dist/github.link-transform.d.ts +2 -2
- package/dist/github.link-transform.js +68 -57
- package/dist/github.loader.js +30 -46
- package/dist/github.logger.d.ts +2 -2
- package/dist/github.logger.js +33 -24
- package/dist/github.paths.d.ts +76 -0
- package/dist/github.paths.js +190 -0
- package/dist/github.storage.d.ts +16 -0
- package/dist/github.storage.js +115 -0
- package/dist/github.types.d.ts +40 -4
- package/dist/index.d.ts +8 -6
- package/dist/index.js +3 -6
- package/dist/test-helpers.d.ts +130 -0
- package/dist/test-helpers.js +194 -0
- package/package.json +3 -1
- package/src/github.assets.spec.ts +717 -0
- package/src/github.assets.ts +365 -0
- package/src/github.auth.spec.ts +245 -0
- package/src/github.auth.ts +24 -10
- package/src/github.cleanup.spec.ts +380 -0
- package/src/github.cleanup.ts +91 -47
- package/src/github.constants.ts +0 -17
- package/src/github.content.spec.ts +305 -454
- package/src/github.content.ts +259 -957
- package/src/github.dryrun.spec.ts +598 -0
- package/src/github.dryrun.ts +108 -54
- package/src/github.link-transform.spec.ts +1345 -0
- package/src/github.link-transform.ts +177 -95
- package/src/github.loader.spec.ts +75 -50
- package/src/github.loader.ts +101 -76
- package/src/github.logger.spec.ts +795 -0
- package/src/github.logger.ts +77 -35
- package/src/github.paths.spec.ts +523 -0
- package/src/github.paths.ts +259 -0
- package/src/github.storage.spec.ts +377 -0
- package/src/github.storage.ts +135 -0
- package/src/github.types.ts +54 -9
- package/src/index.ts +43 -6
- package/src/test-helpers.ts +215 -0
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { slug } from
|
|
2
|
-
import path from
|
|
3
|
-
import type {
|
|
4
|
-
|
|
1
|
+
import { slug } from "github-slugger";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
LinkMapping,
|
|
5
|
+
LinkTransformContext,
|
|
6
|
+
MatchedPattern,
|
|
7
|
+
IncludePattern,
|
|
8
|
+
PathMappingValue,
|
|
9
|
+
EnhancedPathMapping,
|
|
10
|
+
} from "./github.types.js";
|
|
11
|
+
import type { Logger } from "./github.logger.js";
|
|
5
12
|
|
|
6
13
|
/**
|
|
7
14
|
* Represents an imported file with its content and metadata
|
|
@@ -66,8 +73,8 @@ interface LinkContext {
|
|
|
66
73
|
*/
|
|
67
74
|
function extractAnchor(link: string): { path: string; anchor: string } {
|
|
68
75
|
const anchorMatch = link.match(/#.*$/);
|
|
69
|
-
const anchor = anchorMatch ? anchorMatch[0] :
|
|
70
|
-
const path = link.replace(/#.*$/,
|
|
76
|
+
const anchor = anchorMatch ? anchorMatch[0] : "";
|
|
77
|
+
const path = link.replace(/#.*$/, "");
|
|
71
78
|
return { path, anchor };
|
|
72
79
|
}
|
|
73
80
|
|
|
@@ -83,16 +90,12 @@ function isExternalLink(link: string): boolean {
|
|
|
83
90
|
/^tel:/.test(link) ||
|
|
84
91
|
/^ftp:/.test(link) ||
|
|
85
92
|
/^ftps:\/\//.test(link) ||
|
|
86
|
-
|
|
87
93
|
// Any protocol with ://
|
|
88
|
-
link.includes(
|
|
89
|
-
|
|
94
|
+
link.includes("://") ||
|
|
90
95
|
// Anchor-only links (same page)
|
|
91
|
-
link.startsWith(
|
|
92
|
-
|
|
96
|
+
link.startsWith("#") ||
|
|
93
97
|
// Data URLs
|
|
94
98
|
/^data:/.test(link) ||
|
|
95
|
-
|
|
96
99
|
// File protocol
|
|
97
100
|
/^file:\/\//.test(link)
|
|
98
101
|
);
|
|
@@ -101,17 +104,30 @@ function isExternalLink(link: string): boolean {
|
|
|
101
104
|
/**
|
|
102
105
|
* Normalize path separators and resolve relative paths
|
|
103
106
|
*/
|
|
104
|
-
function normalizePath(
|
|
105
|
-
|
|
107
|
+
function normalizePath(
|
|
108
|
+
linkPath: string,
|
|
109
|
+
currentFilePath: string,
|
|
110
|
+
logger?: Logger,
|
|
111
|
+
): string {
|
|
112
|
+
logger?.debug(
|
|
113
|
+
`[normalizePath] BEFORE: linkPath="${linkPath}", currentFilePath="${currentFilePath}"`,
|
|
114
|
+
);
|
|
106
115
|
|
|
107
116
|
// Handle relative paths (including simple relative paths without ./ prefix)
|
|
108
117
|
// A link is relative if it doesn't start with / or contain a protocol
|
|
109
|
-
const isAbsoluteOrExternal =
|
|
118
|
+
const isAbsoluteOrExternal =
|
|
119
|
+
linkPath.startsWith("/") ||
|
|
120
|
+
linkPath.includes("://") ||
|
|
121
|
+
linkPath.startsWith("#");
|
|
110
122
|
|
|
111
123
|
if (!isAbsoluteOrExternal) {
|
|
112
124
|
const currentDir = path.dirname(currentFilePath);
|
|
113
|
-
const resolved = path.posix.normalize(
|
|
114
|
-
|
|
125
|
+
const resolved = path.posix.normalize(
|
|
126
|
+
path.posix.join(currentDir, linkPath),
|
|
127
|
+
);
|
|
128
|
+
logger?.debug(
|
|
129
|
+
`[normalizePath] RELATIVE PATH RESOLVED: "${linkPath}" -> "${resolved}" (currentDir: "${currentDir}")`,
|
|
130
|
+
);
|
|
115
131
|
return resolved;
|
|
116
132
|
}
|
|
117
133
|
|
|
@@ -125,7 +141,7 @@ function normalizePath(linkPath: string, currentFilePath: string, logger?: Logge
|
|
|
125
141
|
function applyLinkMappings(
|
|
126
142
|
linkUrl: string,
|
|
127
143
|
linkMappings: LinkMapping[],
|
|
128
|
-
context: LinkContext
|
|
144
|
+
context: LinkContext,
|
|
129
145
|
): string {
|
|
130
146
|
const { path: linkPath, anchor } = extractAnchor(linkUrl);
|
|
131
147
|
let transformedPath = linkPath;
|
|
@@ -141,24 +157,27 @@ function applyLinkMappings(
|
|
|
141
157
|
// Handle relative links automatically if enabled
|
|
142
158
|
if (mapping.relativeLinks && context.currentFile.linkContext) {
|
|
143
159
|
// Check if this is a relative link (doesn't start with /, http, etc.)
|
|
144
|
-
if (!linkPath.startsWith(
|
|
160
|
+
if (!linkPath.startsWith("/") && !isExternalLink(linkPath)) {
|
|
145
161
|
// Check if the link points to a known directory structure
|
|
146
|
-
const knownPaths = [
|
|
147
|
-
const isKnownPath = knownPaths.some(p => linkPath.startsWith(p));
|
|
162
|
+
const knownPaths = ["modules/", "classes/", "interfaces/", "enums/"];
|
|
163
|
+
const isKnownPath = knownPaths.some((p) => linkPath.startsWith(p));
|
|
148
164
|
|
|
149
165
|
if (isKnownPath) {
|
|
150
166
|
// Strip .md extension from the link path
|
|
151
|
-
const cleanLinkPath = linkPath.replace(/\.md$/,
|
|
167
|
+
const cleanLinkPath = linkPath.replace(/\.md$/, "");
|
|
152
168
|
|
|
153
169
|
// Convert relative path to absolute path using the target base
|
|
154
|
-
const targetBase = generateSiteUrl(
|
|
170
|
+
const targetBase = generateSiteUrl(
|
|
171
|
+
context.currentFile.linkContext.basePath,
|
|
172
|
+
context.global.stripPrefixes,
|
|
173
|
+
);
|
|
155
174
|
|
|
156
175
|
// Construct final URL with proper Starlight formatting
|
|
157
|
-
let finalUrl = targetBase.replace(/\/$/,
|
|
176
|
+
let finalUrl = targetBase.replace(/\/$/, "") + "/" + cleanLinkPath;
|
|
158
177
|
|
|
159
178
|
// Add trailing slash if it doesn't end with one and isn't empty
|
|
160
|
-
if (finalUrl && !finalUrl.endsWith(
|
|
161
|
-
finalUrl +=
|
|
179
|
+
if (finalUrl && !finalUrl.endsWith("/")) {
|
|
180
|
+
finalUrl += "/";
|
|
162
181
|
}
|
|
163
182
|
|
|
164
183
|
transformedPath = finalUrl;
|
|
@@ -168,16 +187,30 @@ function applyLinkMappings(
|
|
|
168
187
|
}
|
|
169
188
|
|
|
170
189
|
let matched = false;
|
|
171
|
-
let replacement =
|
|
190
|
+
let replacement = "";
|
|
191
|
+
|
|
192
|
+
const getLinkTransformContext = (): LinkTransformContext =>
|
|
193
|
+
context.currentFile.linkContext ?? {
|
|
194
|
+
sourcePath: context.currentFile.sourcePath,
|
|
195
|
+
targetPath: context.currentFile.targetPath,
|
|
196
|
+
basePath: "",
|
|
197
|
+
};
|
|
172
198
|
|
|
173
|
-
if (typeof mapping.pattern ===
|
|
199
|
+
if (typeof mapping.pattern === "string") {
|
|
174
200
|
// String pattern - exact match or contains
|
|
175
201
|
if (transformedPath.includes(mapping.pattern)) {
|
|
176
202
|
matched = true;
|
|
177
|
-
if (typeof mapping.replacement ===
|
|
178
|
-
replacement = transformedPath.replace(
|
|
203
|
+
if (typeof mapping.replacement === "string") {
|
|
204
|
+
replacement = transformedPath.replace(
|
|
205
|
+
mapping.pattern,
|
|
206
|
+
mapping.replacement,
|
|
207
|
+
);
|
|
179
208
|
} else {
|
|
180
|
-
replacement = mapping.replacement(
|
|
209
|
+
replacement = mapping.replacement(
|
|
210
|
+
transformedPath,
|
|
211
|
+
anchor,
|
|
212
|
+
getLinkTransformContext(),
|
|
213
|
+
);
|
|
181
214
|
}
|
|
182
215
|
}
|
|
183
216
|
} else {
|
|
@@ -185,10 +218,17 @@ function applyLinkMappings(
|
|
|
185
218
|
const match = transformedPath.match(mapping.pattern);
|
|
186
219
|
if (match) {
|
|
187
220
|
matched = true;
|
|
188
|
-
if (typeof mapping.replacement ===
|
|
189
|
-
replacement = transformedPath.replace(
|
|
221
|
+
if (typeof mapping.replacement === "string") {
|
|
222
|
+
replacement = transformedPath.replace(
|
|
223
|
+
mapping.pattern,
|
|
224
|
+
mapping.replacement,
|
|
225
|
+
);
|
|
190
226
|
} else {
|
|
191
|
-
replacement = mapping.replacement(
|
|
227
|
+
replacement = mapping.replacement(
|
|
228
|
+
transformedPath,
|
|
229
|
+
anchor,
|
|
230
|
+
getLinkTransformContext(),
|
|
231
|
+
);
|
|
192
232
|
}
|
|
193
233
|
}
|
|
194
234
|
}
|
|
@@ -218,35 +258,37 @@ function generateSiteUrl(targetPath: string, stripPrefixes: string[]): string {
|
|
|
218
258
|
}
|
|
219
259
|
|
|
220
260
|
// Remove leading slash if present
|
|
221
|
-
url = url.replace(/^\//,
|
|
261
|
+
url = url.replace(/^\//, "");
|
|
222
262
|
|
|
223
263
|
// Remove file extension
|
|
224
|
-
url = url.replace(/\.(md|mdx)$/i,
|
|
264
|
+
url = url.replace(/\.(md|mdx)$/i, "");
|
|
225
265
|
|
|
226
266
|
// Handle index files - they should resolve to parent directory
|
|
227
|
-
if (url.endsWith(
|
|
228
|
-
url = url.replace(
|
|
229
|
-
} else if (url ===
|
|
230
|
-
url =
|
|
267
|
+
if (url.endsWith("/index")) {
|
|
268
|
+
url = url.replace("/index", "");
|
|
269
|
+
} else if (url === "index") {
|
|
270
|
+
url = "";
|
|
231
271
|
}
|
|
232
272
|
|
|
233
273
|
// Split path into segments and slugify each
|
|
234
|
-
const segments = url
|
|
274
|
+
const segments = url
|
|
275
|
+
.split("/")
|
|
276
|
+
.map((segment) => (segment ? slug(segment) : ""));
|
|
235
277
|
|
|
236
278
|
// Reconstruct URL
|
|
237
|
-
url = segments.filter(s => s).join(
|
|
279
|
+
url = segments.filter((s) => s).join("/");
|
|
238
280
|
|
|
239
281
|
// Ensure leading slash
|
|
240
|
-
if (url && !url.startsWith(
|
|
241
|
-
url =
|
|
282
|
+
if (url && !url.startsWith("/")) {
|
|
283
|
+
url = "/" + url;
|
|
242
284
|
}
|
|
243
285
|
|
|
244
286
|
// Add trailing slash for non-empty paths
|
|
245
|
-
if (url && !url.endsWith(
|
|
246
|
-
url = url +
|
|
287
|
+
if (url && !url.endsWith("/")) {
|
|
288
|
+
url = url + "/";
|
|
247
289
|
}
|
|
248
290
|
|
|
249
|
-
return url ||
|
|
291
|
+
return url || "/";
|
|
250
292
|
}
|
|
251
293
|
|
|
252
294
|
/**
|
|
@@ -260,7 +302,11 @@ function generateSiteUrl(targetPath: string, stripPrefixes: string[]): string {
|
|
|
260
302
|
* 5. Apply non-global path mappings if unresolved
|
|
261
303
|
* 6. Check custom handlers
|
|
262
304
|
*/
|
|
263
|
-
function transformLink(
|
|
305
|
+
function transformLink(
|
|
306
|
+
linkText: string,
|
|
307
|
+
linkUrl: string,
|
|
308
|
+
context: LinkContext,
|
|
309
|
+
): string {
|
|
264
310
|
// Skip external links FIRST - no transformations should ever be applied to them
|
|
265
311
|
if (isExternalLink(linkUrl)) {
|
|
266
312
|
return `[${linkText}](${linkUrl})`;
|
|
@@ -269,14 +315,22 @@ function transformLink(linkText: string, linkUrl: string, context: LinkContext):
|
|
|
269
315
|
const { path: linkPath, anchor } = extractAnchor(linkUrl);
|
|
270
316
|
|
|
271
317
|
// Normalize the link path relative to current file FIRST
|
|
272
|
-
const normalizedPath = normalizePath(
|
|
318
|
+
const normalizedPath = normalizePath(
|
|
319
|
+
linkPath,
|
|
320
|
+
context.currentFile.sourcePath,
|
|
321
|
+
context.global.logger,
|
|
322
|
+
);
|
|
273
323
|
|
|
274
324
|
// Apply global path mappings to the normalized path
|
|
275
325
|
let processedNormalizedPath = normalizedPath;
|
|
276
326
|
if (context.global.linkMappings) {
|
|
277
|
-
const globalMappings = context.global.linkMappings.filter(m => m.global);
|
|
327
|
+
const globalMappings = context.global.linkMappings.filter((m) => m.global);
|
|
278
328
|
if (globalMappings.length > 0) {
|
|
279
|
-
processedNormalizedPath = applyLinkMappings(
|
|
329
|
+
processedNormalizedPath = applyLinkMappings(
|
|
330
|
+
normalizedPath + anchor,
|
|
331
|
+
globalMappings,
|
|
332
|
+
context,
|
|
333
|
+
);
|
|
280
334
|
// Extract path again after global mappings
|
|
281
335
|
const { path: newPath } = extractAnchor(processedNormalizedPath);
|
|
282
336
|
processedNormalizedPath = newPath;
|
|
@@ -287,8 +341,10 @@ function transformLink(linkText: string, linkUrl: string, context: LinkContext):
|
|
|
287
341
|
let targetPath = context.global.sourceToTargetMap.get(normalizedPath);
|
|
288
342
|
|
|
289
343
|
// If not found and path ends with /, try looking for index.md
|
|
290
|
-
if (!targetPath && normalizedPath.endsWith(
|
|
291
|
-
targetPath = context.global.sourceToTargetMap.get(
|
|
344
|
+
if (!targetPath && normalizedPath.endsWith("/")) {
|
|
345
|
+
targetPath = context.global.sourceToTargetMap.get(
|
|
346
|
+
normalizedPath + "index.md",
|
|
347
|
+
);
|
|
292
348
|
}
|
|
293
349
|
|
|
294
350
|
if (targetPath) {
|
|
@@ -299,10 +355,16 @@ function transformLink(linkText: string, linkUrl: string, context: LinkContext):
|
|
|
299
355
|
|
|
300
356
|
// Apply non-global path mappings to unresolved links
|
|
301
357
|
if (context.global.linkMappings) {
|
|
302
|
-
const nonGlobalMappings = context.global.linkMappings.filter(
|
|
358
|
+
const nonGlobalMappings = context.global.linkMappings.filter(
|
|
359
|
+
(m) => !m.global,
|
|
360
|
+
);
|
|
303
361
|
if (nonGlobalMappings.length > 0) {
|
|
304
|
-
const mappedUrl = applyLinkMappings(
|
|
305
|
-
|
|
362
|
+
const mappedUrl = applyLinkMappings(
|
|
363
|
+
processedNormalizedPath + anchor,
|
|
364
|
+
nonGlobalMappings,
|
|
365
|
+
context,
|
|
366
|
+
);
|
|
367
|
+
if (mappedUrl !== processedNormalizedPath + anchor) {
|
|
306
368
|
return `[${linkText}](${mappedUrl})`;
|
|
307
369
|
}
|
|
308
370
|
}
|
|
@@ -321,7 +383,7 @@ function transformLink(linkText: string, linkUrl: string, context: LinkContext):
|
|
|
321
383
|
|
|
322
384
|
// No transformation matched - strip .md extension from unresolved internal links
|
|
323
385
|
// This handles links to files that weren't imported but should still use Starlight routing
|
|
324
|
-
const cleanPath = processedNormalizedPath.replace(/\.md$/i,
|
|
386
|
+
const cleanPath = processedNormalizedPath.replace(/\.md$/i, "");
|
|
325
387
|
return `[${linkText}](${cleanPath + anchor})`;
|
|
326
388
|
}
|
|
327
389
|
|
|
@@ -336,7 +398,7 @@ export function globalLinkTransform(
|
|
|
336
398
|
customHandlers?: LinkHandler[];
|
|
337
399
|
linkMappings?: LinkMapping[];
|
|
338
400
|
logger?: Logger;
|
|
339
|
-
}
|
|
401
|
+
},
|
|
340
402
|
): ImportedFile[] {
|
|
341
403
|
// Build global context
|
|
342
404
|
const sourceToTargetMap = new Map<string, string>();
|
|
@@ -359,31 +421,31 @@ export function globalLinkTransform(
|
|
|
359
421
|
// Transform links in all files
|
|
360
422
|
const markdownLinkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
|
|
361
423
|
|
|
362
|
-
return importedFiles.map(file => ({
|
|
424
|
+
return importedFiles.map((file) => ({
|
|
363
425
|
...file,
|
|
364
|
-
content: file.content.replace(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
426
|
+
content: file.content.replace(
|
|
427
|
+
markdownLinkRegex,
|
|
428
|
+
(match, linkText, linkUrl) => {
|
|
429
|
+
const linkContext: LinkContext = {
|
|
430
|
+
currentFile: file,
|
|
431
|
+
originalLink: linkUrl,
|
|
432
|
+
anchor: extractAnchor(linkUrl).anchor,
|
|
433
|
+
global: globalContext,
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
return transformLink(linkText, linkUrl, linkContext);
|
|
437
|
+
},
|
|
438
|
+
),
|
|
374
439
|
}));
|
|
375
440
|
}
|
|
376
441
|
|
|
377
|
-
|
|
378
442
|
/**
|
|
379
443
|
* Infer cross-section path from basePath
|
|
380
444
|
* @param basePath - The base path from include pattern (e.g., 'src/content/docs/reference/api')
|
|
381
445
|
* @returns Inferred cross-section path (e.g., '/reference/api')
|
|
382
446
|
*/
|
|
383
447
|
function inferCrossSectionPath(basePath: string): string {
|
|
384
|
-
return basePath
|
|
385
|
-
.replace(/^src\/content\/docs/, '')
|
|
386
|
-
.replace(/\/$/, '') || '/';
|
|
448
|
+
return basePath.replace(/^src\/content\/docs/, "").replace(/\/$/, "") || "/";
|
|
387
449
|
}
|
|
388
450
|
|
|
389
451
|
/**
|
|
@@ -394,7 +456,7 @@ function inferCrossSectionPath(basePath: string): string {
|
|
|
394
456
|
*/
|
|
395
457
|
export function generateAutoLinkMappings(
|
|
396
458
|
includes: IncludePattern[],
|
|
397
|
-
stripPrefixes: string[] = []
|
|
459
|
+
stripPrefixes: string[] = [],
|
|
398
460
|
): LinkMapping[] {
|
|
399
461
|
const linkMappings: LinkMapping[] = [];
|
|
400
462
|
|
|
@@ -403,28 +465,43 @@ export function generateAutoLinkMappings(
|
|
|
403
465
|
|
|
404
466
|
const inferredCrossSection = inferCrossSectionPath(includePattern.basePath);
|
|
405
467
|
|
|
406
|
-
for (const [sourcePath, mappingValue] of Object.entries(
|
|
468
|
+
for (const [sourcePath, mappingValue] of Object.entries(
|
|
469
|
+
includePattern.pathMappings,
|
|
470
|
+
)) {
|
|
407
471
|
// Handle both string and enhanced object formats
|
|
408
|
-
const targetPath =
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
472
|
+
const targetPath =
|
|
473
|
+
typeof mappingValue === "string" ? mappingValue : mappingValue.target;
|
|
474
|
+
const crossSectionPath =
|
|
475
|
+
typeof mappingValue === "object" && mappingValue.crossSectionPath
|
|
476
|
+
? mappingValue.crossSectionPath
|
|
477
|
+
: inferredCrossSection;
|
|
478
|
+
|
|
479
|
+
if (sourcePath.endsWith("/")) {
|
|
414
480
|
// Folder mapping - use regex with capture group
|
|
415
|
-
const sourcePattern = sourcePath.replace(/[.*+?^${}()|[\]\\]/g,
|
|
481
|
+
const sourcePattern = sourcePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
416
482
|
|
|
417
483
|
linkMappings.push({
|
|
418
484
|
pattern: new RegExp(`^${sourcePattern}(.+)$`),
|
|
419
|
-
replacement: (
|
|
420
|
-
|
|
485
|
+
replacement: (
|
|
486
|
+
transformedPath: string,
|
|
487
|
+
_anchor: string,
|
|
488
|
+
_context: LinkTransformContext,
|
|
489
|
+
) => {
|
|
490
|
+
const relativePath = transformedPath.replace(
|
|
491
|
+
new RegExp(`^${sourcePattern}`),
|
|
492
|
+
"",
|
|
493
|
+
);
|
|
421
494
|
let finalPath: string;
|
|
422
|
-
if (crossSectionPath && crossSectionPath !==
|
|
423
|
-
finalPath =
|
|
424
|
-
|
|
425
|
-
|
|
495
|
+
if (crossSectionPath && crossSectionPath !== "/") {
|
|
496
|
+
finalPath =
|
|
497
|
+
targetPath === ""
|
|
498
|
+
? `${crossSectionPath}/${relativePath}`
|
|
499
|
+
: `${crossSectionPath}/${targetPath}${relativePath}`;
|
|
426
500
|
} else {
|
|
427
|
-
finalPath =
|
|
501
|
+
finalPath =
|
|
502
|
+
targetPath === ""
|
|
503
|
+
? relativePath
|
|
504
|
+
: `${targetPath}${relativePath}`;
|
|
428
505
|
}
|
|
429
506
|
return generateSiteUrl(finalPath, stripPrefixes);
|
|
430
507
|
},
|
|
@@ -432,14 +509,19 @@ export function generateAutoLinkMappings(
|
|
|
432
509
|
});
|
|
433
510
|
} else {
|
|
434
511
|
// File mapping - exact string match
|
|
435
|
-
const sourcePattern = sourcePath.replace(/[.*+?^${}()|[\]\\]/g,
|
|
512
|
+
const sourcePattern = sourcePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
436
513
|
|
|
437
514
|
linkMappings.push({
|
|
438
515
|
pattern: new RegExp(`^${sourcePattern}$`),
|
|
439
|
-
replacement: (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
516
|
+
replacement: (
|
|
517
|
+
_transformedPath: string,
|
|
518
|
+
_anchor: string,
|
|
519
|
+
_context: LinkTransformContext,
|
|
520
|
+
) => {
|
|
521
|
+
const finalPath =
|
|
522
|
+
crossSectionPath && crossSectionPath !== "/"
|
|
523
|
+
? `${crossSectionPath}/${targetPath}`
|
|
524
|
+
: targetPath;
|
|
443
525
|
return generateSiteUrl(finalPath, stripPrefixes);
|
|
444
526
|
},
|
|
445
527
|
global: true,
|
|
@@ -454,4 +536,4 @@ export function generateAutoLinkMappings(
|
|
|
454
536
|
/**
|
|
455
537
|
* Export types for use in configuration
|
|
456
538
|
*/
|
|
457
|
-
export type { LinkContext, GlobalLinkContext };
|
|
539
|
+
export type { LinkContext, GlobalLinkContext };
|
|
@@ -1,27 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { githubLoader } from "./github.loader.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
globalLinkTransform,
|
|
5
|
+
type ImportedFile,
|
|
6
|
+
} from "./github.link-transform.js";
|
|
4
7
|
import { createLogger, type ImportSummary } from "./github.logger.js";
|
|
8
|
+
import type { ImportOptions, VersionConfig } from "./github.types.js";
|
|
5
9
|
import { Octokit } from "octokit";
|
|
6
10
|
|
|
7
|
-
const FIXTURES = [
|
|
8
|
-
{
|
|
9
|
-
owner: "awesome-algorand",
|
|
10
|
-
repo: "algokit-cli",
|
|
11
|
-
ref: "docs/starlight-preview",
|
|
12
|
-
path: ".devportal/starlight",
|
|
13
|
-
},
|
|
14
|
-
];
|
|
15
11
|
describe("githubLoader", () => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
it("should return a loader object", () => {
|
|
13
|
+
const octokit = new Octokit({ auth: "mock-token" });
|
|
14
|
+
const result = githubLoader({ octokit, configs: [] });
|
|
15
|
+
expect(result).toHaveProperty("name", "github-loader");
|
|
16
|
+
expect(result).toHaveProperty("load");
|
|
17
|
+
expect(typeof result.load).toBe("function");
|
|
19
18
|
});
|
|
20
19
|
|
|
21
|
-
it("should
|
|
22
|
-
const
|
|
20
|
+
it("should accept configs with language and versions fields", () => {
|
|
21
|
+
const octokit = new Octokit({ auth: "mock-token" });
|
|
22
|
+
const configs: ImportOptions[] = [
|
|
23
|
+
{
|
|
24
|
+
name: "AlgoKit Utils TS",
|
|
25
|
+
owner: "algorandfoundation",
|
|
26
|
+
repo: "algokit-utils-ts",
|
|
27
|
+
ref: "docs-dist",
|
|
28
|
+
language: "TypeScript",
|
|
29
|
+
versions: [
|
|
30
|
+
{ slug: "latest", label: "Latest" },
|
|
31
|
+
{ slug: "v8.0.0", label: "v8.0.0" },
|
|
32
|
+
],
|
|
33
|
+
includes: [
|
|
34
|
+
{
|
|
35
|
+
pattern: "docs/**/*.md",
|
|
36
|
+
basePath: "src/content/docs/docs/algokit-utils/typescript",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
];
|
|
23
41
|
|
|
24
|
-
|
|
42
|
+
// Should not throw when constructing the loader with new fields
|
|
43
|
+
const result = githubLoader({ octokit, configs });
|
|
44
|
+
expect(result).toHaveProperty("name", "github-loader");
|
|
25
45
|
});
|
|
26
46
|
|
|
27
47
|
describe("context-aware link transformations", () => {
|
|
@@ -30,76 +50,81 @@ describe("githubLoader", () => {
|
|
|
30
50
|
{
|
|
31
51
|
id: "api-readme",
|
|
32
52
|
sourcePath: "docs/code/README.md",
|
|
33
|
-
targetPath:
|
|
34
|
-
|
|
53
|
+
targetPath:
|
|
54
|
+
"src/content/docs/reference/algokit-utils-ts/api/README.md",
|
|
55
|
+
content: "Check out the [modules](modules/) for more info.",
|
|
35
56
|
linkContext: {
|
|
36
57
|
sourcePath: "docs/code/README.md",
|
|
37
|
-
targetPath:
|
|
58
|
+
targetPath:
|
|
59
|
+
"src/content/docs/reference/algokit-utils-ts/api/README.md",
|
|
38
60
|
basePath: "src/content/docs/reference/algokit-utils-ts/api",
|
|
39
|
-
pathMappings: { "docs/code/": "" }
|
|
40
|
-
}
|
|
61
|
+
pathMappings: { "docs/code/": "" },
|
|
62
|
+
},
|
|
41
63
|
},
|
|
42
64
|
{
|
|
43
65
|
id: "modules-index",
|
|
44
66
|
sourcePath: "docs/code/modules/index.md",
|
|
45
|
-
targetPath:
|
|
46
|
-
|
|
67
|
+
targetPath:
|
|
68
|
+
"src/content/docs/reference/algokit-utils-ts/api/modules/index.md",
|
|
69
|
+
content: "This is the modules index.",
|
|
47
70
|
linkContext: {
|
|
48
71
|
sourcePath: "docs/code/modules/index.md",
|
|
49
|
-
targetPath:
|
|
72
|
+
targetPath:
|
|
73
|
+
"src/content/docs/reference/algokit-utils-ts/api/modules/index.md",
|
|
50
74
|
basePath: "src/content/docs/reference/algokit-utils-ts/api",
|
|
51
|
-
pathMappings: { "docs/code/": "" }
|
|
52
|
-
}
|
|
53
|
-
}
|
|
75
|
+
pathMappings: { "docs/code/": "" },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
54
78
|
];
|
|
55
79
|
|
|
56
80
|
const result = globalLinkTransform(testFiles, {
|
|
57
|
-
stripPrefixes: [
|
|
81
|
+
stripPrefixes: ["src/content/docs"],
|
|
58
82
|
linkMappings: [
|
|
59
83
|
{
|
|
60
|
-
contextFilter: (context) =>
|
|
84
|
+
contextFilter: (context) =>
|
|
85
|
+
context.sourcePath.startsWith("docs/code/"),
|
|
61
86
|
relativeLinks: true,
|
|
62
87
|
pattern: /.*/,
|
|
63
|
-
replacement:
|
|
64
|
-
global: false
|
|
65
|
-
}
|
|
66
|
-
]
|
|
88
|
+
replacement: "",
|
|
89
|
+
global: false,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
67
92
|
});
|
|
68
93
|
|
|
69
|
-
|
|
70
|
-
|
|
94
|
+
expect(result[0].content).toContain(
|
|
95
|
+
"[modules](/reference/algokit-utils-ts/api/modules/)",
|
|
96
|
+
);
|
|
71
97
|
});
|
|
72
98
|
});
|
|
73
99
|
|
|
74
100
|
describe("logging system", () => {
|
|
75
101
|
it("should create logger with different levels", () => {
|
|
76
|
-
const silentLogger = createLogger(
|
|
77
|
-
const defaultLogger = createLogger(
|
|
78
|
-
const verboseLogger = createLogger(
|
|
79
|
-
const debugLogger = createLogger(
|
|
102
|
+
const silentLogger = createLogger("silent");
|
|
103
|
+
const defaultLogger = createLogger("default");
|
|
104
|
+
const verboseLogger = createLogger("verbose");
|
|
105
|
+
const debugLogger = createLogger("debug");
|
|
80
106
|
|
|
81
|
-
expect(silentLogger.getLevel()).toBe(
|
|
82
|
-
expect(defaultLogger.getLevel()).toBe(
|
|
83
|
-
expect(verboseLogger.getLevel()).toBe(
|
|
84
|
-
expect(debugLogger.getLevel()).toBe(
|
|
107
|
+
expect(silentLogger.getLevel()).toBe("silent");
|
|
108
|
+
expect(defaultLogger.getLevel()).toBe("default");
|
|
109
|
+
expect(verboseLogger.getLevel()).toBe("verbose");
|
|
110
|
+
expect(debugLogger.getLevel()).toBe("debug");
|
|
85
111
|
});
|
|
86
112
|
|
|
87
|
-
it("should format import summary
|
|
88
|
-
const logger = createLogger(
|
|
113
|
+
it("should format import summary without throwing", () => {
|
|
114
|
+
const logger = createLogger("default");
|
|
89
115
|
const summary: ImportSummary = {
|
|
90
|
-
configName:
|
|
91
|
-
repository:
|
|
92
|
-
ref:
|
|
116
|
+
configName: "Test Config",
|
|
117
|
+
repository: "test/repo",
|
|
118
|
+
ref: "main",
|
|
93
119
|
filesProcessed: 10,
|
|
94
120
|
filesUpdated: 5,
|
|
95
121
|
filesUnchanged: 5,
|
|
96
122
|
assetsDownloaded: 3,
|
|
97
123
|
assetsCached: 2,
|
|
98
124
|
duration: 1500,
|
|
99
|
-
status:
|
|
125
|
+
status: "success",
|
|
100
126
|
};
|
|
101
127
|
|
|
102
|
-
// This test mainly verifies the types work correctly
|
|
103
128
|
expect(() => logger.logImportSummary(summary)).not.toThrow();
|
|
104
129
|
});
|
|
105
130
|
});
|