@atcute/lex-cli 2.7.0 → 2.8.1

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.
@@ -3,34 +3,14 @@ import * as path from 'node:path';
3
3
 
4
4
  import { lexiconDoc, refineLexiconDoc, type LexiconDoc } from '@atcute/lexicon-doc';
5
5
 
6
- import { merge, object } from '@optique/core/constructs';
7
- import { message } from '@optique/core/message';
8
- import { type InferValue } from '@optique/core/parser';
9
- import { command, constant } from '@optique/core/primitives';
10
6
  import pc from 'picocolors';
11
7
 
8
+ import type { PullCommand } from '../cli.ts';
12
9
  import { loadConfig, type NormalizedConfig, type PullConfig, type SourceConfig } from '../config.ts';
13
10
  import { createFormatter, type Formatter } from '../formatter.ts';
14
11
  import { pullAtprotoSource } from '../pull-sources/atproto.ts';
15
12
  import { pullGitSource } from '../pull-sources/git.ts';
16
13
  import type { PullResult, PulledLexicon, SourceLocation } from '../pull-sources/types.ts';
17
- import { sharedOptions } from '../shared-options.ts';
18
-
19
- export const pullCommandSchema = command(
20
- 'pull',
21
- merge(
22
- object({
23
- type: constant('pull'),
24
- }),
25
- sharedOptions,
26
- ),
27
- {
28
- brief: message`pull lexicon documents from configured sources`,
29
- description: message`fetches lexicon documents from configured git repositories and writes them to the output directory.`,
30
- },
31
- );
32
-
33
- export type PullCommand = InferValue<typeof pullCommandSchema>;
34
14
 
35
15
  interface SourceRevision {
36
16
  source: SourceConfig;
@@ -180,7 +160,7 @@ const writeSourceReadme = async (
180
160
  * runs the pull command to fetch lexicon documents from configured sources
181
161
  * @param args parsed command arguments
182
162
  */
183
- export const runPull = async (args: PullCommand): Promise<void> => {
163
+ export const handler = async (args: PullCommand): Promise<void> => {
184
164
  const config = await loadConfig(args.config);
185
165
  const pullConfig = ensurePullConfig(config);
186
166
 
package/src/config.ts CHANGED
@@ -79,15 +79,6 @@ const formatterConfigSchema = v.union(
79
79
  }),
80
80
  );
81
81
 
82
- export type GitSourceConfig = v.Infer<typeof gitSourceConfigSchema>;
83
- export type AtprotoNsidsSourceConfig = v.Infer<typeof atprotoNsidsSourceConfigSchema>;
84
- export type AtprotoAuthoritySourceConfig = v.Infer<typeof atprotoAuthoritySourceConfigSchema>;
85
- export type AtprotoSourceConfig = v.Infer<typeof atprotoSourceConfigSchema>;
86
- export type SourceConfig = v.Infer<typeof sourceConfigSchema>;
87
- export type PullConfig = v.Infer<typeof pullConfigSchema>;
88
- export type ExportConfig = v.Infer<typeof exportConfigSchema>;
89
- export type FormatterConfig = v.Infer<typeof formatterConfigSchema>;
90
-
91
82
  const isValidLexiconPattern = (pattern: string): boolean => {
92
83
  if (pattern.endsWith('.*')) {
93
84
  return isNsid(`${pattern.slice(0, -2)}.x`);
@@ -127,32 +118,71 @@ const importMappingSchema: v.Type<ImportMapping> = v.object({
127
118
  imports: mappingImports,
128
119
  });
129
120
 
130
- export const lexiconConfigSchema = v.object({
131
- outdir: v.string().assert((value) => value.length > 0, `must not be empty`),
121
+ const modulesConfigSchema = v
122
+ .object({
123
+ importSuffix: v
124
+ .string()
125
+ .assert((value) => value.length > 0, `must not be empty`)
126
+ .optional(),
127
+ })
128
+ .partial();
129
+
130
+ const generateConfigSchema = v.object({
131
+ outdir: v
132
+ .string()
133
+ .assert((value) => value.length > 0, `must not be empty`)
134
+ .optional(),
132
135
  files: v
133
136
  .array(v.string().assert((value) => value.length > 0, `must not be empty`))
134
- .assert((value) => value.length > 0, `must include at least one glob pattern`),
137
+ .assert((value) => value.length > 0, `must include at least one glob pattern`)
138
+ .optional(),
135
139
  imports: v.array(v.string().assert((value) => value.length > 0, `must not be empty`)).optional(),
136
140
  mappings: v.array(importMappingSchema).optional(),
137
- modules: v
138
- .object({
139
- importSuffix: v
140
- .string()
141
- .assert((value) => value.length > 0, `must not be empty`)
142
- .optional(),
143
- })
144
- .partial()
141
+ modules: modulesConfigSchema.optional(),
142
+ clean: v.boolean().optional(),
143
+ });
144
+
145
+ export type GitSourceConfig = v.Infer<typeof gitSourceConfigSchema>;
146
+ export type AtprotoNsidsSourceConfig = v.Infer<typeof atprotoNsidsSourceConfigSchema>;
147
+ export type AtprotoAuthoritySourceConfig = v.Infer<typeof atprotoAuthoritySourceConfigSchema>;
148
+ export type AtprotoSourceConfig = v.Infer<typeof atprotoSourceConfigSchema>;
149
+ export type SourceConfig = v.Infer<typeof sourceConfigSchema>;
150
+ export type PullConfig = v.Infer<typeof pullConfigSchema>;
151
+ export type ExportConfig = v.Infer<typeof exportConfigSchema>;
152
+ export type FormatterConfig = v.Infer<typeof formatterConfigSchema>;
153
+ export type GenerateConfig = v.Infer<typeof generateConfigSchema>;
154
+
155
+ export const lexiconConfigSchema = v.object({
156
+ /** @deprecated moved to `generate.outdir` */
157
+ outdir: v
158
+ .string()
159
+ .assert((value) => value.length > 0, `must not be empty`)
160
+ .optional(),
161
+ /** @deprecated moved to `generate.files` */
162
+ files: v
163
+ .array(v.string().assert((value) => value.length > 0, `must not be empty`))
164
+ .assert((value) => value.length > 0, `must include at least one glob pattern`)
145
165
  .optional(),
166
+ /** @deprecated moved to `generate.imports` */
167
+ imports: v.array(v.string().assert((value) => value.length > 0, `must not be empty`)).optional(),
168
+ /** @deprecated moved to `generate.mappings` */
169
+ mappings: v.array(importMappingSchema).optional(),
170
+ /** @deprecated moved to `generate.modules` */
171
+ modules: modulesConfigSchema.optional(),
146
172
  formatter: formatterConfigSchema.optional((): FormatterConfig => ({ type: 'prettier' })),
173
+ generate: generateConfigSchema.optional(),
147
174
  pull: pullConfigSchema.optional(),
148
175
  export: exportConfigSchema.optional(),
149
176
  });
150
177
 
151
178
  export type LexiconConfig = v.Infer<typeof lexiconConfigSchema>;
152
179
 
153
- export interface NormalizedConfig extends LexiconConfig {
180
+ export type NormalizedConfig = Omit<
181
+ LexiconConfig,
182
+ 'outdir' | 'files' | 'imports' | 'mappings' | 'modules'
183
+ > & {
154
184
  root: string;
155
- }
185
+ };
156
186
 
157
187
  export const loadConfig = async (configPath?: string): Promise<NormalizedConfig> => {
158
188
  let configFilename: string | undefined;
@@ -207,5 +237,29 @@ export const loadConfig = async (configPath?: string): Promise<NormalizedConfig>
207
237
  process.exit(1);
208
238
  }
209
239
 
210
- return { ...configResult.value, root: configDirname };
240
+ const { outdir, files, imports, mappings, modules, generate, ...rest } = configResult.value;
241
+
242
+ // back-compat: top-level generate options were moved into `generate.*`. merge the legacy
243
+ // top-level values into `generate`, with nested `generate.*` winning on conflicts. the result
244
+ // is only present if at least one generate-related option was provided anywhere.
245
+ const hasLegacyTopLevel =
246
+ outdir !== undefined ||
247
+ files !== undefined ||
248
+ imports !== undefined ||
249
+ mappings !== undefined ||
250
+ modules !== undefined;
251
+
252
+ let normalizedGenerate: GenerateConfig | undefined;
253
+ if (generate || hasLegacyTopLevel) {
254
+ normalizedGenerate = {
255
+ outdir: generate?.outdir ?? outdir,
256
+ files: generate?.files ?? files,
257
+ imports: generate?.imports ?? imports,
258
+ mappings: generate?.mappings ?? mappings,
259
+ modules: generate?.modules ?? modules,
260
+ clean: generate?.clean,
261
+ };
262
+ }
263
+
264
+ return { ...rest, generate: normalizedGenerate, root: configDirname };
211
265
  };
@@ -26,80 +26,81 @@ export const pullGitSource = async (
26
26
  const cloneDir = path.join(tempParent, 'repo');
27
27
 
28
28
  try {
29
- await runGit(
30
- [
31
- 'clone',
32
- '--filter=blob:none',
33
- '--depth',
34
- '1',
35
- '--sparse',
36
- ...(source.ref ? ['--branch', source.ref, '--single-branch'] : []),
37
- source.remote,
38
- cloneDir,
39
- ],
40
- { timeoutMs: 60_000 },
41
- );
42
- } catch (err) {
43
- if (err instanceof GitError) {
44
- console.error(pc.bold(pc.red(`git clone failed for ${source.remote}:`)));
45
- console.error(err.stderr || err.message);
46
- process.exit(1);
29
+ try {
30
+ await runGit(
31
+ [
32
+ 'clone',
33
+ '--filter=blob:none',
34
+ '--depth',
35
+ '1',
36
+ '--sparse',
37
+ ...(source.ref ? ['--branch', source.ref, '--single-branch'] : []),
38
+ source.remote,
39
+ cloneDir,
40
+ ],
41
+ { timeoutMs: 60_000 },
42
+ );
43
+ } catch (err) {
44
+ if (err instanceof GitError) {
45
+ console.error(pc.bold(pc.red(`git clone failed for ${source.remote}:`)));
46
+ console.error(err.stderr || err.message);
47
+ process.exit(1);
48
+ }
49
+
50
+ throw err;
47
51
  }
48
52
 
49
- throw err;
50
- }
51
-
52
- try {
53
- await runGit(['-C', cloneDir, 'sparse-checkout', 'set', '--no-cone', ...source.pattern], {
54
- timeoutMs: 30_000,
55
- });
56
- } catch (err) {
57
- if (err instanceof GitError) {
58
- console.error(pc.bold(pc.red(`git sparse-checkout failed for ${source.remote}:`)));
59
- console.error(err.stderr || err.message);
60
- process.exit(1);
53
+ try {
54
+ await runGit(['-C', cloneDir, 'sparse-checkout', 'set', '--no-cone', ...source.pattern], {
55
+ timeoutMs: 30_000,
56
+ });
57
+ } catch (err) {
58
+ if (err instanceof GitError) {
59
+ console.error(pc.bold(pc.red(`git sparse-checkout failed for ${source.remote}:`)));
60
+ console.error(err.stderr || err.message);
61
+ process.exit(1);
62
+ }
63
+
64
+ throw err;
61
65
  }
62
66
 
63
- throw err;
64
- }
65
-
66
- const pulled = new Map<string, PulledLexicon>();
67
+ const pulled = new Map<string, PulledLexicon>();
67
68
 
68
- for await (const filename of fs.glob(source.pattern, { cwd: cloneDir })) {
69
- const absolute = path.join(cloneDir, filename);
70
- const stat = await fs.stat(absolute);
69
+ for await (const filename of fs.glob(source.pattern, { cwd: cloneDir })) {
70
+ const absolute = path.join(cloneDir, filename);
71
+ const stat = await fs.stat(absolute);
71
72
 
72
- if (!stat.isFile()) {
73
- continue;
74
- }
73
+ if (!stat.isFile()) {
74
+ continue;
75
+ }
75
76
 
76
- const location: SourceLocation = {
77
- absolutePath: absolute,
78
- relativePath: filename,
79
- sourceDescription: source.remote,
80
- };
77
+ const location: SourceLocation = {
78
+ absolutePath: absolute,
79
+ relativePath: filename,
80
+ sourceDescription: source.remote,
81
+ };
81
82
 
82
- const doc = await parseLexiconFile(location);
83
+ const doc = await parseLexiconFile(location);
83
84
 
84
- pulled.set(doc.id, { nsid: doc.id, doc, location });
85
- }
85
+ pulled.set(doc.id, { nsid: doc.id, doc, location });
86
+ }
86
87
 
87
- // get the commit hash
88
- let rev: string;
89
- try {
90
- const result = await runGit(['-C', cloneDir, 'rev-parse', 'HEAD'], { timeoutMs: 10_000 });
91
- rev = result.stdout.trim();
92
- } catch (err) {
93
- if (err instanceof GitError) {
94
- console.error(pc.bold(pc.red(`git rev-parse failed for ${source.remote}:`)));
95
- console.error(err.stderr || err.message);
96
- process.exit(1);
88
+ let rev: string;
89
+ try {
90
+ const result = await runGit(['-C', cloneDir, 'rev-parse', 'HEAD'], { timeoutMs: 10_000 });
91
+ rev = result.stdout.trim();
92
+ } catch (err) {
93
+ if (err instanceof GitError) {
94
+ console.error(pc.bold(pc.red(`git rev-parse failed for ${source.remote}:`)));
95
+ console.error(err.stderr || err.message);
96
+ process.exit(1);
97
+ }
98
+
99
+ throw err;
97
100
  }
98
101
 
99
- throw err;
102
+ return { pulled, rev };
103
+ } finally {
104
+ await fs.rm(tempParent, { recursive: true, force: true });
100
105
  }
101
-
102
- await fs.rm(tempParent, { recursive: true, force: true });
103
-
104
- return { pulled, rev };
105
106
  };
@@ -1,6 +0,0 @@
1
- export declare const sharedOptions: import("@optique/core/parser").Parser<"sync", {
2
- readonly config: string | undefined;
3
- }, {
4
- readonly config: [import("@optique/core/valueparser").ValueParserResult<string> | undefined] | undefined;
5
- }>;
6
- //# sourceMappingURL=shared-options.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"shared-options.d.ts","sourceRoot":"","sources":["../src/shared-options.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,aAAa;;;;EAMxB,CAAC"}
@@ -1,11 +0,0 @@
1
- import { object } from '@optique/core/constructs';
2
- import { message } from '@optique/core/message';
3
- import { optional } from '@optique/core/modifiers';
4
- import { option } from '@optique/core/primitives';
5
- import { path as pathParser } from '@optique/run/valueparser';
6
- export const sharedOptions = object(`Global options`, {
7
- config: optional(option('-c', '--config', pathParser({ metavar: 'CONFIG' }), {
8
- description: message `path to the lexicon configuration file. defaults to searching for lex.config.js or lex.config.ts in the current directory.`,
9
- })),
10
- });
11
- //# sourceMappingURL=shared-options.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"shared-options.js","sourceRoot":"","sources":["../src/shared-options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,IAAI,IAAI,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAE9D,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,gBAAgB,EAAE;IACrD,MAAM,EAAE,QAAQ,CACf,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE;QAC3D,WAAW,EAAE,OAAO,CAAA,4HAA4H;KAChJ,CAAC,CACF;CACD,CAAC,CAAC"}
@@ -1,13 +0,0 @@
1
- import { object } from '@optique/core/constructs';
2
- import { message } from '@optique/core/message';
3
- import { optional } from '@optique/core/modifiers';
4
- import { option } from '@optique/core/primitives';
5
- import { path as pathParser } from '@optique/run/valueparser';
6
-
7
- export const sharedOptions = object(`Global options`, {
8
- config: optional(
9
- option('-c', '--config', pathParser({ metavar: 'CONFIG' }), {
10
- description: message`path to the lexicon configuration file. defaults to searching for lex.config.js or lex.config.ts in the current directory.`,
11
- }),
12
- ),
13
- });