@larkiny/astro-github-loader 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,17 +35,15 @@ function isExternalLink(link) {
35
35
  */
36
36
  function normalizePath(linkPath, currentFilePath, logger) {
37
37
  logger?.debug(`[normalizePath] BEFORE: linkPath="${linkPath}", currentFilePath="${currentFilePath}"`);
38
- // Handle relative paths
39
- if (linkPath.startsWith('./') || linkPath.includes('../')) {
38
+ // Handle relative paths (including simple relative paths without ./ prefix)
39
+ // A link is relative if it doesn't start with / or contain a protocol
40
+ const isAbsoluteOrExternal = linkPath.startsWith('/') || linkPath.includes('://') || linkPath.startsWith('#');
41
+ if (!isAbsoluteOrExternal) {
40
42
  const currentDir = path.dirname(currentFilePath);
41
43
  const resolved = path.posix.normalize(path.posix.join(currentDir, linkPath));
42
44
  logger?.debug(`[normalizePath] RELATIVE PATH RESOLVED: "${linkPath}" -> "${resolved}" (currentDir: "${currentDir}")`);
43
45
  return resolved;
44
46
  }
45
- // Remove leading './'
46
- if (linkPath.startsWith('./')) {
47
- return linkPath.slice(2);
48
- }
49
47
  logger?.debug(`[normalizePath] AFTER: "${linkPath}" (no changes)`);
50
48
  return linkPath;
51
49
  }
@@ -188,7 +186,11 @@ function transformLink(linkText, linkUrl, context) {
188
186
  }
189
187
  }
190
188
  // Check if this links to an imported file
191
- const targetPath = context.global.sourceToTargetMap.get(normalizedPath);
189
+ let targetPath = context.global.sourceToTargetMap.get(normalizedPath);
190
+ // If not found and path ends with /, try looking for index.md
191
+ if (!targetPath && normalizedPath.endsWith('/')) {
192
+ targetPath = context.global.sourceToTargetMap.get(normalizedPath + 'index.md');
193
+ }
192
194
  if (targetPath) {
193
195
  // This is an internal link to an imported file
194
196
  const siteUrl = generateSiteUrl(targetPath, context.global.stripPrefixes);
@@ -214,8 +216,10 @@ function transformLink(linkText, linkUrl, context) {
214
216
  }
215
217
  }
216
218
  }
217
- // No transformation needed - return processed URL
218
- return `[${linkText}](${processedNormalizedPath + anchor})`;
219
+ // No transformation matched - strip .md extension from unresolved internal links
220
+ // This handles links to files that weren't imported but should still use Starlight routing
221
+ const cleanPath = processedNormalizedPath.replace(/\.md$/i, '');
222
+ return `[${linkText}](${cleanPath + anchor})`;
219
223
  }
220
224
  /**
221
225
  * Global link transformation function
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,222 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { globalLinkTransform } from './github.link-transform.js';
3
+ describe('globalLinkTransform', () => {
4
+ describe('relative link resolution', () => {
5
+ it('should resolve simple relative links without ./ prefix', () => {
6
+ const files = [
7
+ {
8
+ sourcePath: 'docs/front-end-guide/index.md',
9
+ targetPath: 'src/content/docs/reference/api/front-end-guide/overview.md',
10
+ content: '[Designing a language](02-designing-a-language.md)',
11
+ id: 'file1',
12
+ },
13
+ {
14
+ sourcePath: 'docs/front-end-guide/02-designing-a-language.md',
15
+ targetPath: 'src/content/docs/reference/api/front-end-guide/02-designing-a-language.md',
16
+ content: '# Designing a language',
17
+ id: 'file2',
18
+ },
19
+ ];
20
+ const result = globalLinkTransform(files, {
21
+ stripPrefixes: ['src/content/docs'],
22
+ linkMappings: [],
23
+ });
24
+ expect(result[0].content).toBe('[Designing a language](/reference/api/front-end-guide/02-designing-a-language/)');
25
+ });
26
+ it('should resolve relative links with ./ prefix', () => {
27
+ const files = [
28
+ {
29
+ sourcePath: 'docs/front-end-guide/index.md',
30
+ targetPath: 'src/content/docs/reference/api/front-end-guide/overview.md',
31
+ content: '[Intro](./00-introduction.md)',
32
+ id: 'file1',
33
+ },
34
+ {
35
+ sourcePath: 'docs/front-end-guide/00-introduction.md',
36
+ targetPath: 'src/content/docs/reference/api/front-end-guide/00-introduction.md',
37
+ content: '# Introduction',
38
+ id: 'file2',
39
+ },
40
+ ];
41
+ const result = globalLinkTransform(files, {
42
+ stripPrefixes: ['src/content/docs'],
43
+ linkMappings: [],
44
+ });
45
+ expect(result[0].content).toBe('[Intro](/reference/api/front-end-guide/00-introduction/)');
46
+ });
47
+ it('should resolve relative links with ../ prefix', () => {
48
+ const files = [
49
+ {
50
+ sourcePath: 'docs/front-end-guide/subfolder/page.md',
51
+ targetPath: 'src/content/docs/reference/api/front-end-guide/subfolder/page.md',
52
+ content: '[Back to overview](../index.md)',
53
+ id: 'file1',
54
+ },
55
+ {
56
+ sourcePath: 'docs/front-end-guide/index.md',
57
+ targetPath: 'src/content/docs/reference/api/front-end-guide/overview.md',
58
+ content: '# Overview',
59
+ id: 'file2',
60
+ },
61
+ ];
62
+ const result = globalLinkTransform(files, {
63
+ stripPrefixes: ['src/content/docs'],
64
+ linkMappings: [],
65
+ });
66
+ expect(result[0].content).toBe('[Back to overview](/reference/api/front-end-guide/overview/)');
67
+ });
68
+ it('should preserve absolute links', () => {
69
+ const files = [
70
+ {
71
+ sourcePath: 'docs/page.md',
72
+ targetPath: 'src/content/docs/page.md',
73
+ content: '[Absolute link](/some/absolute/path)',
74
+ id: 'file1',
75
+ },
76
+ ];
77
+ const result = globalLinkTransform(files, {
78
+ stripPrefixes: ['src/content/docs'],
79
+ linkMappings: [],
80
+ });
81
+ expect(result[0].content).toBe('[Absolute link](/some/absolute/path)');
82
+ });
83
+ it('should preserve external links', () => {
84
+ const files = [
85
+ {
86
+ sourcePath: 'docs/page.md',
87
+ targetPath: 'src/content/docs/page.md',
88
+ content: '[External](https://example.com)',
89
+ id: 'file1',
90
+ },
91
+ ];
92
+ const result = globalLinkTransform(files, {
93
+ stripPrefixes: ['src/content/docs'],
94
+ linkMappings: [],
95
+ });
96
+ expect(result[0].content).toBe('[External](https://example.com)');
97
+ });
98
+ it('should preserve anchor-only links', () => {
99
+ const files = [
100
+ {
101
+ sourcePath: 'docs/page.md',
102
+ targetPath: 'src/content/docs/page.md',
103
+ content: '[Jump to section](#my-section)',
104
+ id: 'file1',
105
+ },
106
+ ];
107
+ const result = globalLinkTransform(files, {
108
+ stripPrefixes: ['src/content/docs'],
109
+ linkMappings: [],
110
+ });
111
+ expect(result[0].content).toBe('[Jump to section](#my-section)');
112
+ });
113
+ it('should preserve anchors in relative links', () => {
114
+ const files = [
115
+ {
116
+ sourcePath: 'docs/front-end-guide/index.md',
117
+ targetPath: 'src/content/docs/reference/api/front-end-guide/overview.md',
118
+ content: '[Section](02-designing-a-language.md#primitive-types)',
119
+ id: 'file1',
120
+ },
121
+ {
122
+ sourcePath: 'docs/front-end-guide/02-designing-a-language.md',
123
+ targetPath: 'src/content/docs/reference/api/front-end-guide/02-designing-a-language.md',
124
+ content: '# Designing a language',
125
+ id: 'file2',
126
+ },
127
+ ];
128
+ const result = globalLinkTransform(files, {
129
+ stripPrefixes: ['src/content/docs'],
130
+ linkMappings: [],
131
+ });
132
+ expect(result[0].content).toBe('[Section](/reference/api/front-end-guide/02-designing-a-language/#primitive-types)');
133
+ });
134
+ it('should handle multiple links in the same file', () => {
135
+ const files = [
136
+ {
137
+ sourcePath: 'docs/front-end-guide/index.md',
138
+ targetPath: 'src/content/docs/reference/api/front-end-guide/overview.md',
139
+ content: `
140
+ [Introduction](00-introduction.md)
141
+ [Calling puya](01-calling-puya.md)
142
+ [Designing a language](02-designing-a-language.md)
143
+ `.trim(),
144
+ id: 'file1',
145
+ },
146
+ {
147
+ sourcePath: 'docs/front-end-guide/00-introduction.md',
148
+ targetPath: 'src/content/docs/reference/api/front-end-guide/00-introduction.md',
149
+ content: '# Introduction',
150
+ id: 'file2',
151
+ },
152
+ {
153
+ sourcePath: 'docs/front-end-guide/01-calling-puya.md',
154
+ targetPath: 'src/content/docs/reference/api/front-end-guide/01-calling-puya.md',
155
+ content: '# Calling Puya',
156
+ id: 'file3',
157
+ },
158
+ {
159
+ sourcePath: 'docs/front-end-guide/02-designing-a-language.md',
160
+ targetPath: 'src/content/docs/reference/api/front-end-guide/02-designing-a-language.md',
161
+ content: '# Designing a language',
162
+ id: 'file4',
163
+ },
164
+ ];
165
+ const result = globalLinkTransform(files, {
166
+ stripPrefixes: ['src/content/docs'],
167
+ linkMappings: [],
168
+ });
169
+ expect(result[0].content).toBe(`
170
+ [Introduction](/reference/api/front-end-guide/00-introduction/)
171
+ [Calling puya](/reference/api/front-end-guide/01-calling-puya/)
172
+ [Designing a language](/reference/api/front-end-guide/02-designing-a-language/)
173
+ `.trim());
174
+ });
175
+ it('should strip .md extension from unresolved relative links', () => {
176
+ const files = [
177
+ {
178
+ sourcePath: 'docs/page.md',
179
+ targetPath: 'src/content/docs/page.md',
180
+ content: '[Unresolved](some-file-not-in-map.md)',
181
+ id: 'file1',
182
+ },
183
+ ];
184
+ const result = globalLinkTransform(files, {
185
+ stripPrefixes: ['src/content/docs'],
186
+ linkMappings: [],
187
+ });
188
+ // Should normalize to docs/some-file-not-in-map (current file's dir + relative link)
189
+ // but not resolve to full path since file not in map, so just strips .md
190
+ expect(result[0].content).toBe('[Unresolved](docs/some-file-not-in-map)');
191
+ });
192
+ });
193
+ describe('index.md handling', () => {
194
+ it('should convert index.md to trailing slash', () => {
195
+ const files = [
196
+ {
197
+ sourcePath: 'docs/page.md',
198
+ targetPath: 'src/content/docs/page.md',
199
+ content: '[Link](subfolder/index.md)',
200
+ id: 'file1',
201
+ },
202
+ {
203
+ sourcePath: 'docs/subfolder/index.md',
204
+ targetPath: 'src/content/docs/subfolder/index.md',
205
+ content: '# Index',
206
+ id: 'file2',
207
+ },
208
+ ];
209
+ const result = globalLinkTransform(files, {
210
+ stripPrefixes: ['src/content/docs'],
211
+ linkMappings: [
212
+ {
213
+ pattern: /\/index$/,
214
+ replacement: '/',
215
+ global: true,
216
+ },
217
+ ],
218
+ });
219
+ expect(result[0].content).toBe('[Link](/subfolder/)');
220
+ });
221
+ });
222
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@larkiny/astro-github-loader",
3
3
  "type": "module",
4
- "version": "0.11.0",
4
+ "version": "0.11.2",
5
5
  "description": "Load content from GitHub repositories into Astro content collections with asset management and content transformations",
6
6
  "keywords": [
7
7
  "astro",
@@ -0,0 +1 @@
1
+ # Content
@@ -0,0 +1 @@
1
+ # Content
@@ -0,0 +1 @@
1
+ # Content
@@ -0,0 +1 @@
1
+ # Content
@@ -0,0 +1 @@
1
+ # Content
@@ -104,19 +104,17 @@ function isExternalLink(link: string): boolean {
104
104
  function normalizePath(linkPath: string, currentFilePath: string, logger?: Logger): string {
105
105
  logger?.debug(`[normalizePath] BEFORE: linkPath="${linkPath}", currentFilePath="${currentFilePath}"`);
106
106
 
107
- // Handle relative paths
108
- if (linkPath.startsWith('./') || linkPath.includes('../')) {
107
+ // Handle relative paths (including simple relative paths without ./ prefix)
108
+ // A link is relative if it doesn't start with / or contain a protocol
109
+ const isAbsoluteOrExternal = linkPath.startsWith('/') || linkPath.includes('://') || linkPath.startsWith('#');
110
+
111
+ if (!isAbsoluteOrExternal) {
109
112
  const currentDir = path.dirname(currentFilePath);
110
113
  const resolved = path.posix.normalize(path.posix.join(currentDir, linkPath));
111
114
  logger?.debug(`[normalizePath] RELATIVE PATH RESOLVED: "${linkPath}" -> "${resolved}" (currentDir: "${currentDir}")`);
112
115
  return resolved;
113
116
  }
114
117
 
115
- // Remove leading './'
116
- if (linkPath.startsWith('./')) {
117
- return linkPath.slice(2);
118
- }
119
-
120
118
  logger?.debug(`[normalizePath] AFTER: "${linkPath}" (no changes)`);
121
119
  return linkPath;
122
120
  }
@@ -286,7 +284,12 @@ function transformLink(linkText: string, linkUrl: string, context: LinkContext):
286
284
  }
287
285
 
288
286
  // Check if this links to an imported file
289
- const targetPath = context.global.sourceToTargetMap.get(normalizedPath);
287
+ let targetPath = context.global.sourceToTargetMap.get(normalizedPath);
288
+
289
+ // If not found and path ends with /, try looking for index.md
290
+ if (!targetPath && normalizedPath.endsWith('/')) {
291
+ targetPath = context.global.sourceToTargetMap.get(normalizedPath + 'index.md');
292
+ }
290
293
 
291
294
  if (targetPath) {
292
295
  // This is an internal link to an imported file
@@ -316,8 +319,10 @@ function transformLink(linkText: string, linkUrl: string, context: LinkContext):
316
319
  }
317
320
  }
318
321
 
319
- // No transformation needed - return processed URL
320
- return `[${linkText}](${processedNormalizedPath + anchor})`;
322
+ // No transformation matched - strip .md extension from unresolved internal links
323
+ // This handles links to files that weren't imported but should still use Starlight routing
324
+ const cleanPath = processedNormalizedPath.replace(/\.md$/i, '');
325
+ return `[${linkText}](${cleanPath + anchor})`;
321
326
  }
322
327
 
323
328
  /**