···11+---
22+'@atcute/lex-cli': patch
33+---
44+55+add config auto-discovery
66+77+given that config files are pretty much a required part of lex-cli, the CLI tool now attempts to
88+search for the presence of `lex.config.js` or `lex.config.ts` when a config file is not explicitly
99+specified.
···2020then run the tool:
21212222```
2323-npm exec lex-cli generate -c ./lex.config.js
2323+npm exec lex-cli generate
2424```
25252626## pulling lexicons
···5353pull the lexicons to disk, then generate types from them:
54545555```
5656-npm exec lex-cli pull -c ./lex.config.js
5757-npm exec lex-cli generate -c ./lex.config.js
5656+npm exec lex-cli pull
5757+npm exec lex-cli generate
5858```
59596060## publishing your schemas
61616262-if you're packaging your generated schemas as a publishable library, add the `atcute:lexicons`
6363-field to your package.json. this allows other projects to automatically discover and import your
6464-schemas without manual configuration.
6262+if you're packaging your generated schemas as a publishable library, add the `atcute:lexicons` field
6363+to your package.json. this allows other projects to automatically discover and import your schemas
6464+without manual configuration.
65656666```json
6767{
+6-210
packages/lexicons/lex-cli/src/cli.ts
···11-import * as fs from 'node:fs/promises';
22-import * as path from 'node:path';
33-44-import { lexiconDoc, refineLexiconDoc, type LexiconDoc } from '@atcute/lexicon-doc';
55-66-import { object } from '@optique/core/constructs';
77-import { command, constant, option } from '@optique/core/primitives';
81import { or } from '@optique/core/constructs';
92import { run } from '@optique/run';
1010-import { path as pathParser } from '@optique/run/valueparser';
1111-import pc from 'picocolors';
1212-1313-import { generateLexiconApi, type ImportMapping } from './codegen.js';
1414-import { loadConfig } from './config.js';
1515-import { packageJsonSchema } from './lexicon-metadata.js';
1616-import { runPull } from './pull.js';
1731818-/**
1919- * Resolves package imports to ImportMapping[]
2020- */
2121-const resolveImportsToMappings = async (
2222- imports: string[],
2323- configDirname: string,
2424-): Promise<ImportMapping[]> => {
2525- const mappings: ImportMapping[] = [];
2626-2727- for (const packageName of imports) {
2828- // Walk up from config directory to find package in node_modules
2929- let packageJson: unknown;
3030- let currentDir = configDirname;
3131- let found = false;
3232-3333- while (currentDir !== path.dirname(currentDir)) {
3434- const candidatePath = path.join(currentDir, 'node_modules', packageName, 'package.json');
3535- try {
3636- const content = await fs.readFile(candidatePath, 'utf8');
3737- packageJson = JSON.parse(content);
3838- found = true;
3939- break;
4040- } catch (err: any) {
4141- // Only continue to parent if file not found
4242- if (err.code !== 'ENOENT') {
4343- console.error(pc.bold(pc.red(`failed to read package.json for "${packageName}":`)));
4444- console.error(err);
4545- process.exit(1);
4646- }
4747-4848- // Not found, try parent directory
4949- currentDir = path.dirname(currentDir);
5050- }
5151- }
5252-5353- if (!found) {
5454- console.error(pc.bold(pc.red(`failed to resolve package "${packageName}"`)));
5555- console.error(`Could not find package in node_modules starting from ${configDirname}`);
5656- process.exit(1);
5757- }
5858-5959- // Validate package.json
6060- const result = packageJsonSchema.try(packageJson, { mode: 'passthrough' });
6161- if (!result.ok) {
6262- console.error(pc.bold(pc.red(`invalid atcute:lexicons in "${packageName}":`)));
6363- console.error(result.message);
6464-6565- for (const issue of result.issues) {
6666- console.log(`- ${issue.code} at .${issue.path.join('.')}`);
6767- }
6868-6969- process.exit(1);
7070- }
7171-7272- const lexicons = result.value['atcute:lexicons'];
7373- if (!lexicons?.mappings) {
7474- continue;
7575- }
7676-7777- // Convert mapping to ImportMapping[]
7878- for (const [pattern, entry] of Object.entries(lexicons.mappings)) {
7979- const isWildcard = pattern.endsWith('.*');
8080-8181- mappings.push({
8282- nsid: [pattern],
8383- imports: (nsid: string) => {
8484- // Check if pattern matches
8585- if (isWildcard) {
8686- if (!nsid.startsWith(pattern.slice(0, -1))) {
8787- throw new Error(`NSID ${nsid} does not match pattern ${pattern}`);
8888- }
8989- } else {
9090- if (nsid !== pattern) {
9191- throw new Error(`NSID ${nsid} does not match pattern ${pattern}`);
9292- }
9393- }
9494-9595- const nsidPrefix = isWildcard ? pattern.slice(0, -2) : pattern;
9696- const nsidRemainder = isWildcard ? nsid.slice(nsidPrefix.length + 1) : '';
9797-9898- let expandedPath = entry.path
9999- .replaceAll('{{nsid}}', nsid.replaceAll('.', '/'))
100100- .replaceAll('{{nsid_remainder}}', nsidRemainder.replaceAll('.', '/'))
101101- .replaceAll('{{nsid_prefix}}', nsidPrefix.replaceAll('.', '/'));
102102-103103- if (expandedPath === '.') {
104104- expandedPath = packageName;
105105- } else if (expandedPath.startsWith('./')) {
106106- expandedPath = `${packageName}/${expandedPath.slice(2)}`;
107107- }
108108-109109- return {
110110- type: entry.type,
111111- from: expandedPath,
112112- };
113113- },
114114- });
115115- }
116116- }
117117-118118- return mappings;
119119-};
44+import { generateCommandSchema, runGenerate } from './commands/generate.js';
55+import { pullCommandSchema, runPull } from './commands/pull.js';
1206121121-const parser = or(
122122- command(
123123- 'generate',
124124- object({
125125- type: constant('generate'),
126126- config: option('-c', '--config', pathParser({ metavar: 'CONFIG' })),
127127- }),
128128- ),
129129- command(
130130- 'pull',
131131- object({
132132- type: constant('pull'),
133133- config: option('-c', '--config', pathParser({ metavar: 'CONFIG' })),
134134- }),
135135- ),
136136-);
77+const parser = or(generateCommandSchema, pullCommandSchema);
1378138138-const result = run(parser, { programName: 'lex-cli' });
99+const result = run(parser, { programName: 'lex-cli', help: 'both' });
1391014011if (result.type === 'generate') {
141141- const config = await loadConfig(result.config);
142142-143143- // Resolve imports to mappings
144144- const importMappings = config.imports ? await resolveImportsToMappings(config.imports, config.root) : [];
145145- const allMappings = [...importMappings, ...(config.mappings ?? [])];
146146-147147- const documents: LexiconDoc[] = [];
148148-149149- for await (const filename of fs.glob(config.files, { cwd: config.root })) {
150150- let source: string;
151151- try {
152152- source = await fs.readFile(path.join(config.root, filename), 'utf8');
153153- } catch (err) {
154154- console.error(pc.bold(pc.red(`file read error with "${filename}"`)));
155155- console.error(err);
156156-157157- process.exit(1);
158158- }
159159-160160- let json: unknown;
161161- try {
162162- json = JSON.parse(source);
163163- } catch (err) {
164164- console.error(pc.bold(pc.red(`json parse error in "${filename}"`)));
165165- console.error(err);
166166-167167- process.exit(1);
168168- }
169169-170170- const result = lexiconDoc.try(json, { mode: 'strip' });
171171- if (!result.ok) {
172172- console.error(pc.bold(pc.red(`schema validation failed for "${filename}"`)));
173173- console.error(result.message);
174174-175175- for (const issue of result.issues) {
176176- console.log(`- ${issue.code} at .${issue.path.join('.')}`);
177177- }
178178-179179- process.exit(1);
180180- }
181181-182182- const issues = refineLexiconDoc(result.value, true);
183183- if (issues.length > 0) {
184184- console.error(pc.bold(pc.red(`lint validation failed for "${filename}"`)));
185185-186186- for (const issue of issues) {
187187- console.log(`- ${issue.message} at .${issue.path.join('.')}`);
188188- }
189189-190190- process.exit(1);
191191- }
192192-193193- documents.push(result.value);
194194- }
195195-196196- const generationResult = await generateLexiconApi({
197197- documents: documents,
198198- mappings: allMappings,
199199- modules: {
200200- importSuffix: config.modules?.importSuffix ?? '.js',
201201- },
202202- prettier: {
203203- cwd: process.cwd(),
204204- },
205205- });
206206-207207- const outdir = path.join(config.root, config.outdir);
208208-209209- for (const file of generationResult.files) {
210210- const filename = path.join(outdir, file.filename);
211211- const dirname = path.dirname(filename);
212212-213213- await fs.mkdir(dirname, { recursive: true });
214214- await fs.writeFile(filename, file.code);
215215- }
1212+ await runGenerate(result);
21613} else if (result.type === 'pull') {
217217- const config = await loadConfig(result.config);
218218- await runPull(config);
1414+ await runPull(result);
21915}