@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.
- package/README.md +43 -29
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +6 -7
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/pull.js +26 -9
- package/dist/commands/pull.js.map +1 -1
- package/dist/config.d.ts +60 -157
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +68 -137
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lexicon-loader.d.ts.map +1 -1
- package/dist/lexicon-loader.js +7 -8
- package/dist/lexicon-loader.js.map +1 -1
- package/dist/lexicon-metadata.d.ts +2 -2
- package/dist/lexicon-metadata.d.ts.map +1 -1
- package/dist/lexicon-metadata.js +13 -34
- package/dist/lexicon-metadata.js.map +1 -1
- package/dist/pull-sources/atproto.d.ts.map +1 -1
- package/dist/pull-sources/atproto.js +5 -3
- package/dist/pull-sources/atproto.js.map +1 -1
- package/dist/utils/issues.d.ts +4 -0
- package/dist/utils/issues.d.ts.map +1 -0
- package/dist/utils/issues.js +9 -0
- package/dist/utils/issues.js.map +1 -0
- package/dist/utils/nsid-pattern.d.ts +6 -0
- package/dist/utils/nsid-pattern.d.ts.map +1 -0
- package/dist/utils/nsid-pattern.js +12 -0
- package/dist/utils/nsid-pattern.js.map +1 -0
- package/package.json +7 -7
- package/src/commands/generate.ts +6 -9
- package/src/commands/pull.ts +28 -11
- package/src/config.ts +172 -189
- package/src/index.ts +3 -1
- package/src/lexicon-loader.ts +8 -10
- package/src/lexicon-metadata.ts +19 -36
- package/src/pull-sources/atproto.ts +4 -3
- package/src/utils/issues.ts +9 -0
- 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": "
|
|
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
|
-
"
|
|
32
|
-
"@atcute/identity
|
|
33
|
-
"@atcute/
|
|
34
|
-
"@atcute/
|
|
35
|
-
"@atcute/lexicon-resolver": "^0.
|
|
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",
|
package/src/commands/generate.ts
CHANGED
|
@@ -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 =
|
|
68
|
-
if (!result.
|
|
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
|
-
|
|
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.
|
|
76
|
+
const lexicons = result.output['atcute:lexicons'];
|
|
80
77
|
if (!lexicons?.mappings) {
|
|
81
78
|
continue;
|
|
82
79
|
}
|
package/src/commands/pull.ts
CHANGED
|
@@ -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 =
|
|
56
|
-
if (!result.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
88
|
+
const gitSourceConfigSchema = v.looseObject({
|
|
14
89
|
type: v.literal('git'),
|
|
15
|
-
remote:
|
|
16
|
-
ref: v
|
|
17
|
-
|
|
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.
|
|
95
|
+
const atprotoNsidsSourceConfigSchema = v.looseObject({
|
|
26
96
|
type: v.literal('atproto'),
|
|
27
97
|
mode: v.literal('nsids'),
|
|
28
|
-
nsids: v
|
|
29
|
-
.array(v.
|
|
30
|
-
.
|
|
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.
|
|
104
|
+
const atprotoAuthoritySourceConfigSchema = v.looseObject({
|
|
34
105
|
type: v.literal('atproto'),
|
|
35
106
|
mode: v.literal('authority'),
|
|
36
|
-
authority: v
|
|
37
|
-
.string()
|
|
38
|
-
.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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(
|
|
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.
|
|
53
|
-
outdir:
|
|
54
|
-
clean: v.
|
|
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.
|
|
61
|
-
outdir:
|
|
62
|
-
files: v.
|
|
63
|
-
clean: v.
|
|
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.
|
|
68
|
-
v.
|
|
137
|
+
const formatterConfigSchema = v.union([
|
|
138
|
+
v.looseObject({ type: v.literal('prettier') }),
|
|
139
|
+
v.looseObject({
|
|
69
140
|
type: v.literal('command'),
|
|
70
|
-
command:
|
|
71
|
-
concurrency: v
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
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.
|
|
150
|
+
v.looseObject({
|
|
77
151
|
type: v.literal('lsp'),
|
|
78
|
-
command:
|
|
152
|
+
command: nonEmptyString,
|
|
79
153
|
}),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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 =
|
|
230
|
-
if (!configResult.
|
|
250
|
+
const configResult = v.safeParse(lexiconConfigSchema, rawConfig);
|
|
251
|
+
if (!configResult.success) {
|
|
231
252
|
console.error(pc.bold(pc.red(`invalid config:`)));
|
|
232
|
-
|
|
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
|
-
|
|
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
|
|
8
|
+
return v.parse(lexiconConfigSchema, config);
|
|
7
9
|
};
|
package/src/lexicon-loader.ts
CHANGED
|
@@ -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 =
|
|
74
|
-
if (!result.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
94
|
+
return result.output;
|
|
97
95
|
};
|
|
98
96
|
|
|
99
97
|
/**
|