@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.
- package/README.md +35 -55
- package/dist/github.assets.d.ts +70 -0
- package/dist/github.assets.js +253 -0
- package/dist/github.auth.js +13 -9
- package/dist/github.cleanup.d.ts +3 -2
- package/dist/github.cleanup.js +30 -23
- package/dist/github.constants.d.ts +0 -16
- package/dist/github.constants.js +0 -16
- package/dist/github.content.d.ts +5 -131
- package/dist/github.content.js +152 -794
- package/dist/github.dryrun.d.ts +9 -5
- package/dist/github.dryrun.js +49 -25
- package/dist/github.link-transform.d.ts +2 -2
- package/dist/github.link-transform.js +68 -57
- package/dist/github.loader.js +30 -46
- package/dist/github.logger.d.ts +2 -2
- package/dist/github.logger.js +33 -24
- package/dist/github.paths.d.ts +76 -0
- package/dist/github.paths.js +190 -0
- package/dist/github.storage.d.ts +16 -0
- package/dist/github.storage.js +115 -0
- package/dist/github.types.d.ts +40 -4
- package/dist/index.d.ts +8 -6
- package/dist/index.js +3 -6
- package/dist/test-helpers.d.ts +130 -0
- package/dist/test-helpers.js +194 -0
- package/package.json +3 -1
- package/src/github.assets.spec.ts +717 -0
- package/src/github.assets.ts +365 -0
- package/src/github.auth.spec.ts +245 -0
- package/src/github.auth.ts +24 -10
- package/src/github.cleanup.spec.ts +380 -0
- package/src/github.cleanup.ts +91 -47
- package/src/github.constants.ts +0 -17
- package/src/github.content.spec.ts +305 -454
- package/src/github.content.ts +259 -957
- package/src/github.dryrun.spec.ts +598 -0
- package/src/github.dryrun.ts +108 -54
- package/src/github.link-transform.spec.ts +1345 -0
- package/src/github.link-transform.ts +177 -95
- package/src/github.loader.spec.ts +75 -50
- package/src/github.loader.ts +101 -76
- package/src/github.logger.spec.ts +795 -0
- package/src/github.logger.ts +77 -35
- package/src/github.paths.spec.ts +523 -0
- package/src/github.paths.ts +259 -0
- package/src/github.storage.spec.ts +377 -0
- package/src/github.storage.ts +135 -0
- package/src/github.types.ts +54 -9
- package/src/index.ts +43 -6
- package/src/test-helpers.ts +215 -0
package/dist/github.dryrun.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Octokit } from "octokit";
|
|
2
|
+
import type { Logger } from "./github.logger.js";
|
|
1
3
|
import type { ImportOptions, LoaderContext } from "./github.types.js";
|
|
2
4
|
/**
|
|
3
5
|
* Represents the state of a single import configuration
|
|
@@ -49,11 +51,11 @@ export declare function createConfigId(config: ImportOptions): string;
|
|
|
49
51
|
/**
|
|
50
52
|
* Loads the import state from the state file
|
|
51
53
|
*/
|
|
52
|
-
export declare function loadImportState(workingDir: string): Promise<StateFile>;
|
|
54
|
+
export declare function loadImportState(workingDir: string, logger?: Logger): Promise<StateFile>;
|
|
53
55
|
/**
|
|
54
56
|
* Gets the latest commit information for a repository path
|
|
55
57
|
*/
|
|
56
|
-
export declare function getLatestCommitInfo(octokit:
|
|
58
|
+
export declare function getLatestCommitInfo(octokit: Octokit, config: ImportOptions, signal?: AbortSignal): Promise<{
|
|
57
59
|
sha: string;
|
|
58
60
|
message: string;
|
|
59
61
|
date: string;
|
|
@@ -61,12 +63,14 @@ export declare function getLatestCommitInfo(octokit: any, config: ImportOptions,
|
|
|
61
63
|
/**
|
|
62
64
|
* Updates the import state after a successful import
|
|
63
65
|
*/
|
|
64
|
-
export declare function updateImportState(workingDir: string, config: ImportOptions, commitSha?: string): Promise<void>;
|
|
66
|
+
export declare function updateImportState(workingDir: string, config: ImportOptions, commitSha?: string, logger?: Logger): Promise<void>;
|
|
65
67
|
/**
|
|
66
68
|
* Performs a dry run check on all configured repositories
|
|
67
69
|
*/
|
|
68
|
-
export declare function performDryRun(configs: ImportOptions[], context: LoaderContext, octokit:
|
|
70
|
+
export declare function performDryRun(configs: ImportOptions[], context: LoaderContext, octokit: Octokit, workingDir?: string, signal?: AbortSignal): Promise<RepositoryChangeInfo[]>;
|
|
69
71
|
/**
|
|
70
72
|
* Formats and displays the dry run results
|
|
71
73
|
*/
|
|
72
|
-
export declare function displayDryRunResults(results: RepositoryChangeInfo[], logger:
|
|
74
|
+
export declare function displayDryRunResults(results: RepositoryChangeInfo[], logger: {
|
|
75
|
+
info: (msg: string) => void;
|
|
76
|
+
}): void;
|
package/dist/github.dryrun.js
CHANGED
|
@@ -1,46 +1,64 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
const STATE_FILENAME =
|
|
4
|
+
const STATE_FILENAME = ".github-import-state.json";
|
|
5
5
|
/**
|
|
6
6
|
* Creates a unique identifier for an import configuration
|
|
7
7
|
*/
|
|
8
8
|
export function createConfigId(config) {
|
|
9
|
-
|
|
9
|
+
if (config.stateKey) {
|
|
10
|
+
return config.stateKey;
|
|
11
|
+
}
|
|
12
|
+
return `${config.owner}/${config.repo}@${config.ref || "main"}`;
|
|
10
13
|
}
|
|
11
14
|
/**
|
|
12
15
|
* Loads the import state from the state file
|
|
13
16
|
*/
|
|
14
|
-
export async function loadImportState(workingDir) {
|
|
17
|
+
export async function loadImportState(workingDir, logger) {
|
|
15
18
|
const statePath = join(workingDir, STATE_FILENAME);
|
|
16
19
|
if (!existsSync(statePath)) {
|
|
17
20
|
return {
|
|
18
21
|
imports: {},
|
|
19
|
-
lastChecked: new Date().toISOString()
|
|
22
|
+
lastChecked: new Date().toISOString(),
|
|
20
23
|
};
|
|
21
24
|
}
|
|
22
25
|
try {
|
|
23
|
-
const content = await fs.readFile(statePath,
|
|
24
|
-
|
|
26
|
+
const content = await fs.readFile(statePath, "utf-8");
|
|
27
|
+
const parsed = JSON.parse(content);
|
|
28
|
+
// Validate the parsed state has the expected shape
|
|
29
|
+
if (!parsed ||
|
|
30
|
+
typeof parsed !== "object" ||
|
|
31
|
+
!("imports" in parsed) ||
|
|
32
|
+
typeof parsed.imports !== "object") {
|
|
33
|
+
const msg = `Malformed state file at ${statePath}, starting fresh`;
|
|
34
|
+
// eslint-disable-next-line no-console -- fallback when no logger provided
|
|
35
|
+
logger ? logger.warn(msg) : console.warn(msg);
|
|
36
|
+
return { imports: {}, lastChecked: new Date().toISOString() };
|
|
37
|
+
}
|
|
38
|
+
return parsed;
|
|
25
39
|
}
|
|
26
40
|
catch (error) {
|
|
27
|
-
|
|
41
|
+
const msg = `Failed to load import state from ${statePath}, starting fresh: ${error}`;
|
|
42
|
+
// eslint-disable-next-line no-console -- fallback when no logger provided
|
|
43
|
+
logger ? logger.warn(msg) : console.warn(msg);
|
|
28
44
|
return {
|
|
29
45
|
imports: {},
|
|
30
|
-
lastChecked: new Date().toISOString()
|
|
46
|
+
lastChecked: new Date().toISOString(),
|
|
31
47
|
};
|
|
32
48
|
}
|
|
33
49
|
}
|
|
34
50
|
/**
|
|
35
51
|
* Saves the import state to the state file
|
|
36
52
|
*/
|
|
37
|
-
async function saveImportState(workingDir, state) {
|
|
53
|
+
async function saveImportState(workingDir, state, logger) {
|
|
38
54
|
const statePath = join(workingDir, STATE_FILENAME);
|
|
39
55
|
try {
|
|
40
|
-
await fs.writeFile(statePath, JSON.stringify(state, null, 2),
|
|
56
|
+
await fs.writeFile(statePath, JSON.stringify(state, null, 2), "utf-8");
|
|
41
57
|
}
|
|
42
58
|
catch (error) {
|
|
43
|
-
|
|
59
|
+
const msg = `Failed to save import state to ${statePath}: ${error}`;
|
|
60
|
+
// eslint-disable-next-line no-console -- fallback when no logger provided
|
|
61
|
+
logger ? logger.warn(msg) : console.warn(msg);
|
|
44
62
|
}
|
|
45
63
|
}
|
|
46
64
|
/**
|
|
@@ -55,7 +73,7 @@ export async function getLatestCommitInfo(octokit, config, signal) {
|
|
|
55
73
|
repo,
|
|
56
74
|
sha: ref,
|
|
57
75
|
per_page: 1,
|
|
58
|
-
request: { signal }
|
|
76
|
+
request: { signal },
|
|
59
77
|
});
|
|
60
78
|
if (data.length === 0) {
|
|
61
79
|
return null;
|
|
@@ -63,12 +81,17 @@ export async function getLatestCommitInfo(octokit, config, signal) {
|
|
|
63
81
|
const latestCommit = data[0];
|
|
64
82
|
return {
|
|
65
83
|
sha: latestCommit.sha,
|
|
66
|
-
message: latestCommit.commit.message.split(
|
|
67
|
-
date: latestCommit.commit.committer?.date ||
|
|
84
|
+
message: latestCommit.commit.message.split("\n")[0], // First line only
|
|
85
|
+
date: latestCommit.commit.committer?.date ||
|
|
86
|
+
latestCommit.commit.author?.date ||
|
|
87
|
+
new Date().toISOString(),
|
|
68
88
|
};
|
|
69
89
|
}
|
|
70
90
|
catch (error) {
|
|
71
|
-
if (error
|
|
91
|
+
if (typeof error === "object" &&
|
|
92
|
+
error !== null &&
|
|
93
|
+
"status" in error &&
|
|
94
|
+
error.status === 404) {
|
|
72
95
|
throw new Error(`Repository not found: ${owner}/${repo}`);
|
|
73
96
|
}
|
|
74
97
|
throw error;
|
|
@@ -86,17 +109,18 @@ async function checkRepositoryForChanges(octokit, config, currentState, signal)
|
|
|
86
109
|
config,
|
|
87
110
|
state: currentState,
|
|
88
111
|
needsReimport: false,
|
|
89
|
-
error: "No commits found in repository"
|
|
112
|
+
error: "No commits found in repository",
|
|
90
113
|
};
|
|
91
114
|
}
|
|
92
|
-
const needsReimport = !currentState.lastCommitSha ||
|
|
115
|
+
const needsReimport = !currentState.lastCommitSha ||
|
|
116
|
+
currentState.lastCommitSha !== latestCommit.sha;
|
|
93
117
|
return {
|
|
94
118
|
config,
|
|
95
119
|
state: currentState,
|
|
96
120
|
needsReimport,
|
|
97
121
|
latestCommitSha: latestCommit.sha,
|
|
98
122
|
latestCommitMessage: latestCommit.message,
|
|
99
|
-
latestCommitDate: latestCommit.date
|
|
123
|
+
latestCommitDate: latestCommit.date,
|
|
100
124
|
};
|
|
101
125
|
}
|
|
102
126
|
catch (error) {
|
|
@@ -104,15 +128,15 @@ async function checkRepositoryForChanges(octokit, config, currentState, signal)
|
|
|
104
128
|
config,
|
|
105
129
|
state: currentState,
|
|
106
130
|
needsReimport: false,
|
|
107
|
-
error: error.message
|
|
131
|
+
error: error instanceof Error ? error.message : String(error),
|
|
108
132
|
};
|
|
109
133
|
}
|
|
110
134
|
}
|
|
111
135
|
/**
|
|
112
136
|
* Updates the import state after a successful import
|
|
113
137
|
*/
|
|
114
|
-
export async function updateImportState(workingDir, config, commitSha) {
|
|
115
|
-
const state = await loadImportState(workingDir);
|
|
138
|
+
export async function updateImportState(workingDir, config, commitSha, logger) {
|
|
139
|
+
const state = await loadImportState(workingDir, logger);
|
|
116
140
|
const configId = createConfigId(config);
|
|
117
141
|
const configName = config.name || `${config.owner}/${config.repo}`;
|
|
118
142
|
state.imports[configId] = {
|
|
@@ -120,9 +144,9 @@ export async function updateImportState(workingDir, config, commitSha) {
|
|
|
120
144
|
repoId: configId,
|
|
121
145
|
lastCommitSha: commitSha,
|
|
122
146
|
lastImported: new Date().toISOString(),
|
|
123
|
-
ref: config.ref ||
|
|
147
|
+
ref: config.ref || "main",
|
|
124
148
|
};
|
|
125
|
-
await saveImportState(workingDir, state);
|
|
149
|
+
await saveImportState(workingDir, state, logger);
|
|
126
150
|
}
|
|
127
151
|
/**
|
|
128
152
|
* Performs a dry run check on all configured repositories
|
|
@@ -145,7 +169,7 @@ export async function performDryRun(configs, context, octokit, workingDir = proc
|
|
|
145
169
|
const currentState = state.imports[configId] || {
|
|
146
170
|
name: configName,
|
|
147
171
|
repoId: configId,
|
|
148
|
-
ref: config.ref ||
|
|
172
|
+
ref: config.ref || "main",
|
|
149
173
|
};
|
|
150
174
|
logger.debug(`Checking ${configName}...`);
|
|
151
175
|
try {
|
|
@@ -159,7 +183,7 @@ export async function performDryRun(configs, context, octokit, workingDir = proc
|
|
|
159
183
|
config,
|
|
160
184
|
state: currentState,
|
|
161
185
|
needsReimport: false,
|
|
162
|
-
error: `Failed to check repository: ${error.message}
|
|
186
|
+
error: `Failed to check repository: ${error instanceof Error ? error.message : String(error)}`,
|
|
163
187
|
});
|
|
164
188
|
}
|
|
165
189
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { LinkMapping, LinkTransformContext, IncludePattern } from
|
|
2
|
-
import type { Logger } from
|
|
1
|
+
import type { LinkMapping, LinkTransformContext, IncludePattern } from "./github.types.js";
|
|
2
|
+
import type { Logger } from "./github.logger.js";
|
|
3
3
|
/**
|
|
4
4
|
* Represents an imported file with its content and metadata
|
|
5
5
|
*/
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { slug } from
|
|
2
|
-
import path from
|
|
1
|
+
import { slug } from "github-slugger";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
/**
|
|
4
4
|
* Extract anchor fragment from a link
|
|
5
5
|
*/
|
|
6
6
|
function extractAnchor(link) {
|
|
7
7
|
const anchorMatch = link.match(/#.*$/);
|
|
8
|
-
const anchor = anchorMatch ? anchorMatch[0] :
|
|
9
|
-
const path = link.replace(/#.*$/,
|
|
8
|
+
const anchor = anchorMatch ? anchorMatch[0] : "";
|
|
9
|
+
const path = link.replace(/#.*$/, "");
|
|
10
10
|
return { path, anchor };
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
@@ -22,9 +22,9 @@ function isExternalLink(link) {
|
|
|
22
22
|
/^ftp:/.test(link) ||
|
|
23
23
|
/^ftps:\/\//.test(link) ||
|
|
24
24
|
// Any protocol with ://
|
|
25
|
-
link.includes(
|
|
25
|
+
link.includes("://") ||
|
|
26
26
|
// Anchor-only links (same page)
|
|
27
|
-
link.startsWith(
|
|
27
|
+
link.startsWith("#") ||
|
|
28
28
|
// Data URLs
|
|
29
29
|
/^data:/.test(link) ||
|
|
30
30
|
// File protocol
|
|
@@ -37,7 +37,9 @@ function normalizePath(linkPath, currentFilePath, logger) {
|
|
|
37
37
|
logger?.debug(`[normalizePath] BEFORE: linkPath="${linkPath}", currentFilePath="${currentFilePath}"`);
|
|
38
38
|
// Handle relative paths (including simple relative paths without ./ prefix)
|
|
39
39
|
// A link is relative if it doesn't start with / or contain a protocol
|
|
40
|
-
const isAbsoluteOrExternal = linkPath.startsWith(
|
|
40
|
+
const isAbsoluteOrExternal = linkPath.startsWith("/") ||
|
|
41
|
+
linkPath.includes("://") ||
|
|
42
|
+
linkPath.startsWith("#");
|
|
41
43
|
if (!isAbsoluteOrExternal) {
|
|
42
44
|
const currentDir = path.dirname(currentFilePath);
|
|
43
45
|
const resolved = path.posix.normalize(path.posix.join(currentDir, linkPath));
|
|
@@ -63,20 +65,20 @@ function applyLinkMappings(linkUrl, linkMappings, context) {
|
|
|
63
65
|
// Handle relative links automatically if enabled
|
|
64
66
|
if (mapping.relativeLinks && context.currentFile.linkContext) {
|
|
65
67
|
// Check if this is a relative link (doesn't start with /, http, etc.)
|
|
66
|
-
if (!linkPath.startsWith(
|
|
68
|
+
if (!linkPath.startsWith("/") && !isExternalLink(linkPath)) {
|
|
67
69
|
// Check if the link points to a known directory structure
|
|
68
|
-
const knownPaths = [
|
|
69
|
-
const isKnownPath = knownPaths.some(p => linkPath.startsWith(p));
|
|
70
|
+
const knownPaths = ["modules/", "classes/", "interfaces/", "enums/"];
|
|
71
|
+
const isKnownPath = knownPaths.some((p) => linkPath.startsWith(p));
|
|
70
72
|
if (isKnownPath) {
|
|
71
73
|
// Strip .md extension from the link path
|
|
72
|
-
const cleanLinkPath = linkPath.replace(/\.md$/,
|
|
74
|
+
const cleanLinkPath = linkPath.replace(/\.md$/, "");
|
|
73
75
|
// Convert relative path to absolute path using the target base
|
|
74
76
|
const targetBase = generateSiteUrl(context.currentFile.linkContext.basePath, context.global.stripPrefixes);
|
|
75
77
|
// Construct final URL with proper Starlight formatting
|
|
76
|
-
let finalUrl = targetBase.replace(/\/$/,
|
|
78
|
+
let finalUrl = targetBase.replace(/\/$/, "") + "/" + cleanLinkPath;
|
|
77
79
|
// Add trailing slash if it doesn't end with one and isn't empty
|
|
78
|
-
if (finalUrl && !finalUrl.endsWith(
|
|
79
|
-
finalUrl +=
|
|
80
|
+
if (finalUrl && !finalUrl.endsWith("/")) {
|
|
81
|
+
finalUrl += "/";
|
|
80
82
|
}
|
|
81
83
|
transformedPath = finalUrl;
|
|
82
84
|
return transformedPath + anchor;
|
|
@@ -84,16 +86,21 @@ function applyLinkMappings(linkUrl, linkMappings, context) {
|
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
let matched = false;
|
|
87
|
-
let replacement =
|
|
88
|
-
|
|
89
|
+
let replacement = "";
|
|
90
|
+
const getLinkTransformContext = () => context.currentFile.linkContext ?? {
|
|
91
|
+
sourcePath: context.currentFile.sourcePath,
|
|
92
|
+
targetPath: context.currentFile.targetPath,
|
|
93
|
+
basePath: "",
|
|
94
|
+
};
|
|
95
|
+
if (typeof mapping.pattern === "string") {
|
|
89
96
|
// String pattern - exact match or contains
|
|
90
97
|
if (transformedPath.includes(mapping.pattern)) {
|
|
91
98
|
matched = true;
|
|
92
|
-
if (typeof mapping.replacement ===
|
|
99
|
+
if (typeof mapping.replacement === "string") {
|
|
93
100
|
replacement = transformedPath.replace(mapping.pattern, mapping.replacement);
|
|
94
101
|
}
|
|
95
102
|
else {
|
|
96
|
-
replacement = mapping.replacement(transformedPath, anchor,
|
|
103
|
+
replacement = mapping.replacement(transformedPath, anchor, getLinkTransformContext());
|
|
97
104
|
}
|
|
98
105
|
}
|
|
99
106
|
}
|
|
@@ -102,11 +109,11 @@ function applyLinkMappings(linkUrl, linkMappings, context) {
|
|
|
102
109
|
const match = transformedPath.match(mapping.pattern);
|
|
103
110
|
if (match) {
|
|
104
111
|
matched = true;
|
|
105
|
-
if (typeof mapping.replacement ===
|
|
112
|
+
if (typeof mapping.replacement === "string") {
|
|
106
113
|
replacement = transformedPath.replace(mapping.pattern, mapping.replacement);
|
|
107
114
|
}
|
|
108
115
|
else {
|
|
109
|
-
replacement = mapping.replacement(transformedPath, anchor,
|
|
116
|
+
replacement = mapping.replacement(transformedPath, anchor, getLinkTransformContext());
|
|
110
117
|
}
|
|
111
118
|
}
|
|
112
119
|
}
|
|
@@ -131,29 +138,31 @@ function generateSiteUrl(targetPath, stripPrefixes) {
|
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
140
|
// Remove leading slash if present
|
|
134
|
-
url = url.replace(/^\//,
|
|
141
|
+
url = url.replace(/^\//, "");
|
|
135
142
|
// Remove file extension
|
|
136
|
-
url = url.replace(/\.(md|mdx)$/i,
|
|
143
|
+
url = url.replace(/\.(md|mdx)$/i, "");
|
|
137
144
|
// Handle index files - they should resolve to parent directory
|
|
138
|
-
if (url.endsWith(
|
|
139
|
-
url = url.replace(
|
|
145
|
+
if (url.endsWith("/index")) {
|
|
146
|
+
url = url.replace("/index", "");
|
|
140
147
|
}
|
|
141
|
-
else if (url ===
|
|
142
|
-
url =
|
|
148
|
+
else if (url === "index") {
|
|
149
|
+
url = "";
|
|
143
150
|
}
|
|
144
151
|
// Split path into segments and slugify each
|
|
145
|
-
const segments = url
|
|
152
|
+
const segments = url
|
|
153
|
+
.split("/")
|
|
154
|
+
.map((segment) => (segment ? slug(segment) : ""));
|
|
146
155
|
// Reconstruct URL
|
|
147
|
-
url = segments.filter(s => s).join(
|
|
156
|
+
url = segments.filter((s) => s).join("/");
|
|
148
157
|
// Ensure leading slash
|
|
149
|
-
if (url && !url.startsWith(
|
|
150
|
-
url =
|
|
158
|
+
if (url && !url.startsWith("/")) {
|
|
159
|
+
url = "/" + url;
|
|
151
160
|
}
|
|
152
161
|
// Add trailing slash for non-empty paths
|
|
153
|
-
if (url && !url.endsWith(
|
|
154
|
-
url = url +
|
|
162
|
+
if (url && !url.endsWith("/")) {
|
|
163
|
+
url = url + "/";
|
|
155
164
|
}
|
|
156
|
-
return url ||
|
|
165
|
+
return url || "/";
|
|
157
166
|
}
|
|
158
167
|
/**
|
|
159
168
|
* Transform a single markdown link
|
|
@@ -177,7 +186,7 @@ function transformLink(linkText, linkUrl, context) {
|
|
|
177
186
|
// Apply global path mappings to the normalized path
|
|
178
187
|
let processedNormalizedPath = normalizedPath;
|
|
179
188
|
if (context.global.linkMappings) {
|
|
180
|
-
const globalMappings = context.global.linkMappings.filter(m => m.global);
|
|
189
|
+
const globalMappings = context.global.linkMappings.filter((m) => m.global);
|
|
181
190
|
if (globalMappings.length > 0) {
|
|
182
191
|
processedNormalizedPath = applyLinkMappings(normalizedPath + anchor, globalMappings, context);
|
|
183
192
|
// Extract path again after global mappings
|
|
@@ -188,8 +197,8 @@ function transformLink(linkText, linkUrl, context) {
|
|
|
188
197
|
// Check if this links to an imported file
|
|
189
198
|
let targetPath = context.global.sourceToTargetMap.get(normalizedPath);
|
|
190
199
|
// If not found and path ends with /, try looking for index.md
|
|
191
|
-
if (!targetPath && normalizedPath.endsWith(
|
|
192
|
-
targetPath = context.global.sourceToTargetMap.get(normalizedPath +
|
|
200
|
+
if (!targetPath && normalizedPath.endsWith("/")) {
|
|
201
|
+
targetPath = context.global.sourceToTargetMap.get(normalizedPath + "index.md");
|
|
193
202
|
}
|
|
194
203
|
if (targetPath) {
|
|
195
204
|
// This is an internal link to an imported file
|
|
@@ -198,10 +207,10 @@ function transformLink(linkText, linkUrl, context) {
|
|
|
198
207
|
}
|
|
199
208
|
// Apply non-global path mappings to unresolved links
|
|
200
209
|
if (context.global.linkMappings) {
|
|
201
|
-
const nonGlobalMappings = context.global.linkMappings.filter(m => !m.global);
|
|
210
|
+
const nonGlobalMappings = context.global.linkMappings.filter((m) => !m.global);
|
|
202
211
|
if (nonGlobalMappings.length > 0) {
|
|
203
212
|
const mappedUrl = applyLinkMappings(processedNormalizedPath + anchor, nonGlobalMappings, context);
|
|
204
|
-
if (mappedUrl !==
|
|
213
|
+
if (mappedUrl !== processedNormalizedPath + anchor) {
|
|
205
214
|
return `[${linkText}](${mappedUrl})`;
|
|
206
215
|
}
|
|
207
216
|
}
|
|
@@ -218,7 +227,7 @@ function transformLink(linkText, linkUrl, context) {
|
|
|
218
227
|
}
|
|
219
228
|
// No transformation matched - strip .md extension from unresolved internal links
|
|
220
229
|
// This handles links to files that weren't imported but should still use Starlight routing
|
|
221
|
-
const cleanPath = processedNormalizedPath.replace(/\.md$/i,
|
|
230
|
+
const cleanPath = processedNormalizedPath.replace(/\.md$/i, "");
|
|
222
231
|
return `[${linkText}](${cleanPath + anchor})`;
|
|
223
232
|
}
|
|
224
233
|
/**
|
|
@@ -243,7 +252,7 @@ export function globalLinkTransform(importedFiles, options) {
|
|
|
243
252
|
};
|
|
244
253
|
// Transform links in all files
|
|
245
254
|
const markdownLinkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
|
|
246
|
-
return importedFiles.map(file => ({
|
|
255
|
+
return importedFiles.map((file) => ({
|
|
247
256
|
...file,
|
|
248
257
|
content: file.content.replace(markdownLinkRegex, (match, linkText, linkUrl) => {
|
|
249
258
|
const linkContext = {
|
|
@@ -262,9 +271,7 @@ export function globalLinkTransform(importedFiles, options) {
|
|
|
262
271
|
* @returns Inferred cross-section path (e.g., '/reference/api')
|
|
263
272
|
*/
|
|
264
273
|
function inferCrossSectionPath(basePath) {
|
|
265
|
-
return basePath
|
|
266
|
-
.replace(/^src\/content\/docs/, '')
|
|
267
|
-
.replace(/\/$/, '') || '/';
|
|
274
|
+
return basePath.replace(/^src\/content\/docs/, "").replace(/\/$/, "") || "/";
|
|
268
275
|
}
|
|
269
276
|
/**
|
|
270
277
|
* Generate link mappings automatically from pathMappings in include patterns
|
|
@@ -280,25 +287,29 @@ export function generateAutoLinkMappings(includes, stripPrefixes = []) {
|
|
|
280
287
|
const inferredCrossSection = inferCrossSectionPath(includePattern.basePath);
|
|
281
288
|
for (const [sourcePath, mappingValue] of Object.entries(includePattern.pathMappings)) {
|
|
282
289
|
// Handle both string and enhanced object formats
|
|
283
|
-
const targetPath = typeof mappingValue ===
|
|
284
|
-
const crossSectionPath = typeof mappingValue ===
|
|
290
|
+
const targetPath = typeof mappingValue === "string" ? mappingValue : mappingValue.target;
|
|
291
|
+
const crossSectionPath = typeof mappingValue === "object" && mappingValue.crossSectionPath
|
|
285
292
|
? mappingValue.crossSectionPath
|
|
286
293
|
: inferredCrossSection;
|
|
287
|
-
if (sourcePath.endsWith(
|
|
294
|
+
if (sourcePath.endsWith("/")) {
|
|
288
295
|
// Folder mapping - use regex with capture group
|
|
289
|
-
const sourcePattern = sourcePath.replace(/[.*+?^${}()|[\]\\]/g,
|
|
296
|
+
const sourcePattern = sourcePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
290
297
|
linkMappings.push({
|
|
291
298
|
pattern: new RegExp(`^${sourcePattern}(.+)$`),
|
|
292
|
-
replacement: (transformedPath,
|
|
293
|
-
const relativePath = transformedPath.replace(new RegExp(`^${sourcePattern}`),
|
|
299
|
+
replacement: (transformedPath, _anchor, _context) => {
|
|
300
|
+
const relativePath = transformedPath.replace(new RegExp(`^${sourcePattern}`), "");
|
|
294
301
|
let finalPath;
|
|
295
|
-
if (crossSectionPath && crossSectionPath !==
|
|
296
|
-
finalPath =
|
|
297
|
-
|
|
298
|
-
|
|
302
|
+
if (crossSectionPath && crossSectionPath !== "/") {
|
|
303
|
+
finalPath =
|
|
304
|
+
targetPath === ""
|
|
305
|
+
? `${crossSectionPath}/${relativePath}`
|
|
306
|
+
: `${crossSectionPath}/${targetPath}${relativePath}`;
|
|
299
307
|
}
|
|
300
308
|
else {
|
|
301
|
-
finalPath =
|
|
309
|
+
finalPath =
|
|
310
|
+
targetPath === ""
|
|
311
|
+
? relativePath
|
|
312
|
+
: `${targetPath}${relativePath}`;
|
|
302
313
|
}
|
|
303
314
|
return generateSiteUrl(finalPath, stripPrefixes);
|
|
304
315
|
},
|
|
@@ -307,11 +318,11 @@ export function generateAutoLinkMappings(includes, stripPrefixes = []) {
|
|
|
307
318
|
}
|
|
308
319
|
else {
|
|
309
320
|
// File mapping - exact string match
|
|
310
|
-
const sourcePattern = sourcePath.replace(/[.*+?^${}()|[\]\\]/g,
|
|
321
|
+
const sourcePattern = sourcePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
311
322
|
linkMappings.push({
|
|
312
323
|
pattern: new RegExp(`^${sourcePattern}$`),
|
|
313
|
-
replacement: (
|
|
314
|
-
const finalPath = crossSectionPath && crossSectionPath !==
|
|
324
|
+
replacement: (_transformedPath, _anchor, _context) => {
|
|
325
|
+
const finalPath = crossSectionPath && crossSectionPath !== "/"
|
|
315
326
|
? `${crossSectionPath}/${targetPath}`
|
|
316
327
|
: targetPath;
|
|
317
328
|
return generateSiteUrl(finalPath, stripPrefixes);
|