@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.
Files changed (51) hide show
  1. package/README.md +35 -55
  2. package/dist/github.assets.d.ts +70 -0
  3. package/dist/github.assets.js +253 -0
  4. package/dist/github.auth.js +13 -9
  5. package/dist/github.cleanup.d.ts +3 -2
  6. package/dist/github.cleanup.js +30 -23
  7. package/dist/github.constants.d.ts +0 -16
  8. package/dist/github.constants.js +0 -16
  9. package/dist/github.content.d.ts +5 -131
  10. package/dist/github.content.js +152 -794
  11. package/dist/github.dryrun.d.ts +9 -5
  12. package/dist/github.dryrun.js +49 -25
  13. package/dist/github.link-transform.d.ts +2 -2
  14. package/dist/github.link-transform.js +68 -57
  15. package/dist/github.loader.js +30 -46
  16. package/dist/github.logger.d.ts +2 -2
  17. package/dist/github.logger.js +33 -24
  18. package/dist/github.paths.d.ts +76 -0
  19. package/dist/github.paths.js +190 -0
  20. package/dist/github.storage.d.ts +16 -0
  21. package/dist/github.storage.js +115 -0
  22. package/dist/github.types.d.ts +40 -4
  23. package/dist/index.d.ts +8 -6
  24. package/dist/index.js +3 -6
  25. package/dist/test-helpers.d.ts +130 -0
  26. package/dist/test-helpers.js +194 -0
  27. package/package.json +3 -1
  28. package/src/github.assets.spec.ts +717 -0
  29. package/src/github.assets.ts +365 -0
  30. package/src/github.auth.spec.ts +245 -0
  31. package/src/github.auth.ts +24 -10
  32. package/src/github.cleanup.spec.ts +380 -0
  33. package/src/github.cleanup.ts +91 -47
  34. package/src/github.constants.ts +0 -17
  35. package/src/github.content.spec.ts +305 -454
  36. package/src/github.content.ts +259 -957
  37. package/src/github.dryrun.spec.ts +598 -0
  38. package/src/github.dryrun.ts +108 -54
  39. package/src/github.link-transform.spec.ts +1345 -0
  40. package/src/github.link-transform.ts +177 -95
  41. package/src/github.loader.spec.ts +75 -50
  42. package/src/github.loader.ts +101 -76
  43. package/src/github.logger.spec.ts +795 -0
  44. package/src/github.logger.ts +77 -35
  45. package/src/github.paths.spec.ts +523 -0
  46. package/src/github.paths.ts +259 -0
  47. package/src/github.storage.spec.ts +377 -0
  48. package/src/github.storage.ts +135 -0
  49. package/src/github.types.ts +54 -9
  50. package/src/index.ts +43 -6
  51. package/src/test-helpers.ts +215 -0
@@ -1,157 +1,22 @@
1
1
  import { beforeEach, describe, it, expect, vi } from "vitest";
2
- import { toCollectionEntry } from "./github.content.js";
3
- import { Octokit } from "octokit";
4
- import type { ImportOptions } from "./github.types.js";
5
-
6
- /**
7
- * Test suite for Git Trees API optimization
8
- *
9
- * These tests verify that the new Git Trees API approach:
10
- * 1. Correctly discovers files matching include patterns
11
- * 2. Reduces API calls compared to recursive approach
12
- * 3. Works with the expected repository configuration
13
- *
14
- * These are unit tests with mocked API responses to test the optimization
15
- * logic without requiring network access.
16
- */
17
- describe("Git Trees API Optimization", () => {
18
- let octokit: Octokit;
19
-
20
- // Mock commit data
21
- const mockCommit = {
22
- sha: "abc123def456",
23
- commit: {
24
- tree: {
25
- sha: "tree123abc456"
26
- },
27
- message: "Test commit",
28
- author: {
29
- name: "Test Author",
30
- email: "test@example.com",
31
- date: "2024-01-01T00:00:00Z"
32
- },
33
- committer: {
34
- name: "Test Committer",
35
- email: "test@example.com",
36
- date: "2024-01-01T00:00:00Z"
37
- }
38
- }
39
- };
40
-
41
- // Mock tree data representing a repository structure similar to algokit-cli
42
- const mockTreeData = {
43
- sha: "tree123abc456",
44
- url: "https://api.github.com/repos/test/repo/git/trees/tree123abc456",
45
- tree: [
46
- {
47
- path: "docs/algokit.md",
48
- mode: "100644",
49
- type: "blob",
50
- sha: "file1sha",
51
- size: 1234,
52
- url: "https://api.github.com/repos/test/repo/git/blobs/file1sha"
53
- },
54
- {
55
- path: "docs/features",
56
- mode: "040000",
57
- type: "tree",
58
- sha: "dir1sha",
59
- url: "https://api.github.com/repos/test/repo/git/trees/dir1sha"
60
- },
61
- {
62
- path: "docs/features/accounts.md",
63
- mode: "100644",
64
- type: "blob",
65
- sha: "file2sha",
66
- size: 2345,
67
- url: "https://api.github.com/repos/test/repo/git/blobs/file2sha"
68
- },
69
- {
70
- path: "docs/features/tasks.md",
71
- mode: "100644",
72
- type: "blob",
73
- sha: "file3sha",
74
- size: 3456,
75
- url: "https://api.github.com/repos/test/repo/git/blobs/file3sha"
76
- },
77
- {
78
- path: "docs/features/generate.md",
79
- mode: "100644",
80
- type: "blob",
81
- sha: "file4sha",
82
- size: 4567,
83
- url: "https://api.github.com/repos/test/repo/git/blobs/file4sha"
84
- },
85
- {
86
- path: "docs/cli/index.md",
87
- mode: "100644",
88
- type: "blob",
89
- sha: "file5sha",
90
- size: 5678,
91
- url: "https://api.github.com/repos/test/repo/git/blobs/file5sha"
92
- },
93
- {
94
- path: "README.md",
95
- mode: "100644",
96
- type: "blob",
97
- sha: "file6sha",
98
- size: 678,
99
- url: "https://api.github.com/repos/test/repo/git/blobs/file6sha"
100
- },
101
- {
102
- path: "package.json",
103
- mode: "100644",
104
- type: "blob",
105
- sha: "file7sha",
106
- size: 789,
107
- url: "https://api.github.com/repos/test/repo/git/blobs/file7sha"
108
- }
109
- ],
110
- truncated: false
111
- };
2
+ import { toCollectionEntry, resolveAssetConfig } from "./github.content.js";
3
+ import type { ImportOptions, VersionConfig } from "./github.types.js";
4
+ import {
5
+ createMockContext,
6
+ createMockOctokit,
7
+ mockFetch,
8
+ } from "./test-helpers.js";
112
9
 
10
+ describe("Git Trees API Optimization", () => {
113
11
  beforeEach(() => {
114
- // Create Octokit instance
115
- octokit = new Octokit({ auth: "mock-token" });
116
-
117
- // Reset all mocks
118
12
  vi.restoreAllMocks();
119
13
  });
120
14
 
121
15
  describe("API call efficiency", () => {
122
16
  it("should use Git Trees API (2 calls) instead of recursive getContent (N calls)", async () => {
123
- // Mock the API calls
124
- const listCommitsMock = vi.spyOn(octokit.rest.repos, 'listCommits')
125
- .mockResolvedValue({
126
- data: [mockCommit],
127
- status: 200,
128
- url: '',
129
- headers: {}
130
- } as any);
131
-
132
- const getTreeMock = vi.spyOn(octokit.rest.git, 'getTree')
133
- .mockResolvedValue({
134
- data: mockTreeData,
135
- status: 200,
136
- url: '',
137
- headers: {}
138
- } as any);
139
-
140
- const getContentMock = vi.spyOn(octokit.rest.repos, 'getContent')
141
- .mockResolvedValue({
142
- data: [],
143
- status: 200,
144
- url: '',
145
- headers: {}
146
- } as any);
147
-
148
- // Mock fetch for file downloads
149
- global.fetch = vi.fn().mockResolvedValue({
150
- ok: true,
151
- status: 200,
152
- headers: new Headers(),
153
- text: async () => "# Test Content\n\nThis is test markdown content."
154
- } as any);
17
+ const { octokit, spies } = createMockOctokit();
18
+ mockFetch();
19
+ const ctx = createMockContext();
155
20
 
156
21
  const testConfig: ImportOptions = {
157
22
  name: "Test Repo",
@@ -166,108 +31,42 @@ describe("Git Trees API Optimization", () => {
166
31
  ],
167
32
  };
168
33
 
169
- // Create minimal mock context with Astro-specific components
170
- const mockStore = new Map();
171
- const mockContext = {
172
- store: {
173
- set: (entry: any) => mockStore.set(entry.id, entry),
174
- get: (id: string) => mockStore.get(id),
175
- clear: () => mockStore.clear(),
176
- entries: () => mockStore.entries(),
177
- keys: () => mockStore.keys(),
178
- values: () => mockStore.values(),
179
- },
180
- meta: new Map(),
181
- logger: {
182
- info: vi.fn(),
183
- warn: vi.fn(),
184
- error: vi.fn(),
185
- debug: vi.fn(),
186
- verbose: vi.fn(),
187
- logFileProcessing: vi.fn(),
188
- logImportSummary: vi.fn(),
189
- withSpinner: async (msg: string, fn: () => Promise<any>) => await fn(),
190
- getLevel: () => 'default',
191
- },
192
- config: {},
193
- entryTypes: new Map([
194
- ['.md', {
195
- getEntryInfo: async ({ contents, fileUrl }: any) => ({
196
- body: contents,
197
- data: {}
198
- })
199
- }]
200
- ]),
201
- generateDigest: (content: string) => {
202
- // Simple hash function for testing
203
- return content.length.toString();
204
- },
205
- parseData: async (data: any) => data,
206
- };
207
-
208
34
  await toCollectionEntry({
209
- context: mockContext as any,
35
+ context: ctx as any,
210
36
  octokit,
211
37
  options: testConfig,
212
38
  });
213
39
 
214
- // Verify Git Trees API is used
215
- expect(listCommitsMock).toHaveBeenCalledTimes(1);
216
- expect(listCommitsMock).toHaveBeenCalledWith(
40
+ expect(spies.listCommitsSpy).toHaveBeenCalledTimes(1);
41
+ expect(spies.listCommitsSpy).toHaveBeenCalledWith(
217
42
  expect.objectContaining({
218
43
  owner: "algorandfoundation",
219
44
  repo: "algokit-cli",
220
45
  sha: "chore/content-fix",
221
46
  per_page: 1,
222
- })
47
+ }),
223
48
  );
224
49
 
225
- expect(getTreeMock).toHaveBeenCalledTimes(1);
226
- expect(getTreeMock).toHaveBeenCalledWith(
50
+ expect(spies.getTreeSpy).toHaveBeenCalledTimes(1);
51
+ expect(spies.getTreeSpy).toHaveBeenCalledWith(
227
52
  expect.objectContaining({
228
53
  owner: "algorandfoundation",
229
54
  repo: "algokit-cli",
230
55
  tree_sha: "tree123abc456",
231
56
  recursive: "true",
232
- })
57
+ }),
233
58
  );
234
59
 
235
- // Verify getContent is NOT called (old recursive approach)
236
- expect(getContentMock).not.toHaveBeenCalled();
237
-
238
- console.log('✅ API Efficiency Test Results:');
239
- console.log(` - listCommits calls: ${listCommitsMock.mock.calls.length} (expected: 1)`);
240
- console.log(` - getTree calls: ${getTreeMock.mock.calls.length} (expected: 1)`);
241
- console.log(` - getContent calls: ${getContentMock.mock.calls.length} (expected: 0)`);
242
- console.log(` - Total API calls for discovery: ${listCommitsMock.mock.calls.length + getTreeMock.mock.calls.length}`);
243
- console.log(` - 🎉 Optimization achieved: 2 calls instead of potentially 10+ recursive calls`);
60
+ // getContent should NOT be called (old recursive approach)
61
+ expect(spies.getContentSpy).not.toHaveBeenCalled();
244
62
  });
245
63
  });
246
64
 
247
65
  describe("file filtering", () => {
248
66
  it("should correctly filter files matching the glob pattern", async () => {
249
- const listCommitsMock = vi.spyOn(octokit.rest.repos, 'listCommits')
250
- .mockResolvedValue({
251
- data: [mockCommit],
252
- status: 200,
253
- url: '',
254
- headers: {}
255
- } as any);
256
-
257
- const getTreeMock = vi.spyOn(octokit.rest.git, 'getTree')
258
- .mockResolvedValue({
259
- data: mockTreeData,
260
- status: 200,
261
- url: '',
262
- headers: {}
263
- } as any);
264
-
265
- global.fetch = vi.fn().mockResolvedValue({
266
- ok: true,
267
- status: 200,
268
- headers: new Headers(),
269
- text: async () => "# Test Content\n\nMockfile content."
270
- } as any);
67
+ const { octokit } = createMockOctokit();
68
+ mockFetch("# Test Content\n\nMockfile content.");
69
+ const ctx = createMockContext();
271
70
 
272
71
  const testConfig: ImportOptions = {
273
72
  name: "Test filtering",
@@ -282,239 +81,96 @@ describe("Git Trees API Optimization", () => {
282
81
  ],
283
82
  };
284
83
 
285
- const mockStore = new Map();
286
- const mockContext = {
287
- store: {
288
- set: (entry: any) => {
289
- mockStore.set(entry.id, entry);
290
- return entry;
291
- },
292
- get: (id: string) => mockStore.get(id),
293
- clear: () => mockStore.clear(),
294
- entries: () => mockStore.entries(),
295
- keys: () => mockStore.keys(),
296
- values: () => mockStore.values(),
297
- },
298
- meta: new Map(),
299
- logger: {
300
- info: vi.fn(),
301
- warn: vi.fn(),
302
- error: vi.fn(),
303
- debug: vi.fn(),
304
- verbose: vi.fn(),
305
- logFileProcessing: vi.fn(),
306
- logImportSummary: vi.fn(),
307
- withSpinner: async (msg: string, fn: () => Promise<any>) => await fn(),
308
- getLevel: () => 'default',
309
- },
310
- config: {},
311
- entryTypes: new Map([['.md', { getEntryInfo: async ({ contents }: any) => ({ body: contents, data: {} }) }]]),
312
- generateDigest: (content: string) => content.length.toString(),
313
- parseData: async (data: any) => data,
314
- };
315
-
316
84
  const stats = await toCollectionEntry({
317
- context: mockContext as any,
85
+ context: ctx as any,
318
86
  octokit,
319
87
  options: testConfig,
320
88
  });
321
89
 
322
- console.log('\n🔍 File Filtering Test Results:');
323
- console.log(` - Pattern: docs/{features/**/*.md,algokit.md}`);
324
- console.log(` - Files in tree: ${mockTreeData.tree.length}`);
325
- console.log(` - Files processed: ${stats.processed}`);
326
- console.log(` - Files matched: ${mockStore.size}`);
327
-
328
- // Based on our mock data, we should match:
329
- // - docs/algokit.md (explicit match)
330
- // - docs/features/accounts.md (matches features/**/*.md)
331
- // - docs/features/tasks.md (matches features/**/*.md)
332
- // - docs/features/generate.md (matches features/**/*.md)
333
- // Should NOT match:
334
- // - docs/cli/index.md (not in pattern)
335
- // - README.md (not in pattern)
336
- // - package.json (not in pattern)
337
-
338
- expect(stats.processed).toBe(4); // algokit.md + 3 features/*.md files
339
- expect(mockStore.size).toBe(4);
340
-
341
- // Verify correct files were stored
342
- const storedIds = Array.from(mockStore.keys());
343
- expect(storedIds).toContain('docs/algokit');
344
- expect(storedIds.some(id => id.includes('features'))).toBe(true);
345
- expect(storedIds).not.toContain('package');
346
- expect(storedIds).not.toContain('README');
90
+ // Should match: docs/algokit.md + 3 features/*.md files
91
+ expect(stats.processed).toBe(4);
92
+ expect(ctx._store.size).toBe(4);
93
+
94
+ const storedIds = Array.from(ctx._store.keys());
95
+ expect(storedIds).toContain("docs/algokit");
96
+ expect(storedIds.some((id) => id.includes("features"))).toBe(true);
97
+ expect(storedIds).not.toContain("package");
98
+ expect(storedIds).not.toContain("README");
347
99
  });
348
100
 
349
101
  it("should filter to match only specific file when pattern is exact", async () => {
350
- vi.spyOn(octokit.rest.repos, 'listCommits')
351
- .mockResolvedValue({ data: [mockCommit], status: 200, url: '', headers: {} } as any);
352
-
353
- vi.spyOn(octokit.rest.git, 'getTree')
354
- .mockResolvedValue({ data: mockTreeData, status: 200, url: '', headers: {} } as any);
355
-
356
- global.fetch = vi.fn().mockResolvedValue({
357
- ok: true,
358
- status: 200,
359
- headers: new Headers(),
360
- text: async () => "# Single File Content"
361
- } as any);
102
+ const { octokit } = createMockOctokit();
103
+ mockFetch("# Single File Content");
104
+ const ctx = createMockContext();
362
105
 
363
106
  const testConfig: ImportOptions = {
364
107
  name: "Exact match test",
365
108
  owner: "test",
366
109
  repo: "repo",
367
110
  ref: "main",
368
- includes: [{
369
- pattern: "docs/algokit.md", // Exact file
370
- basePath: "test-output",
371
- }],
372
- };
373
-
374
- const mockStore = new Map();
375
- const mockContext = {
376
- store: {
377
- set: (entry: any) => mockStore.set(entry.id, entry),
378
- get: (id: string) => mockStore.get(id),
379
- clear: () => mockStore.clear(),
380
- entries: () => mockStore.entries(),
381
- keys: () => mockStore.keys(),
382
- values: () => mockStore.values(),
383
- },
384
- meta: new Map(),
385
- logger: {
386
- info: vi.fn(),
387
- warn: vi.fn(),
388
- error: vi.fn(),
389
- debug: vi.fn(),
390
- verbose: vi.fn(),
391
- logFileProcessing: vi.fn(),
392
- logImportSummary: vi.fn(),
393
- withSpinner: async (msg: string, fn: () => Promise<any>) => await fn(),
394
- getLevel: () => 'default',
395
- },
396
- config: {},
397
- entryTypes: new Map([['.md', { getEntryInfo: async ({ contents }: any) => ({ body: contents, data: {} }) }]]),
398
- generateDigest: (content: string) => content.length.toString(),
399
- parseData: async (data: any) => data,
111
+ includes: [
112
+ {
113
+ pattern: "docs/algokit.md",
114
+ basePath: "test-output",
115
+ },
116
+ ],
400
117
  };
401
118
 
402
119
  const stats = await toCollectionEntry({
403
- context: mockContext as any,
120
+ context: ctx as any,
404
121
  octokit,
405
122
  options: testConfig,
406
123
  });
407
124
 
408
- console.log('\n🎯 Exact Pattern Match Test:');
409
- console.log(` - Pattern: docs/algokit.md`);
410
- console.log(` - Files processed: ${stats.processed}`);
411
- console.log(` - Expected: 1 file`);
412
-
413
125
  expect(stats.processed).toBe(1);
414
- expect(mockStore.size).toBe(1);
415
- expect(Array.from(mockStore.keys())[0]).toContain('algokit');
126
+ expect(ctx._store.size).toBe(1);
127
+ expect(Array.from(ctx._store.keys())[0]).toContain("algokit");
416
128
  });
417
129
  });
418
130
 
419
131
  describe("download URL construction", () => {
420
132
  it("should construct valid raw.githubusercontent.com URLs from tree data", async () => {
421
- vi.spyOn(octokit.rest.repos, 'listCommits')
422
- .mockResolvedValue({ data: [mockCommit], status: 200, url: '', headers: {} } as any);
423
-
424
- vi.spyOn(octokit.rest.git, 'getTree')
425
- .mockResolvedValue({ data: mockTreeData, status: 200, url: '', headers: {} } as any);
426
-
427
- const fetchMock = vi.fn().mockResolvedValue({
428
- ok: true,
429
- status: 200,
430
- headers: new Headers(),
431
- text: async () => "# Content"
432
- } as any);
433
- global.fetch = fetchMock;
133
+ const { octokit } = createMockOctokit();
134
+ const fetchMock = mockFetch("# Content");
135
+ const ctx = createMockContext();
434
136
 
435
137
  const testConfig: ImportOptions = {
436
138
  name: "URL test",
437
139
  owner: "algorandfoundation",
438
140
  repo: "algokit-cli",
439
141
  ref: "chore/content-fix",
440
- includes: [{
441
- pattern: "docs/algokit.md",
442
- basePath: "test-output",
443
- }],
444
- };
445
-
446
- const mockStore = new Map();
447
- const mockContext = {
448
- store: {
449
- set: (entry: any) => mockStore.set(entry.id, entry),
450
- get: (id: string) => mockStore.get(id),
451
- clear: () => mockStore.clear(),
452
- entries: () => mockStore.entries(),
453
- keys: () => mockStore.keys(),
454
- values: () => mockStore.values(),
455
- },
456
- meta: new Map(),
457
- logger: {
458
- info: vi.fn(),
459
- warn: vi.fn(),
460
- error: vi.fn(),
461
- debug: vi.fn(),
462
- verbose: vi.fn(),
463
- logFileProcessing: vi.fn(),
464
- logImportSummary: vi.fn(),
465
- withSpinner: async (msg: string, fn: () => Promise<any>) => await fn(),
466
- getLevel: () => 'default',
467
- },
468
- config: {},
469
- entryTypes: new Map([['.md', { getEntryInfo: async ({ contents }: any) => ({ body: contents, data: {} }) }]]),
470
- generateDigest: (content: string) => content.length.toString(),
471
- parseData: async (data: any) => data,
142
+ includes: [
143
+ {
144
+ pattern: "docs/algokit.md",
145
+ basePath: "test-output",
146
+ },
147
+ ],
472
148
  };
473
149
 
474
150
  await toCollectionEntry({
475
- context: mockContext as any,
151
+ context: ctx as any,
476
152
  octokit,
477
153
  options: testConfig,
478
154
  });
479
155
 
480
- // Find fetch calls to raw.githubusercontent.com
481
- const rawGithubCalls = fetchMock.mock.calls.filter(call => {
482
- const url = call[0]?.toString() || '';
483
- return url.includes('raw.githubusercontent.com');
156
+ const rawGithubCalls = fetchMock.mock.calls.filter((call) => {
157
+ const url = call[0]?.toString() || "";
158
+ return url.includes("raw.githubusercontent.com");
484
159
  });
485
160
 
486
- console.log('\n🔗 URL Construction Test:');
487
- console.log(` - Total fetch calls: ${fetchMock.mock.calls.length}`);
488
- console.log(` - Calls to raw.githubusercontent.com: ${rawGithubCalls.length}`);
489
-
490
161
  expect(rawGithubCalls.length).toBeGreaterThan(0);
491
-
492
- const exampleUrl = rawGithubCalls[0][0]?.toString();
493
- console.log(` - Example URL: ${exampleUrl}`);
494
-
495
- // Verify URL format: https://raw.githubusercontent.com/{owner}/{repo}/{commit_sha}/{file_path}
496
- expect(exampleUrl).toMatch(
497
- /^https:\/\/raw\.githubusercontent\.com\/algorandfoundation\/algokit-cli\/abc123def456\/docs\/algokit\.md$/
162
+ expect(rawGithubCalls[0][0]?.toString()).toMatch(
163
+ /^https:\/\/raw\.githubusercontent\.com\/algorandfoundation\/algokit-cli\/abc123def456\/docs\/algokit\.md$/,
498
164
  );
499
165
  });
500
166
  });
501
167
 
502
168
  describe("real-world config simulation", () => {
503
169
  it("should handle the production algokit-cli config pattern correctly", async () => {
504
- vi.spyOn(octokit.rest.repos, 'listCommits')
505
- .mockResolvedValue({ data: [mockCommit], status: 200, url: '', headers: {} } as any);
170
+ const { octokit } = createMockOctokit();
171
+ mockFetch("# Content");
172
+ const ctx = createMockContext();
506
173
 
507
- vi.spyOn(octokit.rest.git, 'getTree')
508
- .mockResolvedValue({ data: mockTreeData, status: 200, url: '', headers: {} } as any);
509
-
510
- global.fetch = vi.fn().mockResolvedValue({
511
- ok: true,
512
- status: 200,
513
- headers: new Headers(),
514
- text: async () => "# Content"
515
- } as any);
516
-
517
- // This is the actual production config from content.config.ts
518
174
  const productionConfig: ImportOptions = {
519
175
  name: "AlgoKit CLI Docs",
520
176
  owner: "algorandfoundation",
@@ -539,61 +195,256 @@ describe("Git Trees API Optimization", () => {
539
195
  ],
540
196
  };
541
197
 
542
- const mockStore = new Map();
543
- const mockContext = {
544
- store: {
545
- set: (entry: any) => mockStore.set(entry.id, entry),
546
- get: (id: string) => mockStore.get(id),
547
- clear: () => mockStore.clear(),
548
- entries: () => mockStore.entries(),
549
- keys: () => mockStore.keys(),
550
- values: () => mockStore.values(),
551
- },
552
- meta: new Map(),
553
- logger: {
554
- info: vi.fn(),
555
- warn: vi.fn(),
556
- error: vi.fn(),
557
- debug: vi.fn(),
558
- verbose: vi.fn(),
559
- logFileProcessing: vi.fn(),
560
- logImportSummary: vi.fn(),
561
- withSpinner: async (msg: string, fn: () => Promise<any>) => await fn(),
562
- getLevel: () => 'default',
563
- },
564
- config: {},
565
- entryTypes: new Map([['.md', { getEntryInfo: async ({ contents }: any) => ({ body: contents, data: {} }) }]]),
566
- generateDigest: (content: string) => content.length.toString(),
567
- parseData: async (data: any) => data,
568
- };
569
-
570
198
  const stats = await toCollectionEntry({
571
- context: mockContext as any,
199
+ context: ctx as any,
572
200
  octokit,
573
201
  options: productionConfig,
574
202
  });
575
203
 
576
- console.log('\n📋 Production Config Test:');
577
- console.log(` - Pattern 1: docs/{features/**/*.md,algokit.md}`);
578
- console.log(` - Pattern 2: docs/cli/index.md`);
579
- console.log(` - Files processed: ${stats.processed}`);
580
- console.log(` - Expected files:`);
581
- console.log(` • docs/algokit.md → overview.md (from pattern 1)`);
582
- console.log(` • docs/features/accounts.md → accounts.md (from pattern 1)`);
583
- console.log(` • docs/features/tasks.md → tasks.md (from pattern 1)`);
584
- console.log(` • docs/features/generate.md → generate.md (from pattern 1)`);
585
- console.log(` • docs/cli/index.md → index.md (from pattern 2)`);
586
-
587
- // Should match 4 files from pattern 1 + 1 file from pattern 2
204
+ // 4 files from pattern 1 + 1 file from pattern 2
588
205
  expect(stats.processed).toBe(5);
589
206
 
590
- const storedIds = Array.from(mockStore.keys());
591
- console.log(` - Stored IDs:`, storedIds);
207
+ const storedIds = Array.from(ctx._store.keys());
208
+ expect(storedIds.some((id) => id.includes("overview"))).toBe(true);
209
+ expect(storedIds.filter((id) => id.includes("features")).length).toBe(3);
210
+ expect(
211
+ storedIds.some((id) => id.includes("cli") && id.includes("index")),
212
+ ).toBe(true);
213
+ });
214
+ });
215
+
216
+ describe("ImportOptions new fields (language, versions)", () => {
217
+ it("should accept language and versions fields without errors", async () => {
218
+ const { octokit } = createMockOctokit();
219
+ mockFetch("# Content with versioned config");
220
+ const ctx = createMockContext();
221
+
222
+ const testConfig: ImportOptions = {
223
+ name: "AlgoKit Utils TS",
224
+ owner: "algorandfoundation",
225
+ repo: "algokit-utils-ts",
226
+ ref: "docs-dist",
227
+ language: "TypeScript",
228
+ versions: [
229
+ { slug: "latest", label: "Latest" },
230
+ { slug: "v8.0.0", label: "v8.0.0" },
231
+ ],
232
+ includes: [
233
+ {
234
+ pattern: "docs/algokit.md",
235
+ basePath: "test-output",
236
+ },
237
+ ],
238
+ };
239
+
240
+ const stats = await toCollectionEntry({
241
+ context: ctx as any,
242
+ octokit,
243
+ options: testConfig,
244
+ });
245
+
246
+ expect(stats.processed).toBe(1);
247
+ expect(ctx._store.size).toBe(1);
248
+ });
249
+
250
+ it("should make language and versions accessible in transform context", async () => {
251
+ const { octokit } = createMockOctokit();
252
+ mockFetch("# Content to transform");
253
+ const ctx = createMockContext();
254
+
255
+ let capturedOptions: ImportOptions | undefined;
256
+
257
+ const testConfig: ImportOptions = {
258
+ name: "Transform context test",
259
+ owner: "test",
260
+ repo: "repo",
261
+ ref: "main",
262
+ language: "Python",
263
+ versions: [{ slug: "latest", label: "Latest" }],
264
+ includes: [
265
+ {
266
+ pattern: "docs/algokit.md",
267
+ basePath: "test-output",
268
+ },
269
+ ],
270
+ transforms: [
271
+ (content, context) => {
272
+ capturedOptions = context.options;
273
+ return content;
274
+ },
275
+ ],
276
+ };
277
+
278
+ await toCollectionEntry({
279
+ context: ctx as any,
280
+ octokit,
281
+ options: testConfig,
282
+ });
283
+
284
+ expect(capturedOptions).toBeDefined();
285
+ expect(capturedOptions!.language).toBe("Python");
286
+ expect(capturedOptions!.versions).toEqual([
287
+ { slug: "latest", label: "Latest" },
288
+ ]);
289
+ });
290
+
291
+ it("should work without language and versions (backward compatible)", async () => {
292
+ const { octokit } = createMockOctokit();
293
+ mockFetch("# Content without new fields");
294
+ const ctx = createMockContext();
295
+
296
+ const testConfig: ImportOptions = {
297
+ name: "No new fields",
298
+ owner: "test",
299
+ repo: "repo",
300
+ ref: "main",
301
+ includes: [
302
+ {
303
+ pattern: "docs/algokit.md",
304
+ basePath: "test-output",
305
+ },
306
+ ],
307
+ };
308
+
309
+ const stats = await toCollectionEntry({
310
+ context: ctx as any,
311
+ octokit,
312
+ options: testConfig,
313
+ });
314
+
315
+ expect(stats.processed).toBe(1);
316
+ });
317
+ });
318
+ });
319
+
320
+ describe("resolveAssetConfig", () => {
321
+ it("should return explicit assetsPath and assetsBaseUrl when both are provided", () => {
322
+ const options: ImportOptions = {
323
+ owner: "test",
324
+ repo: "repo",
325
+ assetsPath: "src/assets/custom",
326
+ assetsBaseUrl: "/assets/custom",
327
+ includes: [
328
+ {
329
+ pattern: "docs/**/*.md",
330
+ basePath: "src/content/docs/lib",
331
+ },
332
+ ],
333
+ };
334
+
335
+ const result = resolveAssetConfig(options, "docs/guide.md");
336
+
337
+ expect(result).toEqual({
338
+ assetsPath: "src/assets/custom",
339
+ assetsBaseUrl: "/assets/custom",
340
+ });
341
+ });
342
+
343
+ it("should derive co-located defaults from basePath when assetsPath/assetsBaseUrl are omitted", () => {
344
+ const options: ImportOptions = {
345
+ owner: "test",
346
+ repo: "repo",
347
+ includes: [
348
+ {
349
+ pattern: "docs/**/*.md",
350
+ basePath: "src/content/docs/algokit-utils/typescript/v8.0.0",
351
+ },
352
+ ],
353
+ };
354
+
355
+ const result = resolveAssetConfig(options, "docs/guide.md");
356
+
357
+ expect(result).toEqual({
358
+ assetsPath: "src/content/docs/algokit-utils/typescript/v8.0.0/assets",
359
+ assetsBaseUrl: "./assets",
360
+ });
361
+ });
362
+
363
+ it("should return null when only assetsPath is set (misconfiguration)", () => {
364
+ const options: ImportOptions = {
365
+ owner: "test",
366
+ repo: "repo",
367
+ assetsPath: "src/assets/custom",
368
+ // assetsBaseUrl intentionally omitted
369
+ includes: [
370
+ {
371
+ pattern: "docs/**/*.md",
372
+ basePath: "src/content/docs/lib",
373
+ },
374
+ ],
375
+ };
376
+
377
+ const result = resolveAssetConfig(options, "docs/guide.md");
378
+ expect(result).toBeNull();
379
+ });
380
+
381
+ it("should return null when only assetsBaseUrl is set (misconfiguration)", () => {
382
+ const options: ImportOptions = {
383
+ owner: "test",
384
+ repo: "repo",
385
+ // assetsPath intentionally omitted
386
+ assetsBaseUrl: "/assets/custom",
387
+ includes: [
388
+ {
389
+ pattern: "docs/**/*.md",
390
+ basePath: "src/content/docs/lib",
391
+ },
392
+ ],
393
+ };
394
+
395
+ const result = resolveAssetConfig(options, "docs/guide.md");
396
+ expect(result).toBeNull();
397
+ });
398
+
399
+ it("should return null when file does not match any include pattern", () => {
400
+ const options: ImportOptions = {
401
+ owner: "test",
402
+ repo: "repo",
403
+ includes: [
404
+ {
405
+ pattern: "docs/**/*.md",
406
+ basePath: "src/content/docs/lib",
407
+ },
408
+ ],
409
+ };
410
+
411
+ const result = resolveAssetConfig(options, "src/main.ts");
412
+ expect(result).toBeNull();
413
+ });
414
+
415
+ it("should return null when no includes are defined and no explicit config", () => {
416
+ const options: ImportOptions = {
417
+ owner: "test",
418
+ repo: "repo",
419
+ };
420
+
421
+ const result = resolveAssetConfig(options, "docs/guide.md");
422
+ // No includes means shouldIncludeFile returns matchedPattern: null
423
+ expect(result).toBeNull();
424
+ });
425
+
426
+ it("should use the correct basePath when multiple patterns exist", () => {
427
+ const options: ImportOptions = {
428
+ owner: "test",
429
+ repo: "repo",
430
+ includes: [
431
+ {
432
+ pattern: "docs/guides/**/*.md",
433
+ basePath: "src/content/docs/guides",
434
+ },
435
+ {
436
+ pattern: "docs/api/**/*.md",
437
+ basePath: "src/content/docs/reference/api",
438
+ },
439
+ ],
440
+ };
441
+
442
+ // File matches the second pattern
443
+ const result = resolveAssetConfig(options, "docs/api/endpoints.md");
592
444
 
593
- // Verify expected files are stored
594
- expect(storedIds.some(id => id.includes('overview'))).toBe(true); // algokit.md mapped to overview
595
- expect(storedIds.filter(id => id.includes('features')).length).toBe(3); // 3 features files
596
- expect(storedIds.some(id => id.includes('cli') && id.includes('index'))).toBe(true); // cli/index.md
445
+ expect(result).toEqual({
446
+ assetsPath: "src/content/docs/reference/api/assets",
447
+ assetsBaseUrl: "./assets",
597
448
  });
598
449
  });
599
450
  });