@larkiny/astro-github-loader 0.11.2 → 0.12.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 +69 -61
  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 +6 -132
  10. package/dist/github.content.js +154 -789
  11. package/dist/github.dryrun.d.ts +9 -5
  12. package/dist/github.dryrun.js +46 -25
  13. package/dist/github.link-transform.d.ts +2 -2
  14. package/dist/github.link-transform.js +65 -57
  15. package/dist/github.loader.js +45 -51
  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 +15 -0
  21. package/dist/github.storage.js +109 -0
  22. package/dist/github.types.d.ts +41 -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 +261 -950
  37. package/src/github.dryrun.spec.ts +586 -0
  38. package/src/github.dryrun.ts +105 -54
  39. package/src/github.link-transform.spec.ts +1345 -0
  40. package/src/github.link-transform.ts +174 -95
  41. package/src/github.loader.spec.ts +75 -50
  42. package/src/github.loader.ts +113 -78
  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 +367 -0
  48. package/src/github.storage.ts +127 -0
  49. package/src/github.types.ts +55 -9
  50. package/src/index.ts +43 -6
  51. package/src/test-helpers.ts +215 -0
@@ -0,0 +1,109 @@
1
+ import { existsSync, promises as fs } from "node:fs";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
+ /**
4
+ * Ensures directory exists and writes file to disk.
5
+ * @internal
6
+ */
7
+ export async function syncFile(path, content) {
8
+ const dir = path.substring(0, path.lastIndexOf("/"));
9
+ if (dir && !existsSync(dir)) {
10
+ await fs.mkdir(dir, { recursive: true });
11
+ }
12
+ await fs.writeFile(path, content, "utf-8");
13
+ }
14
+ /**
15
+ * Stores a processed file in Astro's content store
16
+ * @internal
17
+ */
18
+ export async function storeProcessedFile(file, context, clear) {
19
+ const { store, generateDigest, entryTypes, logger, parseData, config } = context;
20
+ function configForFile(filePath) {
21
+ const ext = filePath.split(".").at(-1);
22
+ if (!ext) {
23
+ logger.warn(`No extension found for ${filePath}`);
24
+ return;
25
+ }
26
+ return entryTypes?.get(`.${ext}`);
27
+ }
28
+ const entryType = configForFile(file.sourcePath || "tmp.md");
29
+ if (!entryType)
30
+ throw new Error("No entry type found");
31
+ const fileUrl = pathToFileURL(file.targetPath);
32
+ const { body, data } = await entryType.getEntryInfo({
33
+ contents: file.content,
34
+ fileUrl: fileUrl,
35
+ });
36
+ // Generate digest for storage (repository-level caching handles change detection)
37
+ const digest = generateDigest(file.content);
38
+ const existingEntry = store.get(file.id);
39
+ if (existingEntry) {
40
+ logger.debug(`🔄 File ${file.id} - updating`);
41
+ }
42
+ else {
43
+ logger.debug(`📄 File ${file.id} - adding`);
44
+ }
45
+ // Write file to disk
46
+ if (!existsSync(fileURLToPath(fileUrl))) {
47
+ logger.verbose(`Writing ${file.id} to ${fileUrl}`);
48
+ await syncFile(fileURLToPath(fileUrl), file.content);
49
+ }
50
+ const parsedData = await parseData({
51
+ id: file.id,
52
+ data,
53
+ filePath: fileUrl.toString(),
54
+ });
55
+ // When clear mode is enabled, delete the existing entry before setting the new one.
56
+ // This provides atomic replacement without breaking Astro's content collection,
57
+ // as opposed to calling store.clear() which empties everything at once.
58
+ if (clear && existingEntry) {
59
+ logger.debug(`🗑️ Clearing existing entry before replacement: ${file.id}`);
60
+ store.delete(file.id);
61
+ }
62
+ // Store in content store
63
+ if (entryType.getRenderFunction) {
64
+ logger.verbose(`Rendering ${file.id}`);
65
+ const render = await entryType.getRenderFunction(config);
66
+ let rendered = undefined;
67
+ try {
68
+ rendered = await render?.({
69
+ id: file.id,
70
+ data,
71
+ body,
72
+ filePath: fileUrl.toString(),
73
+ digest,
74
+ });
75
+ }
76
+ catch (error) {
77
+ logger.error(`Error rendering ${file.id}: ${error instanceof Error ? error.message : String(error)}`);
78
+ }
79
+ logger.debug(`🔍 Storing collection entry: ${file.id} (${file.sourcePath} -> ${file.targetPath})`);
80
+ store.set({
81
+ id: file.id,
82
+ data: parsedData,
83
+ body,
84
+ filePath: file.targetPath,
85
+ digest,
86
+ rendered,
87
+ });
88
+ }
89
+ else if ("contentModuleTypes" in entryType) {
90
+ store.set({
91
+ id: file.id,
92
+ data: parsedData,
93
+ body,
94
+ filePath: file.targetPath,
95
+ digest,
96
+ deferredRender: true,
97
+ });
98
+ }
99
+ else {
100
+ store.set({
101
+ id: file.id,
102
+ data: parsedData,
103
+ body,
104
+ filePath: file.targetPath,
105
+ digest,
106
+ });
107
+ }
108
+ return { id: file.id, filePath: file.targetPath };
109
+ }
@@ -3,7 +3,7 @@ import type { ContentEntryType } from "astro";
3
3
  import type { MarkdownHeading } from "@astrojs/markdown-remark";
4
4
  import { Octokit } from "octokit";
5
5
  import type { LinkHandler } from "./github.link-transform.js";
6
- import type { LogLevel } from "./github.logger.js";
6
+ import type { LogLevel, Logger } from "./github.logger.js";
7
7
  /**
8
8
  * Context information for link transformations
9
9
  */
@@ -26,7 +26,7 @@ export interface LinkMapping {
26
26
  /** Pattern to match (string or regex) */
27
27
  pattern: string | RegExp;
28
28
  /** Replacement string or function */
29
- replacement: string | ((match: string, anchor: string, context: any) => string);
29
+ replacement: string | ((match: string, anchor: string, context: LinkTransformContext) => string);
30
30
  /** Apply to all links, not just unresolved internal links (default: false) */
31
31
  global?: boolean;
32
32
  /** Function to determine if this mapping should apply to the current file context */
@@ -162,7 +162,7 @@ export type CollectionEntryOptions = {
162
162
  * The LoaderContext may contain properties and methods that offer
163
163
  * control or inspection over the loading behavior.
164
164
  */
165
- context: LoaderContext;
165
+ context: ExtendedLoaderContext;
166
166
  /**
167
167
  * An instance of the Octokit library, which provides a way to interact
168
168
  * with GitHub's REST API. This variable allows you to access and perform
@@ -199,6 +199,13 @@ export type CollectionEntryOptions = {
199
199
  * @default false
200
200
  */
201
201
  force?: boolean;
202
+ /**
203
+ * When true, deletes existing store entries before setting new ones.
204
+ * This enables atomic replacement of entries without breaking the content collection.
205
+ * Passed from GithubLoaderOptions.clear
206
+ * @internal
207
+ */
208
+ clear?: boolean;
202
209
  };
203
210
  /**
204
211
  * Interface representing rendered content, including HTML and associated metadata.
@@ -218,6 +225,16 @@ export interface RenderedContent {
218
225
  [key: string]: unknown;
219
226
  };
220
227
  }
228
+ /**
229
+ * Represents a version of a library variant to display in the devportal's version picker.
230
+ * Versions are manually curated in the import config — no auto-discovery.
231
+ */
232
+ export interface VersionConfig {
233
+ /** URL segment for this version (e.g., "latest", "v8.0.0") */
234
+ slug: string;
235
+ /** Display name for this version (e.g., "Latest", "v8.0.0") */
236
+ label: string;
237
+ }
221
238
  /**
222
239
  * Represents configuration options for importing content from GitHub repositories.
223
240
  */
@@ -279,18 +296,38 @@ export type ImportOptions = {
279
296
  * @default 'default'
280
297
  */
281
298
  logLevel?: LogLevel;
299
+ /**
300
+ * Language for this import variant (e.g., "TypeScript", "Python", "Go").
301
+ * Used for logging and passed through to the devportal for UI display.
302
+ */
303
+ language?: string;
304
+ /**
305
+ * Versions to display in the devportal's version picker.
306
+ * Informational — tells the loader which version folders exist in the source content.
307
+ * The loader imports content as-is; the version folder structure carries through from source to destination.
308
+ */
309
+ versions?: VersionConfig[];
282
310
  };
283
311
  export type FetchOptions = RequestInit & {
284
312
  signal?: AbortSignal;
285
313
  concurrency?: number;
286
314
  };
287
315
  /**
288
- * @internal
316
+ * Astro loader context extended with optional entry type support.
317
+ * Use this type when calling `.load(context as LoaderContext)` in multi-loader patterns.
289
318
  */
290
319
  export interface LoaderContext extends AstroLoaderContext {
291
320
  /** @internal */
292
321
  entryTypes?: Map<string, ContentEntryType>;
293
322
  }
323
+ /**
324
+ * LoaderContext with Astro's logger replaced by our Logger class.
325
+ * Used by internal functions that need verbose/logFileProcessing/etc.
326
+ * @internal
327
+ */
328
+ export type ExtendedLoaderContext = Omit<LoaderContext, "logger"> & {
329
+ logger: Logger;
330
+ };
294
331
  /**
295
332
  * @internal
296
333
  */
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- export * from './github.auth.js';
2
- export * from './github.constants.js';
3
- export * from './github.content.js';
4
- export * from './github.loader.js';
5
- export * from './github.types.js';
6
- export * from './github.link-transform.js';
1
+ export { githubLoader } from "./github.loader.js";
2
+ export { createAuthenticatedOctokit, createOctokitFromEnv, } from "./github.auth.js";
3
+ export type { GithubLoaderOptions, ImportOptions, FetchOptions, IncludePattern, PathMappingValue, EnhancedPathMapping, VersionConfig, LoaderContext, } from "./github.types.js";
4
+ export type { TransformFunction, TransformContext, MatchedPattern, } from "./github.types.js";
5
+ export type { LinkMapping, LinkTransformContext, ImportLinkTransformOptions, } from "./github.types.js";
6
+ export type { LinkHandler } from "./github.link-transform.js";
7
+ export type { GitHubAuthConfig, GitHubAppAuthConfig, GitHubPATAuthConfig, } from "./github.auth.js";
8
+ export type { LogLevel } from "./github.logger.js";
package/dist/index.js CHANGED
@@ -1,6 +1,3 @@
1
- export * from './github.auth.js';
2
- export * from './github.constants.js';
3
- export * from './github.content.js';
4
- export * from './github.loader.js';
5
- export * from './github.types.js';
6
- export * from './github.link-transform.js';
1
+ // Public API — functions
2
+ export { githubLoader } from "./github.loader.js";
3
+ export { createAuthenticatedOctokit, createOctokitFromEnv, } from "./github.auth.js";
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Creates a mock Astro LoaderContext with all required properties.
3
+ * The returned store is a real Map wrapped in the store interface,
4
+ * so tests can inspect stored entries directly.
5
+ */
6
+ export declare function createMockContext(): {
7
+ store: {
8
+ set: (entry: any) => any;
9
+ get: (id: string) => any;
10
+ delete: (id: string) => boolean;
11
+ clear: () => void;
12
+ entries: () => MapIterator<[string, any]>;
13
+ keys: () => MapIterator<string>;
14
+ values: () => MapIterator<any>;
15
+ };
16
+ meta: Map<string, string>;
17
+ logger: {
18
+ info: import("vitest").Mock<(...args: any[]) => any>;
19
+ warn: import("vitest").Mock<(...args: any[]) => any>;
20
+ error: import("vitest").Mock<(...args: any[]) => any>;
21
+ debug: import("vitest").Mock<(...args: any[]) => any>;
22
+ verbose: import("vitest").Mock<(...args: any[]) => any>;
23
+ logFileProcessing: import("vitest").Mock<(...args: any[]) => any>;
24
+ logImportSummary: import("vitest").Mock<(...args: any[]) => any>;
25
+ logAssetProcessing: import("vitest").Mock<(...args: any[]) => any>;
26
+ withSpinner: (_msg: string, fn: () => Promise<any>) => Promise<any>;
27
+ getLevel: () => "default";
28
+ };
29
+ config: {};
30
+ entryTypes: Map<string, {
31
+ getEntryInfo: ({ contents, }: {
32
+ contents: string;
33
+ fileUrl: URL;
34
+ }) => Promise<{
35
+ body: string;
36
+ data: {};
37
+ }>;
38
+ }>;
39
+ generateDigest: (content: string) => string;
40
+ parseData: (data: any) => Promise<any>;
41
+ /** Direct access to the underlying store Map for assertions */
42
+ _store: Map<string, any>;
43
+ /** Direct access to the underlying meta Map for assertions */
44
+ _meta: Map<string, string>;
45
+ };
46
+ /** Standard mock commit used across tests */
47
+ export declare const MOCK_COMMIT: {
48
+ sha: string;
49
+ commit: {
50
+ tree: {
51
+ sha: string;
52
+ };
53
+ message: string;
54
+ author: {
55
+ name: string;
56
+ email: string;
57
+ date: string;
58
+ };
59
+ committer: {
60
+ name: string;
61
+ email: string;
62
+ date: string;
63
+ };
64
+ };
65
+ };
66
+ /** Mock tree data representing a typical repository structure */
67
+ export declare const MOCK_TREE_DATA: {
68
+ sha: string;
69
+ url: string;
70
+ tree: ({
71
+ path: string;
72
+ mode: string;
73
+ type: string;
74
+ sha: string;
75
+ size: number;
76
+ url: string;
77
+ } | {
78
+ path: string;
79
+ mode: string;
80
+ type: string;
81
+ sha: string;
82
+ url: string;
83
+ size?: undefined;
84
+ })[];
85
+ truncated: boolean;
86
+ };
87
+ /**
88
+ * Creates an Octokit instance with mocked API methods for listCommits and getTree.
89
+ * Returns both the instance and the spies for assertions.
90
+ */
91
+ export declare function createMockOctokit(options?: {
92
+ treeData?: typeof MOCK_TREE_DATA;
93
+ commitData?: typeof MOCK_COMMIT;
94
+ }): {
95
+ octokit: import("@octokit/core").Octokit & {
96
+ paginate: import("@octokit/plugin-paginate-rest").PaginateInterface;
97
+ } & import("@octokit/plugin-paginate-graphql").paginateGraphQLInterface & import("@octokit/plugin-rest-endpoint-methods").Api & {
98
+ retry: {
99
+ retryRequest: (error: import("octokit").RequestError, retries: number, retryAfter: number) => import("octokit").RequestError;
100
+ };
101
+ };
102
+ spies: {
103
+ listCommitsSpy: import("vitest").MockInstance<{
104
+ (params?: import("@octokit/plugin-rest-endpoint-methods").RestEndpointMethodTypes["repos"]["listCommits"]["parameters"]): Promise<import("@octokit/plugin-rest-endpoint-methods").RestEndpointMethodTypes["repos"]["listCommits"]["response"]>;
105
+ defaults: import("@octokit/types").RequestInterface["defaults"];
106
+ endpoint: import("@octokit/types").EndpointInterface<{
107
+ url: string;
108
+ }>;
109
+ }>;
110
+ getTreeSpy: import("vitest").MockInstance<{
111
+ (params?: import("@octokit/plugin-rest-endpoint-methods").RestEndpointMethodTypes["git"]["getTree"]["parameters"]): Promise<import("@octokit/plugin-rest-endpoint-methods").RestEndpointMethodTypes["git"]["getTree"]["response"]>;
112
+ defaults: import("@octokit/types").RequestInterface["defaults"];
113
+ endpoint: import("@octokit/types").EndpointInterface<{
114
+ url: string;
115
+ }>;
116
+ }>;
117
+ getContentSpy: import("vitest").MockInstance<{
118
+ (params?: import("@octokit/plugin-rest-endpoint-methods").RestEndpointMethodTypes["repos"]["getContent"]["parameters"]): Promise<import("@octokit/plugin-rest-endpoint-methods").RestEndpointMethodTypes["repos"]["getContent"]["response"]>;
119
+ defaults: import("@octokit/types").RequestInterface["defaults"];
120
+ endpoint: import("@octokit/types").EndpointInterface<{
121
+ url: string;
122
+ }>;
123
+ }>;
124
+ };
125
+ };
126
+ /**
127
+ * Sets up a global fetch mock that returns markdown content.
128
+ * Returns the mock for assertions.
129
+ */
130
+ export declare function mockFetch(content?: string): import("vitest").Mock<(...args: any[]) => any>;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Shared test helpers for astro-github-loader test suite.
3
+ * Provides factory functions for creating mock Astro loader contexts,
4
+ * Octokit instances with pre-configured spies, and common fixtures.
5
+ */
6
+ import { vi } from "vitest";
7
+ import { Octokit } from "octokit";
8
+ /**
9
+ * Creates a mock Astro LoaderContext with all required properties.
10
+ * The returned store is a real Map wrapped in the store interface,
11
+ * so tests can inspect stored entries directly.
12
+ */
13
+ export function createMockContext() {
14
+ const store = new Map();
15
+ const meta = new Map();
16
+ return {
17
+ store: {
18
+ set: (entry) => {
19
+ store.set(entry.id, entry);
20
+ return entry;
21
+ },
22
+ get: (id) => store.get(id),
23
+ delete: (id) => store.delete(id),
24
+ clear: () => store.clear(),
25
+ entries: () => store.entries(),
26
+ keys: () => store.keys(),
27
+ values: () => store.values(),
28
+ },
29
+ meta,
30
+ logger: {
31
+ info: vi.fn(),
32
+ warn: vi.fn(),
33
+ error: vi.fn(),
34
+ debug: vi.fn(),
35
+ verbose: vi.fn(),
36
+ logFileProcessing: vi.fn(),
37
+ logImportSummary: vi.fn(),
38
+ logAssetProcessing: vi.fn(),
39
+ withSpinner: async (_msg, fn) => await fn(),
40
+ getLevel: () => "default",
41
+ },
42
+ config: {},
43
+ entryTypes: new Map([
44
+ [
45
+ ".md",
46
+ {
47
+ getEntryInfo: async ({ contents, }) => ({
48
+ body: contents,
49
+ data: {},
50
+ }),
51
+ },
52
+ ],
53
+ ]),
54
+ generateDigest: (content) => String(content.length),
55
+ parseData: async (data) => data,
56
+ /** Direct access to the underlying store Map for assertions */
57
+ _store: store,
58
+ /** Direct access to the underlying meta Map for assertions */
59
+ _meta: meta,
60
+ };
61
+ }
62
+ /** Standard mock commit used across tests */
63
+ export const MOCK_COMMIT = {
64
+ sha: "abc123def456",
65
+ commit: {
66
+ tree: { sha: "tree123abc456" },
67
+ message: "Test commit",
68
+ author: {
69
+ name: "Test Author",
70
+ email: "test@example.com",
71
+ date: "2024-01-01T00:00:00Z",
72
+ },
73
+ committer: {
74
+ name: "Test Committer",
75
+ email: "test@example.com",
76
+ date: "2024-01-01T00:00:00Z",
77
+ },
78
+ },
79
+ };
80
+ /** Mock tree data representing a typical repository structure */
81
+ export const MOCK_TREE_DATA = {
82
+ sha: "tree123abc456",
83
+ url: "https://api.github.com/repos/test/repo/git/trees/tree123abc456",
84
+ tree: [
85
+ {
86
+ path: "docs/algokit.md",
87
+ mode: "100644",
88
+ type: "blob",
89
+ sha: "file1sha",
90
+ size: 1234,
91
+ url: "https://api.github.com/repos/test/repo/git/blobs/file1sha",
92
+ },
93
+ {
94
+ path: "docs/features",
95
+ mode: "040000",
96
+ type: "tree",
97
+ sha: "dir1sha",
98
+ url: "https://api.github.com/repos/test/repo/git/trees/dir1sha",
99
+ },
100
+ {
101
+ path: "docs/features/accounts.md",
102
+ mode: "100644",
103
+ type: "blob",
104
+ sha: "file2sha",
105
+ size: 2345,
106
+ url: "https://api.github.com/repos/test/repo/git/blobs/file2sha",
107
+ },
108
+ {
109
+ path: "docs/features/tasks.md",
110
+ mode: "100644",
111
+ type: "blob",
112
+ sha: "file3sha",
113
+ size: 3456,
114
+ url: "https://api.github.com/repos/test/repo/git/blobs/file3sha",
115
+ },
116
+ {
117
+ path: "docs/features/generate.md",
118
+ mode: "100644",
119
+ type: "blob",
120
+ sha: "file4sha",
121
+ size: 4567,
122
+ url: "https://api.github.com/repos/test/repo/git/blobs/file4sha",
123
+ },
124
+ {
125
+ path: "docs/cli/index.md",
126
+ mode: "100644",
127
+ type: "blob",
128
+ sha: "file5sha",
129
+ size: 5678,
130
+ url: "https://api.github.com/repos/test/repo/git/blobs/file5sha",
131
+ },
132
+ {
133
+ path: "README.md",
134
+ mode: "100644",
135
+ type: "blob",
136
+ sha: "file6sha",
137
+ size: 678,
138
+ url: "https://api.github.com/repos/test/repo/git/blobs/file6sha",
139
+ },
140
+ {
141
+ path: "package.json",
142
+ mode: "100644",
143
+ type: "blob",
144
+ sha: "file7sha",
145
+ size: 789,
146
+ url: "https://api.github.com/repos/test/repo/git/blobs/file7sha",
147
+ },
148
+ ],
149
+ truncated: false,
150
+ };
151
+ /**
152
+ * Creates an Octokit instance with mocked API methods for listCommits and getTree.
153
+ * Returns both the instance and the spies for assertions.
154
+ */
155
+ export function createMockOctokit(options) {
156
+ const octokit = new Octokit({ auth: "mock-token" });
157
+ const commit = options?.commitData ?? MOCK_COMMIT;
158
+ const tree = options?.treeData ?? MOCK_TREE_DATA;
159
+ const listCommitsSpy = vi
160
+ .spyOn(octokit.rest.repos, "listCommits")
161
+ .mockResolvedValue({
162
+ data: [commit],
163
+ status: 200,
164
+ url: "",
165
+ headers: {},
166
+ });
167
+ const getTreeSpy = vi.spyOn(octokit.rest.git, "getTree").mockResolvedValue({
168
+ data: tree,
169
+ status: 200,
170
+ url: "",
171
+ headers: {},
172
+ });
173
+ const getContentSpy = vi
174
+ .spyOn(octokit.rest.repos, "getContent")
175
+ .mockResolvedValue({ data: [], status: 200, url: "", headers: {} });
176
+ return {
177
+ octokit,
178
+ spies: { listCommitsSpy, getTreeSpy, getContentSpy },
179
+ };
180
+ }
181
+ /**
182
+ * Sets up a global fetch mock that returns markdown content.
183
+ * Returns the mock for assertions.
184
+ */
185
+ export function mockFetch(content = "# Test Content\n\nThis is test markdown content.") {
186
+ const fetchMock = vi.fn().mockResolvedValue({
187
+ ok: true,
188
+ status: 200,
189
+ headers: new Headers(),
190
+ text: async () => content,
191
+ });
192
+ global.fetch = fetchMock;
193
+ return fetchMock;
194
+ }
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.2",
4
+ "version": "0.12.0",
5
5
  "description": "Load content from GitHub repositories into Astro content collections with asset management and content transformations",
6
6
  "keywords": [
7
7
  "astro",
@@ -38,6 +38,7 @@
38
38
  "build": "tsc",
39
39
  "test": "vitest run",
40
40
  "test:watch": "vitest",
41
+ "test:coverage": "vitest run --coverage",
41
42
  "lint": "eslint .",
42
43
  "prettier": "prettier --check .",
43
44
  "preview": "astro preview",
@@ -57,6 +58,7 @@
57
58
  "@types/node": "^22.14.0",
58
59
  "@types/picomatch": "^4.0.0",
59
60
  "@typescript-eslint/parser": "^8.29.0",
61
+ "@vitest/coverage-v8": "^3.2.4",
60
62
  "eslint": "^9.24.0",
61
63
  "eslint-plugin-astro": "^1.3.1",
62
64
  "prettier": "3.5.3",