@atcute/lex-cli 2.3.2 → 2.4.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 +34 -0
- package/dist/cli.js +26 -21
- package/dist/cli.js.map +1 -1
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +7 -14
- package/dist/codegen.js.map +1 -1
- package/dist/config.d.ts +53 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +97 -0
- package/dist/config.js.map +1 -0
- 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/index.d.ts +2 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/pull.d.ts +7 -0
- package/dist/pull.d.ts.map +1 -0
- package/dist/pull.js +209 -0
- package/dist/pull.js.map +1 -0
- package/package.json +3 -3
- package/src/cli.ts +39 -28
- package/src/codegen.ts +10 -26
- package/src/config.ts +130 -0
- package/src/git.ts +104 -0
- package/src/index.ts +3 -11
- package/src/pull.ts +298 -0
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
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { lexiconConfigSchema, type LexiconConfig } from './config.js';
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
outdir: string;
|
|
5
|
-
files: string[];
|
|
6
|
-
imports?: string[];
|
|
7
|
-
mappings?: ImportMapping[];
|
|
8
|
-
modules?: {
|
|
9
|
-
importSuffix?: string;
|
|
10
|
-
};
|
|
11
|
-
}
|
|
3
|
+
export type { LexiconConfig };
|
|
12
4
|
|
|
13
5
|
export const defineLexiconConfig = (config: LexiconConfig): LexiconConfig => {
|
|
14
|
-
return config;
|
|
6
|
+
return lexiconConfigSchema.parse(config);
|
|
15
7
|
};
|
package/src/pull.ts
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { lexiconDoc, refineLexiconDoc, type LexiconDoc } from '@atcute/lexicon-doc';
|
|
6
|
+
import prettier from 'prettier';
|
|
7
|
+
import pc from 'picocolors';
|
|
8
|
+
|
|
9
|
+
import { runGit, GitError } from './git.js';
|
|
10
|
+
import type { NormalizedConfig, PullConfig, SourceConfig } from './config.js';
|
|
11
|
+
|
|
12
|
+
interface SourceRevision {
|
|
13
|
+
source: SourceConfig;
|
|
14
|
+
rev: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SourceLocation {
|
|
18
|
+
absolutePath: string;
|
|
19
|
+
relativePath: string;
|
|
20
|
+
sourceDescription: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PulledLexicon {
|
|
24
|
+
nsid: string;
|
|
25
|
+
doc: LexiconDoc;
|
|
26
|
+
location: SourceLocation;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface PullResult {
|
|
30
|
+
pulled: Map<string, PulledLexicon>;
|
|
31
|
+
rev: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ensurePullConfig = (config: NormalizedConfig): PullConfig => {
|
|
35
|
+
if (!config.pull) {
|
|
36
|
+
console.error(pc.bold(pc.red(`pull configuration missing`)));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return config.pull;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const parseLexiconFile = async (loc: SourceLocation): Promise<LexiconDoc> => {
|
|
44
|
+
let source: string;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
source = await fs.readFile(loc.absolutePath, 'utf8');
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(
|
|
50
|
+
pc.bold(pc.red(`file read error for ${loc.relativePath} when pulling ${loc.sourceDescription}`)),
|
|
51
|
+
);
|
|
52
|
+
console.error(`found in ${loc.absolutePath}`);
|
|
53
|
+
console.error(err);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let json: unknown;
|
|
58
|
+
try {
|
|
59
|
+
json = JSON.parse(source);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(
|
|
62
|
+
pc.bold(pc.red(`json parse error in ${loc.relativePath} when pulling ${loc.sourceDescription}`)),
|
|
63
|
+
);
|
|
64
|
+
console.error(`found in ${loc.absolutePath}`);
|
|
65
|
+
console.error(err);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = lexiconDoc.try(json, { mode: 'passthrough' });
|
|
70
|
+
if (!result.ok) {
|
|
71
|
+
console.error(
|
|
72
|
+
pc.bold(
|
|
73
|
+
pc.red(`schema validation failed for ${loc.relativePath} when pulling ${loc.sourceDescription}`),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
console.error(`found in ${loc.absolutePath}`);
|
|
77
|
+
console.error(result.message);
|
|
78
|
+
|
|
79
|
+
for (const issue of result.issues) {
|
|
80
|
+
console.log(`- ${issue.code} at .${issue.path.join('.')}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const issues = refineLexiconDoc(result.value, true);
|
|
87
|
+
if (issues.length > 0) {
|
|
88
|
+
console.error(
|
|
89
|
+
pc.bold(pc.red(`lint validation failed for ${loc.relativePath} when pulling ${loc.sourceDescription}`)),
|
|
90
|
+
);
|
|
91
|
+
console.error(`found in ${loc.absolutePath}`);
|
|
92
|
+
|
|
93
|
+
for (const issue of issues) {
|
|
94
|
+
console.log(`- ${issue.message} at .${issue.path.join('.')}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result.value;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const writeLexicon = async (
|
|
104
|
+
outdir: string,
|
|
105
|
+
nsid: string,
|
|
106
|
+
doc: LexiconDoc,
|
|
107
|
+
prettierConfig: prettier.Options | null,
|
|
108
|
+
): Promise<void> => {
|
|
109
|
+
const nsidPath = nsid.replaceAll('.', '/');
|
|
110
|
+
const target = path.join(outdir, `${nsidPath}.json`);
|
|
111
|
+
const dirname = path.dirname(target);
|
|
112
|
+
|
|
113
|
+
const code = await prettier.format(JSON.stringify(doc, null, 2), {
|
|
114
|
+
...(prettierConfig ?? {}),
|
|
115
|
+
parser: 'json',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await fs.mkdir(dirname, { recursive: true });
|
|
119
|
+
await fs.writeFile(target, code);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* pulls lexicon documents from a git repository source
|
|
124
|
+
* @param source git source configuration
|
|
125
|
+
* @returns pulled lexicons and commit hash
|
|
126
|
+
*/
|
|
127
|
+
const pullGitSource = async (source: SourceConfig & { type: 'git' }): Promise<PullResult> => {
|
|
128
|
+
const tempParent = await fs.mkdtemp(path.join(os.tmpdir(), 'lex-cli-pull-'));
|
|
129
|
+
|
|
130
|
+
const cloneDir = path.join(tempParent, 'repo');
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await runGit(
|
|
134
|
+
[
|
|
135
|
+
'clone',
|
|
136
|
+
'--filter=blob:none',
|
|
137
|
+
'--depth',
|
|
138
|
+
'1',
|
|
139
|
+
'--sparse',
|
|
140
|
+
...(source.ref ? ['--branch', source.ref, '--single-branch'] : []),
|
|
141
|
+
source.remote,
|
|
142
|
+
cloneDir,
|
|
143
|
+
],
|
|
144
|
+
{ timeoutMs: 60_000 },
|
|
145
|
+
);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (err instanceof GitError) {
|
|
148
|
+
console.error(pc.bold(pc.red(`git clone failed for ${source.remote}:`)));
|
|
149
|
+
console.error(err.stderr || err.message);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await runGit(['-C', cloneDir, 'sparse-checkout', 'set', '--no-cone', ...source.pattern], {
|
|
158
|
+
timeoutMs: 30_000,
|
|
159
|
+
});
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (err instanceof GitError) {
|
|
162
|
+
console.error(pc.bold(pc.red(`git sparse-checkout failed for ${source.remote}:`)));
|
|
163
|
+
console.error(err.stderr || err.message);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const pulled = new Map<string, PulledLexicon>();
|
|
171
|
+
|
|
172
|
+
for await (const filename of fs.glob(source.pattern, { cwd: cloneDir })) {
|
|
173
|
+
const absolute = path.join(cloneDir, filename);
|
|
174
|
+
const stat = await fs.stat(absolute);
|
|
175
|
+
|
|
176
|
+
if (!stat.isFile()) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const location: SourceLocation = {
|
|
181
|
+
absolutePath: absolute,
|
|
182
|
+
relativePath: filename,
|
|
183
|
+
sourceDescription: source.remote,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const doc = await parseLexiconFile(location);
|
|
187
|
+
|
|
188
|
+
pulled.set(doc.id, { nsid: doc.id, doc, location });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// get the commit hash
|
|
192
|
+
let rev: string;
|
|
193
|
+
try {
|
|
194
|
+
const result = await runGit(['-C', cloneDir, 'rev-parse', 'HEAD'], { timeoutMs: 10_000 });
|
|
195
|
+
rev = result.stdout.trim();
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (err instanceof GitError) {
|
|
198
|
+
console.error(pc.bold(pc.red(`git rev-parse failed for ${source.remote}:`)));
|
|
199
|
+
console.error(err.stderr || err.message);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await fs.rm(tempParent, { recursive: true, force: true });
|
|
207
|
+
|
|
208
|
+
return { pulled, rev };
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const pullSource = async (source: SourceConfig): Promise<PullResult> => {
|
|
212
|
+
switch (source.type) {
|
|
213
|
+
case 'git': {
|
|
214
|
+
return pullGitSource(source);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const writeSourceReadme = async (
|
|
220
|
+
outdir: string,
|
|
221
|
+
revisions: SourceRevision[],
|
|
222
|
+
prettierConfig: prettier.Options | null,
|
|
223
|
+
): Promise<void> => {
|
|
224
|
+
const lines = [
|
|
225
|
+
'# lexicon sources',
|
|
226
|
+
'',
|
|
227
|
+
'this directory contains lexicon documents pulled from the following sources:',
|
|
228
|
+
'',
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
for (const { source, rev } of revisions) {
|
|
232
|
+
switch (source.type) {
|
|
233
|
+
case 'git': {
|
|
234
|
+
lines.push(`- ${source.remote}${source.ref ? ` (ref: ${source.ref})` : ``}`);
|
|
235
|
+
lines.push(` - commit: ${rev}`);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
lines.push('');
|
|
242
|
+
|
|
243
|
+
const content = lines.join('\n');
|
|
244
|
+
const formatted = await prettier.format(content, {
|
|
245
|
+
...(prettierConfig ?? {}),
|
|
246
|
+
parser: 'markdown',
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await fs.writeFile(path.join(outdir, 'README.md'), formatted);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* pulls lexicon documents from configured sources and writes them to disk using nsid-based paths.
|
|
254
|
+
* @param config normalized lex-cli configuration
|
|
255
|
+
*/
|
|
256
|
+
export const runPull = async (config: NormalizedConfig): Promise<void> => {
|
|
257
|
+
const pullConfig = ensurePullConfig(config);
|
|
258
|
+
const outdir = path.resolve(config.root, pullConfig.outdir);
|
|
259
|
+
const prettierConfig = await prettier.resolveConfig(config.root, { editorconfig: true });
|
|
260
|
+
|
|
261
|
+
const seen = new Map<string, SourceLocation>();
|
|
262
|
+
const collected: PulledLexicon[] = [];
|
|
263
|
+
const sourceRevisions: SourceRevision[] = [];
|
|
264
|
+
|
|
265
|
+
for (const source of pullConfig.sources) {
|
|
266
|
+
const result = await pullSource(source);
|
|
267
|
+
|
|
268
|
+
sourceRevisions.push({ source, rev: result.rev });
|
|
269
|
+
|
|
270
|
+
for (const [nsid, entry] of result.pulled) {
|
|
271
|
+
const existing = seen.get(nsid);
|
|
272
|
+
|
|
273
|
+
if (existing) {
|
|
274
|
+
console.error(pc.bold(pc.red(`duplicate lexicon "${nsid}"`)));
|
|
275
|
+
console.error(`- found ${entry.location.relativePath} from ${entry.location.sourceDescription}`);
|
|
276
|
+
console.error(` at ${entry.location.absolutePath}`);
|
|
277
|
+
console.error(`- already found ${existing.relativePath} from ${existing.sourceDescription}`);
|
|
278
|
+
console.error(` at ${existing.absolutePath}`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
seen.set(nsid, entry.location);
|
|
283
|
+
collected.push(entry);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (pullConfig.clean) {
|
|
288
|
+
await fs.rm(outdir, { recursive: true, force: true });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await fs.mkdir(outdir, { recursive: true });
|
|
292
|
+
|
|
293
|
+
for (const entry of collected) {
|
|
294
|
+
await writeLexicon(outdir, entry.nsid, entry.doc, prettierConfig);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
await writeSourceReadme(outdir, sourceRevisions, prettierConfig);
|
|
298
|
+
};
|