@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,9 +1,11 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { Octokit } from "octokit";
5
+ import type { Logger } from "./github.logger.js";
4
6
  import type { ImportOptions, LoaderContext } from "./github.types.js";
5
7
 
6
- const STATE_FILENAME = '.github-import-state.json';
8
+ const STATE_FILENAME = ".github-import-state.json";
7
9
 
8
10
  /**
9
11
  * Represents the state of a single import configuration
@@ -55,30 +57,51 @@ export interface RepositoryChangeInfo {
55
57
  * Creates a unique identifier for an import configuration
56
58
  */
57
59
  export function createConfigId(config: ImportOptions): string {
58
- return `${config.owner}/${config.repo}@${config.ref || 'main'}`;
60
+ if (config.stateKey) {
61
+ return config.stateKey;
62
+ }
63
+ return `${config.owner}/${config.repo}@${config.ref || "main"}`;
59
64
  }
60
65
 
61
66
  /**
62
67
  * Loads the import state from the state file
63
68
  */
64
- export async function loadImportState(workingDir: string): Promise<StateFile> {
69
+ export async function loadImportState(
70
+ workingDir: string,
71
+ logger?: Logger,
72
+ ): Promise<StateFile> {
65
73
  const statePath = join(workingDir, STATE_FILENAME);
66
-
74
+
67
75
  if (!existsSync(statePath)) {
68
76
  return {
69
77
  imports: {},
70
- lastChecked: new Date().toISOString()
78
+ lastChecked: new Date().toISOString(),
71
79
  };
72
80
  }
73
81
 
74
82
  try {
75
- const content = await fs.readFile(statePath, 'utf-8');
76
- return JSON.parse(content);
83
+ const content = await fs.readFile(statePath, "utf-8");
84
+ const parsed = JSON.parse(content);
85
+ // Validate the parsed state has the expected shape
86
+ if (
87
+ !parsed ||
88
+ typeof parsed !== "object" ||
89
+ !("imports" in parsed) ||
90
+ typeof parsed.imports !== "object"
91
+ ) {
92
+ const msg = `Malformed state file at ${statePath}, starting fresh`;
93
+ // eslint-disable-next-line no-console -- fallback when no logger provided
94
+ logger ? logger.warn(msg) : console.warn(msg);
95
+ return { imports: {}, lastChecked: new Date().toISOString() };
96
+ }
97
+ return parsed;
77
98
  } catch (error) {
78
- console.warn(`Failed to load import state from ${statePath}, starting fresh:`, error);
99
+ const msg = `Failed to load import state from ${statePath}, starting fresh: ${error}`;
100
+ // eslint-disable-next-line no-console -- fallback when no logger provided
101
+ logger ? logger.warn(msg) : console.warn(msg);
79
102
  return {
80
103
  imports: {},
81
- lastChecked: new Date().toISOString()
104
+ lastChecked: new Date().toISOString(),
82
105
  };
83
106
  }
84
107
  }
@@ -86,13 +109,19 @@ export async function loadImportState(workingDir: string): Promise<StateFile> {
86
109
  /**
87
110
  * Saves the import state to the state file
88
111
  */
89
- async function saveImportState(workingDir: string, state: StateFile): Promise<void> {
112
+ async function saveImportState(
113
+ workingDir: string,
114
+ state: StateFile,
115
+ logger?: Logger,
116
+ ): Promise<void> {
90
117
  const statePath = join(workingDir, STATE_FILENAME);
91
-
118
+
92
119
  try {
93
- await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf-8');
120
+ await fs.writeFile(statePath, JSON.stringify(state, null, 2), "utf-8");
94
121
  } catch (error) {
95
- console.warn(`Failed to save import state to ${statePath}:`, error);
122
+ const msg = `Failed to save import state to ${statePath}: ${error}`;
123
+ // eslint-disable-next-line no-console -- fallback when no logger provided
124
+ logger ? logger.warn(msg) : console.warn(msg);
96
125
  }
97
126
  }
98
127
 
@@ -100,9 +129,9 @@ async function saveImportState(workingDir: string, state: StateFile): Promise<vo
100
129
  * Gets the latest commit information for a repository path
101
130
  */
102
131
  export async function getLatestCommitInfo(
103
- octokit: any,
132
+ octokit: Octokit,
104
133
  config: ImportOptions,
105
- signal?: AbortSignal
134
+ signal?: AbortSignal,
106
135
  ): Promise<{ sha: string; message: string; date: string } | null> {
107
136
  const { owner, repo, ref = "main" } = config;
108
137
 
@@ -113,7 +142,7 @@ export async function getLatestCommitInfo(
113
142
  repo,
114
143
  sha: ref,
115
144
  per_page: 1,
116
- request: { signal }
145
+ request: { signal },
117
146
  });
118
147
 
119
148
  if (data.length === 0) {
@@ -123,11 +152,19 @@ export async function getLatestCommitInfo(
123
152
  const latestCommit = data[0];
124
153
  return {
125
154
  sha: latestCommit.sha,
126
- message: latestCommit.commit.message.split('\n')[0], // First line only
127
- date: latestCommit.commit.committer?.date || latestCommit.commit.author?.date || new Date().toISOString()
155
+ message: latestCommit.commit.message.split("\n")[0], // First line only
156
+ date:
157
+ latestCommit.commit.committer?.date ||
158
+ latestCommit.commit.author?.date ||
159
+ new Date().toISOString(),
128
160
  };
129
- } catch (error: any) {
130
- if (error.status === 404) {
161
+ } catch (error: unknown) {
162
+ if (
163
+ typeof error === "object" &&
164
+ error !== null &&
165
+ "status" in error &&
166
+ (error as { status: number }).status === 404
167
+ ) {
131
168
  throw new Error(`Repository not found: ${owner}/${repo}`);
132
169
  }
133
170
  throw error;
@@ -138,26 +175,28 @@ export async function getLatestCommitInfo(
138
175
  * Checks a single repository for changes
139
176
  */
140
177
  async function checkRepositoryForChanges(
141
- octokit: any,
178
+ octokit: Octokit,
142
179
  config: ImportOptions,
143
180
  currentState: ImportState,
144
- signal?: AbortSignal
181
+ signal?: AbortSignal,
145
182
  ): Promise<RepositoryChangeInfo> {
146
183
  const configName = config.name || `${config.owner}/${config.repo}`;
147
184
 
148
185
  try {
149
186
  const latestCommit = await getLatestCommitInfo(octokit, config, signal);
150
-
187
+
151
188
  if (!latestCommit) {
152
189
  return {
153
190
  config,
154
191
  state: currentState,
155
192
  needsReimport: false,
156
- error: "No commits found in repository"
193
+ error: "No commits found in repository",
157
194
  };
158
195
  }
159
196
 
160
- const needsReimport = !currentState.lastCommitSha || currentState.lastCommitSha !== latestCommit.sha;
197
+ const needsReimport =
198
+ !currentState.lastCommitSha ||
199
+ currentState.lastCommitSha !== latestCommit.sha;
161
200
 
162
201
  return {
163
202
  config,
@@ -165,15 +204,14 @@ async function checkRepositoryForChanges(
165
204
  needsReimport,
166
205
  latestCommitSha: latestCommit.sha,
167
206
  latestCommitMessage: latestCommit.message,
168
- latestCommitDate: latestCommit.date
207
+ latestCommitDate: latestCommit.date,
169
208
  };
170
-
171
- } catch (error: any) {
209
+ } catch (error: unknown) {
172
210
  return {
173
211
  config,
174
212
  state: currentState,
175
213
  needsReimport: false,
176
- error: error.message
214
+ error: error instanceof Error ? error.message : String(error),
177
215
  };
178
216
  }
179
217
  }
@@ -184,9 +222,10 @@ async function checkRepositoryForChanges(
184
222
  export async function updateImportState(
185
223
  workingDir: string,
186
224
  config: ImportOptions,
187
- commitSha?: string
225
+ commitSha?: string,
226
+ logger?: Logger,
188
227
  ): Promise<void> {
189
- const state = await loadImportState(workingDir);
228
+ const state = await loadImportState(workingDir, logger);
190
229
  const configId = createConfigId(config);
191
230
  const configName = config.name || `${config.owner}/${config.repo}`;
192
231
 
@@ -195,10 +234,10 @@ export async function updateImportState(
195
234
  repoId: configId,
196
235
  lastCommitSha: commitSha,
197
236
  lastImported: new Date().toISOString(),
198
- ref: config.ref || 'main'
237
+ ref: config.ref || "main",
199
238
  };
200
239
 
201
- await saveImportState(workingDir, state);
240
+ await saveImportState(workingDir, state, logger);
202
241
  }
203
242
 
204
243
  /**
@@ -207,14 +246,14 @@ export async function updateImportState(
207
246
  export async function performDryRun(
208
247
  configs: ImportOptions[],
209
248
  context: LoaderContext,
210
- octokit: any,
249
+ octokit: Octokit,
211
250
  workingDir: string = process.cwd(),
212
- signal?: AbortSignal
251
+ signal?: AbortSignal,
213
252
  ): Promise<RepositoryChangeInfo[]> {
214
253
  const { logger } = context;
215
-
254
+
216
255
  logger.info("šŸ” Performing dry run - checking for repository changes...");
217
-
256
+
218
257
  // Load current state
219
258
  const state = await loadImportState(workingDir);
220
259
  const results: RepositoryChangeInfo[] = [];
@@ -222,33 +261,40 @@ export async function performDryRun(
222
261
  // Check each configuration
223
262
  for (const config of configs) {
224
263
  if (config.enabled === false) {
225
- logger.debug(`Skipping disabled config: ${config.name || `${config.owner}/${config.repo}`}`);
264
+ logger.debug(
265
+ `Skipping disabled config: ${config.name || `${config.owner}/${config.repo}`}`,
266
+ );
226
267
  continue;
227
268
  }
228
269
 
229
270
  const configId = createConfigId(config);
230
271
  const configName = config.name || `${config.owner}/${config.repo}`;
231
-
272
+
232
273
  // Get current state for this config
233
274
  const currentState: ImportState = state.imports[configId] || {
234
275
  name: configName,
235
276
  repoId: configId,
236
- ref: config.ref || 'main'
277
+ ref: config.ref || "main",
237
278
  };
238
279
 
239
280
  logger.debug(`Checking ${configName}...`);
240
-
281
+
241
282
  try {
242
- const changeInfo = await checkRepositoryForChanges(octokit, config, currentState, signal);
283
+ const changeInfo = await checkRepositoryForChanges(
284
+ octokit,
285
+ config,
286
+ currentState,
287
+ signal,
288
+ );
243
289
  results.push(changeInfo);
244
- } catch (error: any) {
290
+ } catch (error: unknown) {
245
291
  if (signal?.aborted) throw error;
246
-
292
+
247
293
  results.push({
248
294
  config,
249
295
  state: currentState,
250
296
  needsReimport: false,
251
- error: `Failed to check repository: ${error.message}`
297
+ error: `Failed to check repository: ${error instanceof Error ? error.message : String(error)}`,
252
298
  });
253
299
  }
254
300
  }
@@ -263,16 +309,20 @@ export async function performDryRun(
263
309
  /**
264
310
  * Formats and displays the dry run results
265
311
  */
266
- export function displayDryRunResults(results: RepositoryChangeInfo[], logger: any): void {
312
+ export function displayDryRunResults(
313
+ results: RepositoryChangeInfo[],
314
+ logger: { info: (msg: string) => void },
315
+ ): void {
267
316
  logger.info("\nšŸ“Š Repository Import Status:");
268
- logger.info("=" .repeat(50));
317
+ logger.info("=".repeat(50));
269
318
 
270
319
  let needsReimportCount = 0;
271
320
  let errorCount = 0;
272
321
 
273
322
  for (const result of results) {
274
- const configName = result.config.name || `${result.config.owner}/${result.config.repo}`;
275
-
323
+ const configName =
324
+ result.config.name || `${result.config.owner}/${result.config.repo}`;
325
+
276
326
  if (result.error) {
277
327
  logger.info(`āŒ ${configName}: ${result.error}`);
278
328
  errorCount++;
@@ -304,12 +354,16 @@ export function displayDryRunResults(results: RepositoryChangeInfo[], logger: an
304
354
  }
305
355
  }
306
356
 
307
- logger.info("=" .repeat(50));
308
- logger.info(`šŸ“ˆ Summary: ${needsReimportCount} of ${results.length} repositories need re-import, ${errorCount} errors`);
309
-
357
+ logger.info("=".repeat(50));
358
+ logger.info(
359
+ `šŸ“ˆ Summary: ${needsReimportCount} of ${results.length} repositories need re-import, ${errorCount} errors`,
360
+ );
361
+
310
362
  if (needsReimportCount > 0) {
311
363
  logger.info("\nšŸ’” To import updated repositories:");
312
- logger.info("1. Delete the target import folders for repositories that need re-import");
364
+ logger.info(
365
+ "1. Delete the target import folders for repositories that need re-import",
366
+ );
313
367
  logger.info("2. Run the import process normally (dryRun: false)");
314
368
  logger.info("3. Fresh content will be imported automatically");
315
369
  } else {
@@ -336,4 +390,4 @@ function getTimeAgo(date: Date): string {
336
390
  } else {
337
391
  return date.toLocaleDateString();
338
392
  }
339
- }
393
+ }