@atcute/lex-cli 2.3.3 → 2.5.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 +70 -4
- package/dist/cli.js +13 -162
- package/dist/cli.js.map +1 -1
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +76 -78
- package/dist/codegen.js.map +1 -1
- package/dist/commands/export.d.ts +13 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +76 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/generate.d.ts +13 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +136 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/pull.d.ts +13 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/pull.js +163 -0
- package/dist/commands/pull.js.map +1 -0
- package/dist/config.d.ts +99 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +82 -25
- package/dist/config.js.map +1 -1
- package/dist/git.d.ts +27 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +73 -0
- package/dist/git.js.map +1 -0
- package/dist/lexicon-loader.d.ts +17 -0
- package/dist/lexicon-loader.d.ts.map +1 -0
- package/dist/lexicon-loader.js +167 -0
- package/dist/lexicon-loader.js.map +1 -0
- package/dist/pull-sources/atproto.d.ts +9 -0
- package/dist/pull-sources/atproto.d.ts.map +1 -0
- package/dist/pull-sources/atproto.js +192 -0
- package/dist/pull-sources/atproto.js.map +1 -0
- package/dist/pull-sources/git.d.ts +11 -0
- package/dist/pull-sources/git.d.ts.map +1 -0
- package/dist/pull-sources/git.js +80 -0
- package/dist/pull-sources/git.js.map +1 -0
- package/dist/pull-sources/types.d.ts +16 -0
- package/dist/pull-sources/types.d.ts.map +1 -0
- package/dist/pull-sources/types.js +2 -0
- package/dist/pull-sources/types.js.map +1 -0
- package/dist/shared-options.d.ts +6 -0
- package/dist/shared-options.d.ts.map +1 -0
- package/dist/shared-options.js +11 -0
- package/dist/shared-options.js.map +1 -0
- package/package.json +10 -7
- package/src/cli.ts +11 -198
- package/src/codegen.ts +90 -88
- package/src/commands/export.ts +106 -0
- package/src/commands/generate.ts +170 -0
- package/src/commands/pull.ts +231 -0
- package/src/config.ts +102 -30
- package/src/git.ts +104 -0
- package/src/lexicon-loader.ts +199 -0
- package/src/pull-sources/atproto.ts +243 -0
- package/src/pull-sources/git.ts +103 -0
- package/src/pull-sources/types.ts +18 -0
- package/src/shared-options.ts +13 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { lexiconDoc, refineLexiconDoc, type LexiconDoc } from '@atcute/lexicon-doc';
|
|
5
|
+
import { merge, object } from '@optique/core/constructs';
|
|
6
|
+
import { message } from '@optique/core/message';
|
|
7
|
+
import { type InferValue } from '@optique/core/parser';
|
|
8
|
+
import { command, constant } from '@optique/core/primitives';
|
|
9
|
+
import pc from 'picocolors';
|
|
10
|
+
import prettier from 'prettier';
|
|
11
|
+
|
|
12
|
+
import { loadConfig, type NormalizedConfig, type PullConfig, type SourceConfig } from '../config.js';
|
|
13
|
+
import { pullAtprotoSource } from '../pull-sources/atproto.js';
|
|
14
|
+
import { pullGitSource } from '../pull-sources/git.js';
|
|
15
|
+
import type { PullResult, PulledLexicon, SourceLocation } from '../pull-sources/types.js';
|
|
16
|
+
import { sharedOptions } from '../shared-options.js';
|
|
17
|
+
|
|
18
|
+
export const pullCommandSchema = command(
|
|
19
|
+
'pull',
|
|
20
|
+
merge(
|
|
21
|
+
object({
|
|
22
|
+
type: constant('pull'),
|
|
23
|
+
}),
|
|
24
|
+
sharedOptions,
|
|
25
|
+
),
|
|
26
|
+
{
|
|
27
|
+
brief: message`pull lexicon documents from configured sources`,
|
|
28
|
+
description: message`fetches lexicon documents from configured git repositories and writes them to the output directory.`,
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export type PullCommand = InferValue<typeof pullCommandSchema>;
|
|
33
|
+
|
|
34
|
+
interface SourceRevision {
|
|
35
|
+
source: SourceConfig;
|
|
36
|
+
rev?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ensurePullConfig = (config: NormalizedConfig): PullConfig => {
|
|
40
|
+
if (!config.pull) {
|
|
41
|
+
console.error(pc.bold(pc.red(`pull configuration missing`)));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return config.pull;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const parseLexiconFile = async (loc: SourceLocation): Promise<LexiconDoc> => {
|
|
49
|
+
let source: string;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
source = await fs.readFile(loc.absolutePath, 'utf8');
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error(
|
|
55
|
+
pc.bold(pc.red(`file read error for ${loc.relativePath} when pulling ${loc.sourceDescription}`)),
|
|
56
|
+
);
|
|
57
|
+
console.error(`found in ${loc.absolutePath}`);
|
|
58
|
+
console.error(err);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let json: unknown;
|
|
63
|
+
try {
|
|
64
|
+
json = JSON.parse(source);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(
|
|
67
|
+
pc.bold(pc.red(`json parse error in ${loc.relativePath} when pulling ${loc.sourceDescription}`)),
|
|
68
|
+
);
|
|
69
|
+
console.error(`found in ${loc.absolutePath}`);
|
|
70
|
+
console.error(err);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = lexiconDoc.try(json, { mode: 'passthrough' });
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
console.error(
|
|
77
|
+
pc.bold(
|
|
78
|
+
pc.red(`schema validation failed for ${loc.relativePath} when pulling ${loc.sourceDescription}`),
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
console.error(`found in ${loc.absolutePath}`);
|
|
82
|
+
console.error(result.message);
|
|
83
|
+
|
|
84
|
+
for (const issue of result.issues) {
|
|
85
|
+
console.log(`- ${issue.code} at .${issue.path.join('.')}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const issues = refineLexiconDoc(result.value, true);
|
|
92
|
+
if (issues.length > 0) {
|
|
93
|
+
console.error(
|
|
94
|
+
pc.bold(pc.red(`lint validation failed for ${loc.relativePath} when pulling ${loc.sourceDescription}`)),
|
|
95
|
+
);
|
|
96
|
+
console.error(`found in ${loc.absolutePath}`);
|
|
97
|
+
|
|
98
|
+
for (const issue of issues) {
|
|
99
|
+
console.log(`- ${issue.message} at .${issue.path.join('.')}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result.value;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const writeLexicon = async (
|
|
109
|
+
outdir: string,
|
|
110
|
+
nsid: string,
|
|
111
|
+
doc: LexiconDoc,
|
|
112
|
+
prettierConfig: prettier.Options | null,
|
|
113
|
+
): Promise<void> => {
|
|
114
|
+
const nsidPath = nsid.replaceAll('.', '/');
|
|
115
|
+
const target = path.join(outdir, `${nsidPath}.json`);
|
|
116
|
+
const dirname = path.dirname(target);
|
|
117
|
+
|
|
118
|
+
const code = await prettier.format(JSON.stringify(doc, null, 2), {
|
|
119
|
+
...(prettierConfig ?? {}),
|
|
120
|
+
parser: 'json',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await fs.mkdir(dirname, { recursive: true });
|
|
124
|
+
await fs.writeFile(target, code);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const pullSource = async (source: SourceConfig): Promise<PullResult> => {
|
|
128
|
+
switch (source.type) {
|
|
129
|
+
case 'git': {
|
|
130
|
+
return pullGitSource(source, parseLexiconFile);
|
|
131
|
+
}
|
|
132
|
+
case 'atproto': {
|
|
133
|
+
return pullAtprotoSource(source);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const writeSourceReadme = async (
|
|
139
|
+
outdir: string,
|
|
140
|
+
revisions: SourceRevision[],
|
|
141
|
+
prettierConfig: prettier.Options | null,
|
|
142
|
+
): Promise<void> => {
|
|
143
|
+
const lines = [
|
|
144
|
+
'# lexicon sources',
|
|
145
|
+
'',
|
|
146
|
+
'this directory contains lexicon documents pulled from the following sources:',
|
|
147
|
+
'',
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
for (const { source, rev } of revisions) {
|
|
151
|
+
switch (source.type) {
|
|
152
|
+
case 'git': {
|
|
153
|
+
lines.push(`- ${source.remote}${source.ref ? ` (ref: ${source.ref})` : ``}`);
|
|
154
|
+
if (rev) {
|
|
155
|
+
lines.push(` - commit: ${rev}`);
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case 'atproto': {
|
|
160
|
+
if (source.mode === 'nsids') {
|
|
161
|
+
lines.push(`- atproto (nsids: ${source.nsids.join(', ')})`);
|
|
162
|
+
} else {
|
|
163
|
+
lines.push(
|
|
164
|
+
`- atproto (authority: ${source.authority}${source.pattern ? `, pattern: ${source.pattern.join(', ')}` : ''})`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
lines.push('');
|
|
173
|
+
|
|
174
|
+
const content = lines.join('\n');
|
|
175
|
+
const formatted = await prettier.format(content, {
|
|
176
|
+
...(prettierConfig ?? {}),
|
|
177
|
+
parser: 'markdown',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await fs.writeFile(path.join(outdir, 'README.md'), formatted);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* runs the pull command to fetch lexicon documents from configured sources
|
|
185
|
+
* @param args parsed command arguments
|
|
186
|
+
*/
|
|
187
|
+
export const runPull = async (args: PullCommand): Promise<void> => {
|
|
188
|
+
const config = await loadConfig(args.config);
|
|
189
|
+
const pullConfig = ensurePullConfig(config);
|
|
190
|
+
|
|
191
|
+
const outdir = path.resolve(config.root, pullConfig.outdir);
|
|
192
|
+
const prettierConfig = await prettier.resolveConfig(config.root, { editorconfig: true });
|
|
193
|
+
|
|
194
|
+
const seen = new Map<string, SourceLocation>();
|
|
195
|
+
const collected: PulledLexicon[] = [];
|
|
196
|
+
const sourceRevisions: SourceRevision[] = [];
|
|
197
|
+
|
|
198
|
+
for (const source of pullConfig.sources) {
|
|
199
|
+
const result = await pullSource(source);
|
|
200
|
+
|
|
201
|
+
sourceRevisions.push({ source, rev: result.rev });
|
|
202
|
+
|
|
203
|
+
for (const [nsid, entry] of result.pulled) {
|
|
204
|
+
const existing = seen.get(nsid);
|
|
205
|
+
|
|
206
|
+
if (existing) {
|
|
207
|
+
console.error(pc.bold(pc.red(`duplicate lexicon "${nsid}"`)));
|
|
208
|
+
console.error(`- found ${entry.location.relativePath} from ${entry.location.sourceDescription}`);
|
|
209
|
+
console.error(` at ${entry.location.absolutePath}`);
|
|
210
|
+
console.error(`- already found ${existing.relativePath} from ${existing.sourceDescription}`);
|
|
211
|
+
console.error(` at ${existing.absolutePath}`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
seen.set(nsid, entry.location);
|
|
216
|
+
collected.push(entry);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (pullConfig.clean) {
|
|
221
|
+
await fs.rm(outdir, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await fs.mkdir(outdir, { recursive: true });
|
|
225
|
+
|
|
226
|
+
for (const entry of collected) {
|
|
227
|
+
await writeLexicon(outdir, entry.nsid, entry.doc, prettierConfig);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await writeSourceReadme(outdir, sourceRevisions, prettierConfig);
|
|
231
|
+
};
|
package/src/config.ts
CHANGED
|
@@ -1,13 +1,76 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
1
2
|
import * as path from 'node:path';
|
|
2
3
|
import * as url from 'node:url';
|
|
3
4
|
|
|
4
5
|
import * as v from '@badrap/valita';
|
|
5
6
|
import pc from 'picocolors';
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
+
import { isAtprotoDid } from '@atcute/identity';
|
|
9
|
+
import { isHandle, isNsid } from '@atcute/lexicons/syntax';
|
|
8
10
|
|
|
9
11
|
import type { ImportMapping } from './codegen.js';
|
|
10
12
|
|
|
13
|
+
const gitSourceConfigSchema = v.object({
|
|
14
|
+
type: v.literal('git'),
|
|
15
|
+
remote: v.string().assert((value) => value.length > 0, `must not be empty`),
|
|
16
|
+
ref: v
|
|
17
|
+
.string()
|
|
18
|
+
.assert((value) => value.length > 0, `must not be empty`)
|
|
19
|
+
.optional(),
|
|
20
|
+
pattern: v
|
|
21
|
+
.array(v.string().assert((value) => value.length > 0, `must not be empty`))
|
|
22
|
+
.assert((value) => value.length > 0, `must include at least one glob pattern`),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const atprotoNsidsSourceConfigSchema = v.object({
|
|
26
|
+
type: v.literal('atproto'),
|
|
27
|
+
mode: v.literal('nsids'),
|
|
28
|
+
nsids: v
|
|
29
|
+
.array(v.string().assert((value) => isNsid(value), `must be valid nsid`))
|
|
30
|
+
.assert((value) => value.length > 0, `must include at least one nsid`),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const atprotoAuthoritySourceConfigSchema = v.object({
|
|
34
|
+
type: v.literal('atproto'),
|
|
35
|
+
mode: v.literal('authority'),
|
|
36
|
+
authority: v
|
|
37
|
+
.string()
|
|
38
|
+
.assert((value) => isHandle(value) || isAtprotoDid(value), `must a valid at-identifier`),
|
|
39
|
+
pattern: v
|
|
40
|
+
.array(
|
|
41
|
+
v
|
|
42
|
+
.string()
|
|
43
|
+
.assert((value) => isValidLexiconPattern(value), `must be valid nsid or pattern ending with .*`),
|
|
44
|
+
)
|
|
45
|
+
.optional(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const atprotoSourceConfigSchema = v.union(atprotoNsidsSourceConfigSchema, atprotoAuthoritySourceConfigSchema);
|
|
49
|
+
|
|
50
|
+
const sourceConfigSchema = v.union(gitSourceConfigSchema, atprotoSourceConfigSchema);
|
|
51
|
+
|
|
52
|
+
const pullConfigSchema = v.object({
|
|
53
|
+
outdir: v.string().assert((value) => value.length > 0, `must not be empty`),
|
|
54
|
+
clean: v.boolean().optional(),
|
|
55
|
+
sources: v
|
|
56
|
+
.array(sourceConfigSchema)
|
|
57
|
+
.assert((value) => value.length > 0, `must include at least one source`),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const exportConfigSchema = v.object({
|
|
61
|
+
outdir: v.string().assert((value) => value.length > 0, `must not be empty`),
|
|
62
|
+
files: v.array(v.string().assert((value) => value.length > 0, `must not be empty`)).optional(),
|
|
63
|
+
clean: v.boolean().optional(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export type GitSourceConfig = v.Infer<typeof gitSourceConfigSchema>;
|
|
67
|
+
export type AtprotoNsidsSourceConfig = v.Infer<typeof atprotoNsidsSourceConfigSchema>;
|
|
68
|
+
export type AtprotoAuthoritySourceConfig = v.Infer<typeof atprotoAuthoritySourceConfigSchema>;
|
|
69
|
+
export type AtprotoSourceConfig = v.Infer<typeof atprotoSourceConfigSchema>;
|
|
70
|
+
export type SourceConfig = v.Infer<typeof sourceConfigSchema>;
|
|
71
|
+
export type PullConfig = v.Infer<typeof pullConfigSchema>;
|
|
72
|
+
export type ExportConfig = v.Infer<typeof exportConfigSchema>;
|
|
73
|
+
|
|
11
74
|
const isValidLexiconPattern = (pattern: string): boolean => {
|
|
12
75
|
if (pattern.endsWith('.*')) {
|
|
13
76
|
return isNsid(`${pattern.slice(0, -2)}.x`);
|
|
@@ -19,7 +82,7 @@ const isValidLexiconPattern = (pattern: string): boolean => {
|
|
|
19
82
|
const mappingImports: v.Type<ImportMapping['imports']> = v.unknown().chain((value) => {
|
|
20
83
|
if (typeof value === 'string') {
|
|
21
84
|
if (value.length === 0) {
|
|
22
|
-
return v.err(
|
|
85
|
+
return v.err('imports must not be empty');
|
|
23
86
|
}
|
|
24
87
|
|
|
25
88
|
return v.ok(value);
|
|
@@ -29,7 +92,7 @@ const mappingImports: v.Type<ImportMapping['imports']> = v.unknown().chain((valu
|
|
|
29
92
|
return v.ok(value as ImportMapping['imports']);
|
|
30
93
|
}
|
|
31
94
|
|
|
32
|
-
return v.err(
|
|
95
|
+
return v.err('imports must be a string or function');
|
|
33
96
|
});
|
|
34
97
|
|
|
35
98
|
const importMappingSchema: v.Type<ImportMapping> = v.object({
|
|
@@ -37,50 +100,34 @@ const importMappingSchema: v.Type<ImportMapping> = v.object({
|
|
|
37
100
|
.array(
|
|
38
101
|
v.string().chain((value) => {
|
|
39
102
|
if (!isValidLexiconPattern(value)) {
|
|
40
|
-
return v.err(
|
|
41
|
-
message: 'invalid NSID pattern (must be valid NSID or end with .*)',
|
|
42
|
-
});
|
|
103
|
+
return v.err(`invalid NSID pattern (must be valid NSID or end with .*)`);
|
|
43
104
|
}
|
|
44
105
|
|
|
45
106
|
return v.ok(value);
|
|
46
107
|
}),
|
|
47
108
|
)
|
|
48
|
-
.assert((patterns) => patterns.length > 0,
|
|
49
|
-
message: 'nsid requires at least one pattern',
|
|
50
|
-
}),
|
|
109
|
+
.assert((patterns) => patterns.length > 0, `nsid requires at least one pattern`),
|
|
51
110
|
imports: mappingImports,
|
|
52
111
|
});
|
|
53
112
|
|
|
54
113
|
export const lexiconConfigSchema = v.object({
|
|
55
|
-
outdir: v.string().assert((value) => value.length > 0,
|
|
56
|
-
message: 'outdir must not be empty',
|
|
57
|
-
}),
|
|
114
|
+
outdir: v.string().assert((value) => value.length > 0, `must not be empty`),
|
|
58
115
|
files: v
|
|
59
|
-
.array(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}),
|
|
63
|
-
)
|
|
64
|
-
.assert((value) => value.length > 0, {
|
|
65
|
-
message: 'files must include at least one pattern',
|
|
66
|
-
}),
|
|
67
|
-
imports: v
|
|
68
|
-
.array(
|
|
69
|
-
v.string().assert((value) => value.length > 0, {
|
|
70
|
-
message: 'imports entries must not be empty',
|
|
71
|
-
}),
|
|
72
|
-
)
|
|
73
|
-
.optional(),
|
|
116
|
+
.array(v.string().assert((value) => value.length > 0, `must not be empty`))
|
|
117
|
+
.assert((value) => value.length > 0, `must include at least one glob pattern`),
|
|
118
|
+
imports: v.array(v.string().assert((value) => value.length > 0, `must not be empty`)).optional(),
|
|
74
119
|
mappings: v.array(importMappingSchema).optional(),
|
|
75
120
|
modules: v
|
|
76
121
|
.object({
|
|
77
122
|
importSuffix: v
|
|
78
123
|
.string()
|
|
79
|
-
.assert((value) => value.length > 0,
|
|
124
|
+
.assert((value) => value.length > 0, `must not be empty`)
|
|
80
125
|
.optional(),
|
|
81
126
|
})
|
|
82
127
|
.partial()
|
|
83
128
|
.optional(),
|
|
129
|
+
pull: pullConfigSchema.optional(),
|
|
130
|
+
export: exportConfigSchema.optional(),
|
|
84
131
|
});
|
|
85
132
|
|
|
86
133
|
export type LexiconConfig = v.Infer<typeof lexiconConfigSchema>;
|
|
@@ -89,8 +136,33 @@ export interface NormalizedConfig extends LexiconConfig {
|
|
|
89
136
|
root: string;
|
|
90
137
|
}
|
|
91
138
|
|
|
92
|
-
export const loadConfig = async (configPath
|
|
93
|
-
|
|
139
|
+
export const loadConfig = async (configPath?: string): Promise<NormalizedConfig> => {
|
|
140
|
+
let configFilename: string | undefined;
|
|
141
|
+
|
|
142
|
+
if (configPath) {
|
|
143
|
+
configFilename = path.resolve(configPath);
|
|
144
|
+
} else {
|
|
145
|
+
// try to find lex.config.js or lex.config.ts in the current directory
|
|
146
|
+
const candidates = ['lex.config.js', 'lex.config.ts'];
|
|
147
|
+
|
|
148
|
+
for (const candidate of candidates) {
|
|
149
|
+
const candidatePath = path.resolve(candidate);
|
|
150
|
+
try {
|
|
151
|
+
await fs.access(candidatePath);
|
|
152
|
+
configFilename = candidatePath;
|
|
153
|
+
break;
|
|
154
|
+
} catch {
|
|
155
|
+
// file doesn't exist, try next candidate
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!configFilename) {
|
|
160
|
+
console.error(pc.bold(pc.red(`config file not found`)));
|
|
161
|
+
console.error(`looked for: ${candidates.join(', ')}`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
94
166
|
const configDirname = path.dirname(configFilename);
|
|
95
167
|
|
|
96
168
|
let rawConfig: unknown;
|
package/src/git.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export interface GitCommandOptions {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
stdin?: string;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface GitResult {
|
|
11
|
+
stdout: string;
|
|
12
|
+
stderr: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class GitError extends Error {
|
|
16
|
+
readonly args: readonly string[];
|
|
17
|
+
readonly code: number | null;
|
|
18
|
+
readonly signal: NodeJS.Signals | null;
|
|
19
|
+
readonly stdout: string;
|
|
20
|
+
readonly stderr: string;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
args: readonly string[],
|
|
24
|
+
stdout: string,
|
|
25
|
+
stderr: string,
|
|
26
|
+
code: number | null,
|
|
27
|
+
signal: NodeJS.Signals | null,
|
|
28
|
+
) {
|
|
29
|
+
const reason = code !== null ? `code ${code}` : `signal ${signal ?? 'unknown'}`;
|
|
30
|
+
super(`git ${args.join(' ')} failed with ${reason}`);
|
|
31
|
+
|
|
32
|
+
this.name = 'GitError';
|
|
33
|
+
this.args = args;
|
|
34
|
+
this.code = code;
|
|
35
|
+
this.signal = signal;
|
|
36
|
+
this.stdout = stdout;
|
|
37
|
+
this.stderr = stderr;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* runs git with the provided arguments and throws when the command fails.
|
|
43
|
+
* @param args positional arguments for git
|
|
44
|
+
* @param options execution options
|
|
45
|
+
* @returns stdout and stderr from git
|
|
46
|
+
* @throws GitError when git exits with a non-zero status or is terminated
|
|
47
|
+
*/
|
|
48
|
+
export const runGit = (args: readonly string[], options: GitCommandOptions = {}): Promise<GitResult> => {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const child = spawn('git', args, {
|
|
51
|
+
cwd: options.cwd,
|
|
52
|
+
env: options.env,
|
|
53
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let stdout = '';
|
|
57
|
+
let stderr = '';
|
|
58
|
+
|
|
59
|
+
child.stdout?.setEncoding('utf8');
|
|
60
|
+
child.stderr?.setEncoding('utf8');
|
|
61
|
+
|
|
62
|
+
child.stdout?.on('data', (chunk) => {
|
|
63
|
+
stdout += chunk;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
child.stderr?.on('data', (chunk) => {
|
|
67
|
+
stderr += chunk;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let timer: NodeJS.Timeout | undefined;
|
|
71
|
+
if (options.timeoutMs !== undefined) {
|
|
72
|
+
timer = setTimeout(() => {
|
|
73
|
+
child.kill('SIGKILL');
|
|
74
|
+
}, options.timeoutMs);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
child.on('error', (err) => {
|
|
78
|
+
if (timer) {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
reject(err);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
child.on('close', (code, signal) => {
|
|
86
|
+
if (timer) {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (code === 0) {
|
|
91
|
+
resolve({ stdout, stderr });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
reject(new GitError(args, stdout, stderr, code, signal));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (options.stdin !== undefined) {
|
|
99
|
+
child.stdin?.end(options.stdin);
|
|
100
|
+
} else {
|
|
101
|
+
child.stdin?.end();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
};
|