-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathversion.ts
More file actions
280 lines (252 loc) · 6.1 KB
/
version.ts
File metadata and controls
280 lines (252 loc) · 6.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
/**
* Package version and metadata detection utilities.
*
* Provides functions to locate and read `package.json` files by walking up the
* directory tree, extracting version strings, and gathering package metadata
* (homepage, repository URLs) for automatic epilog generation in help output.
*
* @packageDocumentation
*/
import { readFileSync, realpathSync } from 'node:fs';
import { readFile, realpath } from 'node:fs/promises';
import { dirname, join } from 'node:path';
/**
* Get the real path of the entry script, resolving symlinks. Falls back to cwd
* if process.argv[1] is unavailable.
*
* @function
*/
const getScriptDir = (): string => {
try {
// process.argv[1] is the script being executed
const scriptPath = process.argv[1];
if (scriptPath) {
// Resolve symlinks to find the real location
const realPath = realpathSync(scriptPath);
return dirname(realPath);
}
} catch {
// Fall through to cwd
}
return process.cwd();
};
/**
* Get the real path of the entry script asynchronously, resolving symlinks.
* Falls back to cwd if process.argv[1] is unavailable.
*
* @function
*/
const getScriptDirAsync = async (): Promise<string> => {
try {
const scriptPath = process.argv[1];
if (scriptPath) {
const realPath = await realpath(scriptPath);
return dirname(realPath);
}
} catch {
// Fall through to cwd
}
return process.cwd();
};
/**
* Package info extracted from `package.json` for epilog generation.
*/
interface PackageInfo {
homepage?: string;
repository?: string;
}
/**
* Raw `package.json` structure (partial). Represents the file as-is before
* normalization.
*/
interface RawPackageJson {
homepage?: string;
repository?: RepositoryField | string;
version?: string;
}
/**
* Raw repository field from package.json (can be string or object).
*/
interface RepositoryField {
type?: string;
url?: string;
}
/**
* Normalize a repository URL to clean HTTPS format. Strips leading `git+` and
* trailing `.git`.
*
* @function
*/
const normalizeRepoUrl = (url: string): string => {
return url.replace(/^git\+/, '').replace(/\.git$/, '');
};
/**
* Validate that a URL is HTTPS. Returns the URL if valid, undefined otherwise.
*
* @function
*/
const validateHttpsUrl = (url: string | undefined): string | undefined => {
if (!url) {
return undefined;
}
return url.startsWith('https://') ? url : undefined;
};
/**
* Extract repository URL from package.json repository field. Handles both
* string and object forms.
*
* @function
*/
const extractRepoUrl = (
repository: RepositoryField | string | undefined,
): string | undefined => {
if (!repository) {
return undefined;
}
const rawUrl = typeof repository === 'string' ? repository : repository.url;
if (!rawUrl) {
return undefined;
}
const normalized = normalizeRepoUrl(rawUrl);
return validateHttpsUrl(normalized);
};
/**
* Find package.json by walking up from startDir (async).
*
* @function
*/
const findPackageJson = async (
startDir: string,
): Promise<string | undefined> => {
let dir = startDir;
let prevDir = '';
while (dir !== prevDir) {
const pkgPath = join(dir, 'package.json');
try {
await readFile(pkgPath, 'utf-8');
return pkgPath;
} catch {
prevDir = dir;
dir = dirname(dir);
}
}
return undefined;
};
/**
* Find package.json by walking up from startDir (sync).
*
* @function
*/
const findPackageJsonSync = (startDir: string): string | undefined => {
let dir = startDir;
let prevDir = '';
while (dir !== prevDir) {
const pkgPath = join(dir, 'package.json');
try {
readFileSync(pkgPath, 'utf-8');
return pkgPath;
} catch {
prevDir = dir;
dir = dirname(dir);
}
}
return undefined;
};
/**
* Read version from package.json (async).
*
* @function
*/
const readVersionFromPackageJson = async (
pkgPath: string,
): Promise<string | undefined> => {
try {
const content = await readFile(pkgPath, 'utf-8');
const pkg = JSON.parse(content) as RawPackageJson;
return pkg.version;
} catch {
return undefined;
}
};
/**
* Read version from package.json (sync).
*
* @function
*/
const readVersionFromPackageJsonSync = (
pkgPath: string,
): string | undefined => {
try {
const content = readFileSync(pkgPath, 'utf-8');
const pkg = JSON.parse(content) as RawPackageJson;
return pkg.version;
} catch {
return undefined;
}
};
/**
* Detect version: use provided version or read from nearest package.json.
* Starts from the entry script's real location (resolving symlinks) by
* default.
*
* @function
*/
export const detectVersion = async (
providedVersion?: string,
startDir?: string,
): Promise<string | undefined> => {
if (providedVersion) {
return providedVersion;
}
const dir = startDir ?? (await getScriptDirAsync());
const pkgPath = await findPackageJson(dir);
if (!pkgPath) {
return undefined;
}
return readVersionFromPackageJson(pkgPath);
};
/**
* Detect version synchronously: use provided version or read from nearest
* package.json. Starts from the entry script's real location (resolving
* symlinks) by default.
*
* @function
*/
export const detectVersionSync = (
providedVersion?: string,
startDir?: string,
): string | undefined => {
if (providedVersion) {
return providedVersion;
}
const dir = startDir ?? getScriptDir();
const pkgPath = findPackageJsonSync(dir);
if (!pkgPath) {
return undefined;
}
return readVersionFromPackageJsonSync(pkgPath);
};
/**
* Read package info (homepage, repository) from package.json synchronously.
* Returns only HTTPS URLs; other URL schemes are omitted.
*
* @function
*/
export const readPackageInfoSync = (
startDir: string = process.cwd(),
): PackageInfo => {
const pkgPath = findPackageJsonSync(startDir);
if (!pkgPath) {
return {};
}
try {
const content = readFileSync(pkgPath, 'utf-8');
const pkg = JSON.parse(content) as RawPackageJson;
return {
homepage: validateHttpsUrl(pkg.homepage),
repository: extractRepoUrl(pkg.repository),
};
} catch {
return {};
}
};