@larkiny/astro-github-loader 0.9.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/LICENSE +21 -0
- package/README.md +675 -0
- package/dist/github.cleanup.d.ts +5 -0
- package/dist/github.cleanup.js +216 -0
- package/dist/github.constants.d.ts +24 -0
- package/dist/github.constants.js +24 -0
- package/dist/github.content.d.ts +138 -0
- package/dist/github.content.js +1016 -0
- package/dist/github.dryrun.d.ts +72 -0
- package/dist/github.dryrun.js +247 -0
- package/dist/github.link-transform.d.ts +77 -0
- package/dist/github.link-transform.js +321 -0
- package/dist/github.loader.d.ts +14 -0
- package/dist/github.loader.js +143 -0
- package/dist/github.loader.spec.d.ts +1 -0
- package/dist/github.loader.spec.js +96 -0
- package/dist/github.logger.d.ts +132 -0
- package/dist/github.logger.js +260 -0
- package/dist/github.sync.d.ts +5 -0
- package/dist/github.sync.js +292 -0
- package/dist/github.types.d.ts +315 -0
- package/dist/github.types.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/package.json +66 -0
- package/src/github.cleanup.ts +243 -0
- package/src/github.constants.ts +25 -0
- package/src/github.content.ts +1205 -0
- package/src/github.dryrun.ts +339 -0
- package/src/github.link-transform.ts +452 -0
- package/src/github.loader.spec.ts +106 -0
- package/src/github.loader.ts +189 -0
- package/src/github.logger.ts +324 -0
- package/src/github.types.ts +339 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ImportOptions, LoaderContext } from "./github.types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Represents the state of a single import configuration
|
|
4
|
+
*/
|
|
5
|
+
export interface ImportState {
|
|
6
|
+
/** Configuration name for identification */
|
|
7
|
+
name: string;
|
|
8
|
+
/** Repository owner/name/path identifier */
|
|
9
|
+
repoId: string;
|
|
10
|
+
/** Last known commit SHA */
|
|
11
|
+
lastCommitSha?: string;
|
|
12
|
+
/** Last import timestamp */
|
|
13
|
+
lastImported?: string;
|
|
14
|
+
/** Git reference being tracked */
|
|
15
|
+
ref: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* State file structure
|
|
19
|
+
*/
|
|
20
|
+
export interface StateFile {
|
|
21
|
+
/** Map of config identifiers to their state */
|
|
22
|
+
imports: Record<string, ImportState>;
|
|
23
|
+
/** Last check timestamp */
|
|
24
|
+
lastChecked: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Information about repository changes
|
|
28
|
+
*/
|
|
29
|
+
export interface RepositoryChangeInfo {
|
|
30
|
+
/** Configuration details */
|
|
31
|
+
config: ImportOptions;
|
|
32
|
+
/** Current state */
|
|
33
|
+
state: ImportState;
|
|
34
|
+
/** Whether repository needs to be re-imported */
|
|
35
|
+
needsReimport: boolean;
|
|
36
|
+
/** Latest commit SHA from remote */
|
|
37
|
+
latestCommitSha?: string;
|
|
38
|
+
/** Latest commit message */
|
|
39
|
+
latestCommitMessage?: string;
|
|
40
|
+
/** Latest commit date */
|
|
41
|
+
latestCommitDate?: string;
|
|
42
|
+
/** Error message if check failed */
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Creates a unique identifier for an import configuration
|
|
47
|
+
*/
|
|
48
|
+
export declare function createConfigId(config: ImportOptions): string;
|
|
49
|
+
/**
|
|
50
|
+
* Loads the import state from the state file
|
|
51
|
+
*/
|
|
52
|
+
export declare function loadImportState(workingDir: string): Promise<StateFile>;
|
|
53
|
+
/**
|
|
54
|
+
* Gets the latest commit information for a repository path
|
|
55
|
+
*/
|
|
56
|
+
export declare function getLatestCommitInfo(octokit: any, config: ImportOptions, signal?: AbortSignal): Promise<{
|
|
57
|
+
sha: string;
|
|
58
|
+
message: string;
|
|
59
|
+
date: string;
|
|
60
|
+
} | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Updates the import state after a successful import
|
|
63
|
+
*/
|
|
64
|
+
export declare function updateImportState(workingDir: string, config: ImportOptions, commitSha?: string): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Performs a dry run check on all configured repositories
|
|
67
|
+
*/
|
|
68
|
+
export declare function performDryRun(configs: ImportOptions[], context: LoaderContext, octokit: any, workingDir?: string, signal?: AbortSignal): Promise<RepositoryChangeInfo[]>;
|
|
69
|
+
/**
|
|
70
|
+
* Formats and displays the dry run results
|
|
71
|
+
*/
|
|
72
|
+
export declare function displayDryRunResults(results: RepositoryChangeInfo[], logger: any): void;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const STATE_FILENAME = '.github-import-state.json';
|
|
5
|
+
/**
|
|
6
|
+
* Creates a unique identifier for an import configuration
|
|
7
|
+
*/
|
|
8
|
+
export function createConfigId(config) {
|
|
9
|
+
return `${config.owner}/${config.repo}@${config.ref || 'main'}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Loads the import state from the state file
|
|
13
|
+
*/
|
|
14
|
+
export async function loadImportState(workingDir) {
|
|
15
|
+
const statePath = join(workingDir, STATE_FILENAME);
|
|
16
|
+
if (!existsSync(statePath)) {
|
|
17
|
+
return {
|
|
18
|
+
imports: {},
|
|
19
|
+
lastChecked: new Date().toISOString()
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const content = await fs.readFile(statePath, 'utf-8');
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.warn(`Failed to load import state from ${statePath}, starting fresh:`, error);
|
|
28
|
+
return {
|
|
29
|
+
imports: {},
|
|
30
|
+
lastChecked: new Date().toISOString()
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Saves the import state to the state file
|
|
36
|
+
*/
|
|
37
|
+
async function saveImportState(workingDir, state) {
|
|
38
|
+
const statePath = join(workingDir, STATE_FILENAME);
|
|
39
|
+
try {
|
|
40
|
+
await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.warn(`Failed to save import state to ${statePath}:`, error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Gets the latest commit information for a repository path
|
|
48
|
+
*/
|
|
49
|
+
export async function getLatestCommitInfo(octokit, config, signal) {
|
|
50
|
+
const { owner, repo, ref = "main" } = config;
|
|
51
|
+
try {
|
|
52
|
+
// Get commits for the entire repository
|
|
53
|
+
const { data } = await octokit.rest.repos.listCommits({
|
|
54
|
+
owner,
|
|
55
|
+
repo,
|
|
56
|
+
sha: ref,
|
|
57
|
+
per_page: 1,
|
|
58
|
+
request: { signal }
|
|
59
|
+
});
|
|
60
|
+
if (data.length === 0) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const latestCommit = data[0];
|
|
64
|
+
return {
|
|
65
|
+
sha: latestCommit.sha,
|
|
66
|
+
message: latestCommit.commit.message.split('\n')[0], // First line only
|
|
67
|
+
date: latestCommit.commit.committer?.date || latestCommit.commit.author?.date || new Date().toISOString()
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.status === 404) {
|
|
72
|
+
throw new Error(`Repository not found: ${owner}/${repo}`);
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Checks a single repository for changes
|
|
79
|
+
*/
|
|
80
|
+
async function checkRepositoryForChanges(octokit, config, currentState, signal) {
|
|
81
|
+
const configName = config.name || `${config.owner}/${config.repo}`;
|
|
82
|
+
try {
|
|
83
|
+
const latestCommit = await getLatestCommitInfo(octokit, config, signal);
|
|
84
|
+
if (!latestCommit) {
|
|
85
|
+
return {
|
|
86
|
+
config,
|
|
87
|
+
state: currentState,
|
|
88
|
+
needsReimport: false,
|
|
89
|
+
error: "No commits found in repository"
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const needsReimport = !currentState.lastCommitSha || currentState.lastCommitSha !== latestCommit.sha;
|
|
93
|
+
return {
|
|
94
|
+
config,
|
|
95
|
+
state: currentState,
|
|
96
|
+
needsReimport,
|
|
97
|
+
latestCommitSha: latestCommit.sha,
|
|
98
|
+
latestCommitMessage: latestCommit.message,
|
|
99
|
+
latestCommitDate: latestCommit.date
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return {
|
|
104
|
+
config,
|
|
105
|
+
state: currentState,
|
|
106
|
+
needsReimport: false,
|
|
107
|
+
error: error.message
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Updates the import state after a successful import
|
|
113
|
+
*/
|
|
114
|
+
export async function updateImportState(workingDir, config, commitSha) {
|
|
115
|
+
const state = await loadImportState(workingDir);
|
|
116
|
+
const configId = createConfigId(config);
|
|
117
|
+
const configName = config.name || `${config.owner}/${config.repo}`;
|
|
118
|
+
state.imports[configId] = {
|
|
119
|
+
name: configName,
|
|
120
|
+
repoId: configId,
|
|
121
|
+
lastCommitSha: commitSha,
|
|
122
|
+
lastImported: new Date().toISOString(),
|
|
123
|
+
ref: config.ref || 'main'
|
|
124
|
+
};
|
|
125
|
+
await saveImportState(workingDir, state);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Performs a dry run check on all configured repositories
|
|
129
|
+
*/
|
|
130
|
+
export async function performDryRun(configs, context, octokit, workingDir = process.cwd(), signal) {
|
|
131
|
+
const { logger } = context;
|
|
132
|
+
logger.info("š Performing dry run - checking for repository changes...");
|
|
133
|
+
// Load current state
|
|
134
|
+
const state = await loadImportState(workingDir);
|
|
135
|
+
const results = [];
|
|
136
|
+
// Check each configuration
|
|
137
|
+
for (const config of configs) {
|
|
138
|
+
if (config.enabled === false) {
|
|
139
|
+
logger.debug(`Skipping disabled config: ${config.name || `${config.owner}/${config.repo}`}`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const configId = createConfigId(config);
|
|
143
|
+
const configName = config.name || `${config.owner}/${config.repo}`;
|
|
144
|
+
// Get current state for this config
|
|
145
|
+
const currentState = state.imports[configId] || {
|
|
146
|
+
name: configName,
|
|
147
|
+
repoId: configId,
|
|
148
|
+
ref: config.ref || 'main'
|
|
149
|
+
};
|
|
150
|
+
logger.debug(`Checking ${configName}...`);
|
|
151
|
+
try {
|
|
152
|
+
const changeInfo = await checkRepositoryForChanges(octokit, config, currentState, signal);
|
|
153
|
+
results.push(changeInfo);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (signal?.aborted)
|
|
157
|
+
throw error;
|
|
158
|
+
results.push({
|
|
159
|
+
config,
|
|
160
|
+
state: currentState,
|
|
161
|
+
needsReimport: false,
|
|
162
|
+
error: `Failed to check repository: ${error.message}`
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Update last checked time
|
|
167
|
+
state.lastChecked = new Date().toISOString();
|
|
168
|
+
await saveImportState(workingDir, state);
|
|
169
|
+
return results;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Formats and displays the dry run results
|
|
173
|
+
*/
|
|
174
|
+
export function displayDryRunResults(results, logger) {
|
|
175
|
+
logger.info("\nš Repository Import Status:");
|
|
176
|
+
logger.info("=".repeat(50));
|
|
177
|
+
let needsReimportCount = 0;
|
|
178
|
+
let errorCount = 0;
|
|
179
|
+
for (const result of results) {
|
|
180
|
+
const configName = result.config.name || `${result.config.owner}/${result.config.repo}`;
|
|
181
|
+
if (result.error) {
|
|
182
|
+
logger.info(`ā ${configName}: ${result.error}`);
|
|
183
|
+
errorCount++;
|
|
184
|
+
}
|
|
185
|
+
else if (result.needsReimport) {
|
|
186
|
+
logger.info(`š ${configName}: Needs re-import`);
|
|
187
|
+
if (result.latestCommitMessage) {
|
|
188
|
+
logger.info(` Latest commit: ${result.latestCommitMessage}`);
|
|
189
|
+
}
|
|
190
|
+
if (result.latestCommitDate) {
|
|
191
|
+
const date = new Date(result.latestCommitDate);
|
|
192
|
+
const timeAgo = getTimeAgo(date);
|
|
193
|
+
logger.info(` Committed: ${timeAgo}`);
|
|
194
|
+
}
|
|
195
|
+
if (result.state.lastImported) {
|
|
196
|
+
const lastImported = new Date(result.state.lastImported);
|
|
197
|
+
const importTimeAgo = getTimeAgo(lastImported);
|
|
198
|
+
logger.info(` Last imported: ${importTimeAgo}`);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
logger.info(` Last imported: Never`);
|
|
202
|
+
}
|
|
203
|
+
needsReimportCount++;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
logger.info(`ā
${configName}: Up to date`);
|
|
207
|
+
if (result.state.lastImported) {
|
|
208
|
+
const lastImported = new Date(result.state.lastImported);
|
|
209
|
+
const timeAgo = getTimeAgo(lastImported);
|
|
210
|
+
logger.info(` Last imported: ${timeAgo}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
logger.info("=".repeat(50));
|
|
215
|
+
logger.info(`š Summary: ${needsReimportCount} of ${results.length} repositories need re-import, ${errorCount} errors`);
|
|
216
|
+
if (needsReimportCount > 0) {
|
|
217
|
+
logger.info("\nš” To import updated repositories:");
|
|
218
|
+
logger.info("1. Delete the target import folders for repositories that need re-import");
|
|
219
|
+
logger.info("2. Run the import process normally (dryRun: false)");
|
|
220
|
+
logger.info("3. Fresh content will be imported automatically");
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
logger.info("\nš All repositories are up to date!");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Helper function to format time differences in a human-readable way
|
|
228
|
+
*/
|
|
229
|
+
function getTimeAgo(date) {
|
|
230
|
+
const now = new Date();
|
|
231
|
+
const diffMs = now.getTime() - date.getTime();
|
|
232
|
+
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
233
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
234
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
235
|
+
if (diffMins < 60) {
|
|
236
|
+
return `${diffMins} minutes ago`;
|
|
237
|
+
}
|
|
238
|
+
else if (diffHours < 24) {
|
|
239
|
+
return `${diffHours} hours ago`;
|
|
240
|
+
}
|
|
241
|
+
else if (diffDays < 7) {
|
|
242
|
+
return `${diffDays} days ago`;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
return date.toLocaleDateString();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { LinkMapping, LinkTransformContext, IncludePattern } from './github.types.js';
|
|
2
|
+
import type { Logger } from './github.logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Represents an imported file with its content and metadata
|
|
5
|
+
*/
|
|
6
|
+
export interface ImportedFile {
|
|
7
|
+
/** Original source path in the repository */
|
|
8
|
+
sourcePath: string;
|
|
9
|
+
/** Target path where the file will be written */
|
|
10
|
+
targetPath: string;
|
|
11
|
+
/** File content */
|
|
12
|
+
content: string;
|
|
13
|
+
/** File ID for cross-referencing */
|
|
14
|
+
id: string;
|
|
15
|
+
/** Context information for link transformations */
|
|
16
|
+
linkContext?: LinkTransformContext;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Context for global link transformation
|
|
20
|
+
*/
|
|
21
|
+
interface GlobalLinkContext {
|
|
22
|
+
/** Map from source paths to target paths for all imported files */
|
|
23
|
+
sourceToTargetMap: Map<string, string>;
|
|
24
|
+
/** Map from source paths to file IDs */
|
|
25
|
+
sourceToIdMap: Map<string, string>;
|
|
26
|
+
/** Base paths to strip from final URLs (e.g., "src/content/docs") */
|
|
27
|
+
stripPrefixes: string[];
|
|
28
|
+
/** Custom handlers for special link types */
|
|
29
|
+
customHandlers?: LinkHandler[];
|
|
30
|
+
/** Path mappings for common transformations */
|
|
31
|
+
linkMappings?: LinkMapping[];
|
|
32
|
+
/** Logger for debug output */
|
|
33
|
+
logger?: Logger;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Custom handler for specific link patterns
|
|
37
|
+
*/
|
|
38
|
+
export interface LinkHandler {
|
|
39
|
+
/** Test if this handler should process the link */
|
|
40
|
+
test: (link: string, context: LinkContext) => boolean;
|
|
41
|
+
/** Transform the link */
|
|
42
|
+
transform: (link: string, context: LinkContext) => string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Context for individual link transformation
|
|
46
|
+
*/
|
|
47
|
+
interface LinkContext {
|
|
48
|
+
/** The file containing the link */
|
|
49
|
+
currentFile: ImportedFile;
|
|
50
|
+
/** The original link text */
|
|
51
|
+
originalLink: string;
|
|
52
|
+
/** Any anchor/fragment in the link */
|
|
53
|
+
anchor: string;
|
|
54
|
+
/** Global context */
|
|
55
|
+
global: GlobalLinkContext;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Global link transformation function
|
|
59
|
+
* Processes all imported files and resolves internal links
|
|
60
|
+
*/
|
|
61
|
+
export declare function globalLinkTransform(importedFiles: ImportedFile[], options: {
|
|
62
|
+
stripPrefixes: string[];
|
|
63
|
+
customHandlers?: LinkHandler[];
|
|
64
|
+
linkMappings?: LinkMapping[];
|
|
65
|
+
logger?: Logger;
|
|
66
|
+
}): ImportedFile[];
|
|
67
|
+
/**
|
|
68
|
+
* Generate link mappings automatically from pathMappings in include patterns
|
|
69
|
+
* @param includes - Array of include patterns with pathMappings
|
|
70
|
+
* @param stripPrefixes - Prefixes to strip when generating URLs
|
|
71
|
+
* @returns Array of generated link mappings
|
|
72
|
+
*/
|
|
73
|
+
export declare function generateAutoLinkMappings(includes: IncludePattern[], stripPrefixes?: string[]): LinkMapping[];
|
|
74
|
+
/**
|
|
75
|
+
* Export types for use in configuration
|
|
76
|
+
*/
|
|
77
|
+
export type { LinkContext, GlobalLinkContext };
|