@atcute/lex-cli 2.8.2 → 3.0.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.
Files changed (42) hide show
  1. package/README.md +43 -29
  2. package/dist/commands/generate.d.ts.map +1 -1
  3. package/dist/commands/generate.js +6 -7
  4. package/dist/commands/generate.js.map +1 -1
  5. package/dist/commands/pull.d.ts.map +1 -1
  6. package/dist/commands/pull.js +26 -9
  7. package/dist/commands/pull.js.map +1 -1
  8. package/dist/config.d.ts +60 -157
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +68 -137
  11. package/dist/config.js.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/lexicon-loader.d.ts.map +1 -1
  16. package/dist/lexicon-loader.js +7 -8
  17. package/dist/lexicon-loader.js.map +1 -1
  18. package/dist/lexicon-metadata.d.ts +2 -2
  19. package/dist/lexicon-metadata.d.ts.map +1 -1
  20. package/dist/lexicon-metadata.js +13 -34
  21. package/dist/lexicon-metadata.js.map +1 -1
  22. package/dist/pull-sources/atproto.d.ts.map +1 -1
  23. package/dist/pull-sources/atproto.js +5 -3
  24. package/dist/pull-sources/atproto.js.map +1 -1
  25. package/dist/utils/issues.d.ts +4 -0
  26. package/dist/utils/issues.d.ts.map +1 -0
  27. package/dist/utils/issues.js +9 -0
  28. package/dist/utils/issues.js.map +1 -0
  29. package/dist/utils/nsid-pattern.d.ts +6 -0
  30. package/dist/utils/nsid-pattern.d.ts.map +1 -0
  31. package/dist/utils/nsid-pattern.js +12 -0
  32. package/dist/utils/nsid-pattern.js.map +1 -0
  33. package/package.json +7 -7
  34. package/src/commands/generate.ts +6 -9
  35. package/src/commands/pull.ts +28 -11
  36. package/src/config.ts +172 -189
  37. package/src/index.ts +3 -1
  38. package/src/lexicon-loader.ts +8 -10
  39. package/src/lexicon-metadata.ts +19 -36
  40. package/src/pull-sources/atproto.ts +4 -3
  41. package/src/utils/issues.ts +9 -0
  42. package/src/utils/nsid-pattern.ts +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atcute/lex-cli",
3
- "version": "2.8.2",
3
+ "version": "3.0.0",
4
4
  "description": "cli tool to generate type definitions for atcute",
5
5
  "license": "0BSD",
6
6
  "repository": {
@@ -23,16 +23,16 @@
23
23
  "access": "public"
24
24
  },
25
25
  "dependencies": {
26
- "@badrap/valita": "^0.4.6",
27
26
  "@optique/core": "^1.0.2",
28
27
  "@optique/run": "^1.0.2",
29
28
  "picocolors": "^1.1.1",
30
29
  "prettier": "^3.8.3",
31
- "@atcute/lexicon-doc": "^2.2.1",
32
- "@atcute/identity-resolver": "^1.2.3",
33
- "@atcute/identity": "^1.1.5",
34
- "@atcute/lexicons": "^1.3.1",
35
- "@atcute/lexicon-resolver": "^0.1.7"
30
+ "valibot": "^1.4.0",
31
+ "@atcute/identity": "^2.0.0",
32
+ "@atcute/lexicon-doc": "^3.0.0",
33
+ "@atcute/identity-resolver": "^2.0.0",
34
+ "@atcute/lexicon-resolver": "^1.0.0",
35
+ "@atcute/lexicons": "^2.0.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^25.6.0",
@@ -3,6 +3,7 @@ import * as module from 'node:module';
3
3
  import * as path from 'node:path';
4
4
 
5
5
  import pc from 'picocolors';
6
+ import * as v from 'valibot';
6
7
 
7
8
  import type { GenerateCommand } from '../cli.ts';
8
9
  import { generateLexiconApi, type ImportMapping } from '../codegen.ts';
@@ -10,6 +11,7 @@ import { loadConfig, type GenerateConfig, type NormalizedConfig } from '../confi
10
11
  import { createFormatter } from '../formatter.ts';
11
12
  import { loadLexicons } from '../lexicon-loader.ts';
12
13
  import { packageJsonSchema } from '../lexicon-metadata.ts';
14
+ import { printValibotIssues } from '../utils/issues.ts';
13
15
 
14
16
  /**
15
17
  * resolves package imports to ImportMapping[]
@@ -64,19 +66,14 @@ const resolveImportsToMappings = async (
64
66
  process.exit(1);
65
67
  }
66
68
 
67
- const result = packageJsonSchema.try(packageJson, { mode: 'passthrough' });
68
- if (!result.ok) {
69
+ const result = v.safeParse(packageJsonSchema, packageJson);
70
+ if (!result.success) {
69
71
  console.error(pc.bold(pc.red(`invalid atcute:lexicons in "${packageName}":`)));
70
- console.error(result.message);
71
-
72
- for (const issue of result.issues) {
73
- console.log(`- ${issue.code} at .${issue.path.join('.')}`);
74
- }
75
-
72
+ printValibotIssues(result.issues);
76
73
  process.exit(1);
77
74
  }
78
75
 
79
- const lexicons = result.value['atcute:lexicons'];
76
+ const lexicons = result.output['atcute:lexicons'];
80
77
  if (!lexicons?.mappings) {
81
78
  continue;
82
79
  }
@@ -4,6 +4,7 @@ import * as path from 'node:path';
4
4
  import { lexiconDoc, refineLexiconDoc, type LexiconDoc } from '@atcute/lexicon-doc';
5
5
 
6
6
  import pc from 'picocolors';
7
+ import * as v from 'valibot';
7
8
 
8
9
  import type { PullCommand } from '../cli.ts';
9
10
  import { loadConfig, type NormalizedConfig, type PullConfig, type SourceConfig } from '../config.ts';
@@ -11,6 +12,7 @@ import { createFormatter, type Formatter } from '../formatter.ts';
11
12
  import { pullAtprotoSource } from '../pull-sources/atproto.ts';
12
13
  import { pullGitSource } from '../pull-sources/git.ts';
13
14
  import type { PullResult, PulledLexicon, SourceLocation } from '../pull-sources/types.ts';
15
+ import { printValibotIssues } from '../utils/issues.ts';
14
16
 
15
17
  interface SourceRevision {
16
18
  source: SourceConfig;
@@ -52,24 +54,19 @@ const parseLexiconFile = async (loc: SourceLocation): Promise<LexiconDoc> => {
52
54
  process.exit(1);
53
55
  }
54
56
 
55
- const result = lexiconDoc.try(json, { mode: 'passthrough' });
56
- if (!result.ok) {
57
+ const result = v.safeParse(lexiconDoc, json);
58
+ if (!result.success) {
57
59
  console.error(
58
60
  pc.bold(
59
61
  pc.red(`schema validation failed for ${loc.relativePath} when pulling ${loc.sourceDescription}`),
60
62
  ),
61
63
  );
62
64
  console.error(`found in ${loc.absolutePath}`);
63
- console.error(result.message);
64
-
65
- for (const issue of result.issues) {
66
- console.log(`- ${issue.code} at .${issue.path.join('.')}`);
67
- }
68
-
65
+ printValibotIssues(result.issues);
69
66
  process.exit(1);
70
67
  }
71
68
 
72
- const issues = refineLexiconDoc(result.value, true);
69
+ const issues = refineLexiconDoc(result.output, true);
73
70
  if (issues.length > 0) {
74
71
  console.error(
75
72
  pc.bold(pc.red(`lint validation failed for ${loc.relativePath} when pulling ${loc.sourceDescription}`)),
@@ -83,7 +80,27 @@ const parseLexiconFile = async (loc: SourceLocation): Promise<LexiconDoc> => {
83
80
  process.exit(1);
84
81
  }
85
82
 
86
- return result.value;
83
+ return result.output;
84
+ };
85
+
86
+ // valibot rebuilds objects in schema key order; reorder back into dag-cbor
87
+ // canonical order (shorter keys first, then lexicographic) so writes are
88
+ // deterministic regardless of schema field declaration order
89
+ const canonicalize = (value: unknown): unknown => {
90
+ if (Array.isArray(value)) {
91
+ return value.map(canonicalize);
92
+ }
93
+ if (value !== null && typeof value === 'object') {
94
+ const obj = value as Record<string, unknown>;
95
+ const keys = Object.keys(obj).toSorted((a, b) => a.length - b.length || (a < b ? -1 : 1));
96
+ const result: Record<string, unknown> = {};
97
+ for (const key of keys) {
98
+ result[key] = canonicalize(obj[key]);
99
+ }
100
+
101
+ return result;
102
+ }
103
+ return value;
87
104
  };
88
105
 
89
106
  const writeLexicon = async (
@@ -96,7 +113,7 @@ const writeLexicon = async (
96
113
  const target = path.join(outdir, `${nsidPath}.json`);
97
114
  const dirname = path.dirname(target);
98
115
 
99
- const code = await formatter.format(JSON.stringify(doc, null, 2), target);
116
+ const code = await formatter.format(JSON.stringify(canonicalize(doc), null, 2), target);
100
117
 
101
118
  await fs.mkdir(dirname, { recursive: true });
102
119
  await fs.writeFile(target, code);
package/src/config.ts CHANGED
@@ -3,187 +3,209 @@ import * as path from 'node:path';
3
3
  import * as url from 'node:url';
4
4
 
5
5
  import { isAtprotoDid } from '@atcute/identity';
6
- import { isHandle, isNsid } from '@atcute/lexicons/syntax';
6
+ import { isHandle, isNsid, type Nsid } from '@atcute/lexicons/syntax';
7
7
 
8
- import * as v from '@badrap/valita';
9
8
  import pc from 'picocolors';
9
+ import * as v from 'valibot';
10
10
 
11
11
  import type { ImportMapping } from './codegen.ts';
12
+ import { printValibotIssues } from './utils/issues.ts';
13
+ import { isValidLexiconPattern } from './utils/nsid-pattern.ts';
14
+
15
+ // `lexiconConfigSchema` is wide and deep enough that valibot's inferred output bottoms out at
16
+ // `{}` for its nested fields. annotating it against an explicit interface forces tsgo to use the
17
+ // declared shape; inner schemas infer cleanly without help. the interfaces also strip the
18
+ // `{ [key: string]: unknown }` index signature that `looseObject` would otherwise expose.
19
+
20
+ export interface GitSourceConfig {
21
+ type: 'git';
22
+ remote: string;
23
+ ref?: string;
24
+ pattern: string[];
25
+ }
26
+
27
+ export interface AtprotoNsidsSourceConfig {
28
+ type: 'atproto';
29
+ mode: 'nsids';
30
+ nsids: Nsid[];
31
+ }
32
+
33
+ export interface AtprotoAuthoritySourceConfig {
34
+ type: 'atproto';
35
+ mode: 'authority';
36
+ authority: string;
37
+ pattern?: string[];
38
+ }
39
+
40
+ export type AtprotoSourceConfig = AtprotoNsidsSourceConfig | AtprotoAuthoritySourceConfig;
41
+
42
+ export type SourceConfig = GitSourceConfig | AtprotoSourceConfig;
43
+
44
+ export interface PullConfig {
45
+ outdir: string;
46
+ clean?: boolean;
47
+ sources: SourceConfig[];
48
+ }
49
+
50
+ export interface ExportConfig {
51
+ outdir: string;
52
+ files?: string[];
53
+ clean?: boolean;
54
+ }
55
+
56
+ export type FormatterConfig =
57
+ | { type: 'prettier' }
58
+ | { type: 'command'; command: string; concurrency: number }
59
+ | { type: 'lsp'; command: string };
60
+
61
+ export interface ModulesConfig {
62
+ importSuffix?: string;
63
+ }
64
+
65
+ export interface GenerateConfig {
66
+ outdir?: string;
67
+ files?: string[];
68
+ imports?: string[];
69
+ mappings?: ImportMapping[];
70
+ modules?: ModulesConfig;
71
+ clean?: boolean;
72
+ }
73
+
74
+ export interface LexiconConfig {
75
+ formatter?: FormatterConfig;
76
+ generate?: GenerateConfig;
77
+ pull?: PullConfig;
78
+ export?: ExportConfig;
79
+ }
80
+
81
+ export type NormalizedConfig = LexiconConfig & {
82
+ formatter: FormatterConfig;
83
+ root: string;
84
+ };
85
+
86
+ const nonEmptyString = v.pipe(v.string(), v.nonEmpty(`must not be empty`));
12
87
 
13
- const gitSourceConfigSchema = v.object({
88
+ const gitSourceConfigSchema = v.looseObject({
14
89
  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`),
90
+ remote: nonEmptyString,
91
+ ref: v.optional(nonEmptyString),
92
+ pattern: v.pipe(v.array(nonEmptyString), v.minLength(1, `must include at least one glob pattern`)),
23
93
  });
24
94
 
25
- const atprotoNsidsSourceConfigSchema = v.object({
95
+ const atprotoNsidsSourceConfigSchema = v.looseObject({
26
96
  type: v.literal('atproto'),
27
97
  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`),
98
+ nsids: v.pipe(
99
+ v.array(v.custom<Nsid>(isNsid, `must be valid nsid`)),
100
+ v.minLength(1, `must include at least one nsid`),
101
+ ),
31
102
  });
32
103
 
33
- const atprotoAuthoritySourceConfigSchema = v.object({
104
+ const atprotoAuthoritySourceConfigSchema = v.looseObject({
34
105
  type: v.literal('atproto'),
35
106
  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(),
107
+ authority: v.pipe(
108
+ v.string(),
109
+ v.check((value) => isHandle(value) || isAtprotoDid(value), `must be a valid at-identifier`),
110
+ ),
111
+ pattern: v.optional(
112
+ v.array(
113
+ v.pipe(v.string(), v.check(isValidLexiconPattern, `must be valid nsid or pattern ending with .*`)),
114
+ ),
115
+ ),
46
116
  });
47
117
 
48
- const atprotoSourceConfigSchema = v.union(atprotoNsidsSourceConfigSchema, atprotoAuthoritySourceConfigSchema);
118
+ const atprotoSourceConfigSchema = v.union([
119
+ atprotoNsidsSourceConfigSchema,
120
+ atprotoAuthoritySourceConfigSchema,
121
+ ]);
49
122
 
50
- const sourceConfigSchema = v.union(gitSourceConfigSchema, atprotoSourceConfigSchema);
123
+ const sourceConfigSchema = v.union([gitSourceConfigSchema, atprotoSourceConfigSchema]);
51
124
 
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`),
125
+ const pullConfigSchema = v.looseObject({
126
+ outdir: nonEmptyString,
127
+ clean: v.optional(v.boolean()),
128
+ sources: v.pipe(v.array(sourceConfigSchema), v.minLength(1, `must include at least one source`)),
58
129
  });
59
130
 
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(),
131
+ const exportConfigSchema = v.looseObject({
132
+ outdir: nonEmptyString,
133
+ files: v.optional(v.array(nonEmptyString)),
134
+ clean: v.optional(v.boolean()),
64
135
  });
65
136
 
66
- const formatterConfigSchema = v.union(
67
- v.object({ type: v.literal('prettier') }),
68
- v.object({
137
+ const formatterConfigSchema = v.union([
138
+ v.looseObject({ type: v.literal('prettier') }),
139
+ v.looseObject({
69
140
  type: v.literal('command'),
70
- command: v.string().assert((value) => value.length > 0, `must not be empty`),
71
- concurrency: v
72
- .number()
73
- .assert((value) => Number.isInteger(value) && value > 0, `must be a positive integer`)
74
- .optional(() => 1),
141
+ command: nonEmptyString,
142
+ concurrency: v.optional(
143
+ v.pipe(
144
+ v.number(),
145
+ v.check((value) => Number.isInteger(value) && value > 0, `must be a positive integer`),
146
+ ),
147
+ () => 1,
148
+ ),
75
149
  }),
76
- v.object({
150
+ v.looseObject({
77
151
  type: v.literal('lsp'),
78
- command: v.string().assert((value) => value.length > 0, `must not be empty`),
152
+ command: nonEmptyString,
79
153
  }),
80
- );
81
-
82
- const isValidLexiconPattern = (pattern: string): boolean => {
83
- if (pattern.endsWith('.*')) {
84
- return isNsid(`${pattern.slice(0, -2)}.x`);
85
- }
86
-
87
- return isNsid(pattern);
88
- };
89
-
90
- const mappingImports: v.Type<ImportMapping['imports']> = v.unknown().chain((value) => {
91
- if (typeof value === 'string') {
92
- if (value.length === 0) {
93
- return v.err('imports must not be empty');
154
+ ]);
155
+
156
+ const mappingImports = v.pipe(
157
+ v.unknown(),
158
+ v.rawTransform<unknown, ImportMapping['imports']>(({ dataset, addIssue, NEVER }) => {
159
+ const value = dataset.value;
160
+ if (typeof value === 'string') {
161
+ if (value.length === 0) {
162
+ addIssue({ message: 'imports must not be empty' });
163
+ return NEVER;
164
+ }
165
+ return value;
94
166
  }
167
+ if (typeof value === 'function') {
168
+ return value as ImportMapping['imports'];
169
+ }
170
+ addIssue({ message: 'imports must be a string or function' });
171
+ return NEVER;
172
+ }),
173
+ );
95
174
 
96
- return v.ok(value);
97
- }
98
-
99
- if (typeof value === 'function') {
100
- return v.ok(value as ImportMapping['imports']);
101
- }
102
-
103
- return v.err('imports must be a string or function');
104
- });
105
-
106
- const importMappingSchema: v.Type<ImportMapping> = v.object({
107
- nsid: v
108
- .array(
109
- v.string().chain((value) => {
110
- if (!isValidLexiconPattern(value)) {
111
- return v.err(`invalid NSID pattern (must be valid NSID or end with .*)`);
112
- }
113
-
114
- return v.ok(value);
115
- }),
116
- )
117
- .assert((patterns) => patterns.length > 0, `nsid requires at least one pattern`),
175
+ const importMappingSchema = v.looseObject({
176
+ nsid: v.pipe(
177
+ v.array(
178
+ v.pipe(
179
+ v.string(),
180
+ v.check(isValidLexiconPattern, `invalid NSID pattern (must be valid NSID or end with .*)`),
181
+ ),
182
+ ),
183
+ v.minLength(1, `nsid requires at least one pattern`),
184
+ ),
118
185
  imports: mappingImports,
119
186
  });
120
187
 
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(),
135
- files: v
136
- .array(v.string().assert((value) => value.length > 0, `must not be empty`))
137
- .assert((value) => value.length > 0, `must include at least one glob pattern`)
138
- .optional(),
139
- imports: v.array(v.string().assert((value) => value.length > 0, `must not be empty`)).optional(),
140
- mappings: v.array(importMappingSchema).optional(),
141
- modules: modulesConfigSchema.optional(),
142
- clean: v.boolean().optional(),
188
+ const modulesConfigSchema = v.looseObject({
189
+ importSuffix: v.optional(nonEmptyString),
143
190
  });
144
191
 
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`)
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(),
172
- formatter: formatterConfigSchema.optional(),
173
- generate: generateConfigSchema.optional(),
174
- pull: pullConfigSchema.optional(),
175
- export: exportConfigSchema.optional(),
192
+ const generateConfigSchema = v.looseObject({
193
+ outdir: v.optional(nonEmptyString),
194
+ files: v.optional(
195
+ v.pipe(v.array(nonEmptyString), v.minLength(1, `must include at least one glob pattern`)),
196
+ ),
197
+ imports: v.optional(v.array(nonEmptyString)),
198
+ mappings: v.optional(v.array(importMappingSchema)),
199
+ modules: v.optional(modulesConfigSchema),
200
+ clean: v.optional(v.boolean()),
176
201
  });
177
202
 
178
- export type LexiconConfig = v.Infer<typeof lexiconConfigSchema>;
179
-
180
- export type NormalizedConfig = Omit<
181
- LexiconConfig,
182
- 'formatter' | 'outdir' | 'files' | 'imports' | 'mappings' | 'modules'
183
- > & {
184
- formatter: FormatterConfig;
185
- root: string;
186
- };
203
+ export const lexiconConfigSchema: v.GenericSchema<unknown, Omit<NormalizedConfig, 'root'>> = v.looseObject({
204
+ formatter: v.optional(formatterConfigSchema, (): FormatterConfig => ({ type: 'prettier' })),
205
+ generate: v.optional(generateConfigSchema),
206
+ pull: v.optional(pullConfigSchema),
207
+ export: v.optional(exportConfigSchema),
208
+ });
187
209
 
188
210
  export const loadConfig = async (configPath?: string): Promise<NormalizedConfig> => {
189
211
  let configFilename: string | undefined;
@@ -191,7 +213,6 @@ export const loadConfig = async (configPath?: string): Promise<NormalizedConfig>
191
213
  if (configPath) {
192
214
  configFilename = path.resolve(configPath);
193
215
  } else {
194
- // try to find lex.config.js or lex.config.ts in the current directory
195
216
  const candidates = ['lex.config.js', 'lex.config.ts'];
196
217
 
197
218
  for (const candidate of candidates) {
@@ -226,50 +247,12 @@ export const loadConfig = async (configPath?: string): Promise<NormalizedConfig>
226
247
  process.exit(1);
227
248
  }
228
249
 
229
- const configResult = lexiconConfigSchema.try(rawConfig, { mode: 'passthrough' });
230
- if (!configResult.ok) {
250
+ const configResult = v.safeParse(lexiconConfigSchema, rawConfig);
251
+ if (!configResult.success) {
231
252
  console.error(pc.bold(pc.red(`invalid config:`)));
232
- console.error(configResult.message);
233
-
234
- for (const issue of configResult.issues) {
235
- console.log(`- ${issue.code} at .${issue.path.join('.')}`);
236
- }
237
-
253
+ printValibotIssues(configResult.issues);
238
254
  process.exit(1);
239
255
  }
240
256
 
241
- const {
242
- formatter = { type: 'prettier' },
243
- outdir,
244
- files,
245
- imports,
246
- mappings,
247
- modules,
248
- generate,
249
- ...rest
250
- } = configResult.value;
251
-
252
- // back-compat: top-level generate options were moved into `generate.*`. merge the legacy
253
- // top-level values into `generate`, with nested `generate.*` winning on conflicts. the result
254
- // is only present if at least one generate-related option was provided anywhere.
255
- const hasLegacyTopLevel =
256
- outdir !== undefined ||
257
- files !== undefined ||
258
- imports !== undefined ||
259
- mappings !== undefined ||
260
- modules !== undefined;
261
-
262
- let normalizedGenerate: GenerateConfig | undefined;
263
- if (generate || hasLegacyTopLevel) {
264
- normalizedGenerate = {
265
- outdir: generate?.outdir ?? outdir,
266
- files: generate?.files ?? files,
267
- imports: generate?.imports ?? imports,
268
- mappings: generate?.mappings ?? mappings,
269
- modules: generate?.modules ?? modules,
270
- clean: generate?.clean,
271
- };
272
- }
273
-
274
- return { ...rest, formatter, generate: normalizedGenerate, root: configDirname };
257
+ return { ...configResult.output, root: configDirname };
275
258
  };
package/src/index.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import * as v from 'valibot';
2
+
1
3
  import { lexiconConfigSchema, type LexiconConfig } from './config.ts';
2
4
 
3
5
  export type { LexiconConfig };
4
6
 
5
7
  export const defineLexiconConfig = (config: LexiconConfig): LexiconConfig => {
6
- return lexiconConfigSchema.parse(config);
8
+ return v.parse(lexiconConfigSchema, config);
7
9
  };
@@ -6,6 +6,9 @@ import { lexiconDoc, refineLexiconDoc, type LexiconDoc } from '@atcute/lexicon-d
6
6
  import { build, type LexDocumentBuilder } from '@atcute/lexicon-doc/builder';
7
7
 
8
8
  import pc from 'picocolors';
9
+ import * as v from 'valibot';
10
+
11
+ import { printValibotIssues } from './utils/issues.ts';
9
12
 
10
13
  /** file extensions recognized as module files */
11
14
  const MODULE_EXTENSIONS = new Set(['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts']);
@@ -70,19 +73,14 @@ const loadJsonFile = async (absolutePath: string, relativePath: string): Promise
70
73
  process.exit(1);
71
74
  }
72
75
 
73
- const result = lexiconDoc.try(json, { mode: 'strip' });
74
- if (!result.ok) {
76
+ const result = v.safeParse(lexiconDoc, json);
77
+ if (!result.success) {
75
78
  console.error(pc.bold(pc.red(`schema validation failed for "${relativePath}"`)));
76
- console.error(result.message);
77
-
78
- for (const issue of result.issues) {
79
- console.log(`- ${issue.code} at .${issue.path.join('.')}`);
80
- }
81
-
79
+ printValibotIssues(result.issues);
82
80
  process.exit(1);
83
81
  }
84
82
 
85
- const issues = refineLexiconDoc(result.value, true);
83
+ const issues = refineLexiconDoc(result.output, true);
86
84
  if (issues.length > 0) {
87
85
  console.error(pc.bold(pc.red(`lint validation failed for "${relativePath}"`)));
88
86
 
@@ -93,7 +91,7 @@ const loadJsonFile = async (absolutePath: string, relativePath: string): Promise
93
91
  process.exit(1);
94
92
  }
95
93
 
96
- return result.value;
94
+ return result.output;
97
95
  };
98
96
 
99
97
  /**