-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathcli.ts
More file actions
287 lines (257 loc) · 8.96 KB
/
Copy pathcli.ts
File metadata and controls
287 lines (257 loc) · 8.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
#!/usr/bin/env node
// LLM agent docs: See the llm-docs/ directory in the package root for
// Markdown documentation covering all Grats features and configuration.
import * as E from "./Errors.js";
import { Location } from "graphql";
import { getParsedTsConfig } from "./index.js";
import {
SchemaAndDoc,
buildSchemaAndDocResult,
extractSchemaAndDoc,
} from "./lib.js";
import { Command } from "commander";
import { writeFileSync, readFileSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
import { locate } from "./Locate.js";
import {
printGratsSDL,
printExecutableSchema,
printEnumsModule,
} from "./printSchema.js";
import * as ts from "typescript";
import {
diagnosticsMessage,
locationlessErr,
ReportableDiagnostics,
DiagnosticsWithoutLocationResult,
} from "./utils/DiagnosticError.js";
import { GratsConfig, ParsedCommandLineGrats } from "./gratsConfig.js";
import { err, ok } from "./utils/Result.js";
import { cacheFromProgram, cachesAreEqual, RunCache } from "./runCache.js";
import { withFixesFixed, FixOptions, applyFixes } from "./fixFixable.js";
type BuildOptions = FixOptions;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const version = readPackageVersion();
function readPackageVersion(): string {
// Works from both source (src/cli.ts → ../package.json)
// and compiled output (dist/src/cli.js → ../../package.json)
for (const relPath of ["../package.json", "../../package.json"]) {
try {
const pkg = JSON.parse(readFileSync(resolve(__dirname, relPath), "utf8"));
if (pkg.name === "grats") return pkg.version;
} catch {
// Ignore missing/unreadable package.json files
}
}
console.error(
"Grats: Could not determine package version. Please report this issue at https://github.com/captbaritone/grats/issues",
);
return "unknown";
}
const program = new Command();
program
.name("grats")
.description("Extract GraphQL schema from your TypeScript project")
.version(version)
.option(
"--tsconfig <TSCONFIG>",
"Path to tsconfig.json. Defaults to auto-detecting based on the current working directory",
)
.option("--watch", "Watch for changes and rebuild schema files as needed")
.option("--fix", "Automatically fix fixable diagnostics")
.action(async ({ tsconfig, watch, fix }) => {
if (watch) {
startWatchMode(tsconfig, { fix, log: console.error });
} else {
runBuild(tsconfig, { fix, log: console.error });
}
});
program
.command("locate")
.argument(
"<COORDINATE>",
"Schema coordinate to locate. E.g. `User`, `User.name`, `Query.user(id:)`, `@deprecated`",
)
.option(
"--tsconfig <TSCONFIG>",
"Path to tsconfig.json. Defaults to auto-detecting based on the current working directory",
)
.action((entity, { tsconfig }) => {
const { config } = handleDiagnostics(getTsConfig(tsconfig));
const { schema } = handleDiagnostics(buildSchemaAndDocResult(config));
const loc = locate(schema, entity);
if (loc.kind === "ERROR") {
console.error(loc.err);
process.exit(1);
}
console.log(formatLoc(loc.value));
});
program.parse();
/**
* Run the compiler in watch mode.
*/
function startWatchMode(tsconfig: string, options: BuildOptions) {
const configInfo = handleDiagnostics(
withFixesFixed(() => getTsConfig(tsconfig), options),
);
const { configPath } = configInfo;
let config = configInfo.config;
const watchHost = ts.createWatchCompilerHost(
configPath,
{},
ts.sys,
ts.createSemanticDiagnosticsBuilderProgram,
(diagnostic) => reportDiagnostics([diagnostic]),
(diagnostic) => {
// Some messages we handle ourselves since we ignore some updates. e.g.
// when we observe a change we ourselves created.
switch (diagnostic.code) {
case 6031: // Starting compilation in watch mode...
case 6032: // File change detected. Starting incremental compilation...
return;
default:
reportDiagnostics([diagnostic]);
}
},
);
let lastRunCache: RunCache | null = null;
watchHost.afterProgramCreate = (builderProgram) => {
const program = builderProgram.getProgram();
const runCache = cacheFromProgram(program);
if (lastRunCache != null) {
const tsSchemaPath = resolve(
dirname(configPath),
config.raw.grats.tsSchema,
);
const ignorePaths = new Set([tsSchemaPath]);
if (cachesAreEqual(lastRunCache, runCache, ignorePaths)) {
return;
}
reportDiagnostics([
diagnosticsMessage(
"File change detected. Starting incremental compilation...",
),
]);
}
lastRunCache = runCache;
function fixOrReport(diagnostics: ts.Diagnostic[]) {
if (options.fix && applyFixes(diagnostics, options)) {
// Watch mode should re-run after applying fixes
return;
}
reportDiagnostics(diagnostics);
}
// It's possible our config was updated, so re-read it.
const configResult = getTsConfig(tsconfig);
if (configResult.kind === "ERROR") {
fixOrReport(configResult.err);
return;
}
config = configResult.value.config;
// For now we just rebuild the schema on every change.
const schemaResult = extractSchemaAndDoc(config, program);
if (schemaResult.kind === "ERROR") {
fixOrReport(schemaResult.err);
return;
}
writeSchemaFilesAndReport(schemaResult.value, config, configPath);
};
reportDiagnostics([
diagnosticsMessage("Starting compilation in watch mode..."),
]);
ts.createWatchProgram(watchHost);
}
/**
* Run the compiler performing a single build.
*/
function runBuild(tsconfig: string, options: BuildOptions) {
const { config, configPath } = handleDiagnostics(
withFixesFixed(() => getTsConfig(tsconfig), options),
);
const schemaAndDoc = handleDiagnostics(
withFixesFixed(() => buildSchemaAndDocResult(config), options),
);
writeSchemaFilesAndReport(schemaAndDoc, config, configPath);
}
/**
* Serializes the SDL and TypeScript schema to disk and reports to the console.
*/
function writeSchemaFilesAndReport(
schemaAndDoc: SchemaAndDoc,
config: ParsedCommandLineGrats,
configPath: string,
) {
const { schema, doc, resolvers } = schemaAndDoc;
const gratsConfig: GratsConfig = config.raw.grats;
const dest = resolve(dirname(configPath), gratsConfig.tsSchema);
const code = printExecutableSchema(schema, resolvers, gratsConfig, dest);
writeFileSync(dest, code);
console.error(`Grats: Wrote TypeScript schema to \`${dest}\`.`);
const schemaStr = printGratsSDL(doc, gratsConfig);
const absOutput = resolve(dirname(configPath), gratsConfig.graphqlSchema);
writeFileSync(absOutput, schemaStr);
console.error(`Grats: Wrote schema to \`${absOutput}\`.`);
if (config.raw.grats.EXPERIMENTAL__emitMetadata) {
const absOutput = resolve(
dirname(configPath),
gratsConfig.graphqlSchema.replace(/\.graphql$/, ".json"),
);
writeFileSync(absOutput, JSON.stringify(resolvers, null, 2));
console.error(`Grats: Wrote resolver signatures to \`${absOutput}\`.`);
}
if (config.raw.grats.tsClientEnums != null) {
const absOutput = resolve(
dirname(configPath),
config.raw.grats.tsClientEnums,
);
const enumCode = printEnumsModule(schema, gratsConfig, absOutput);
writeFileSync(absOutput, enumCode);
console.error(`Grats: Wrote enums module to \`${absOutput}\`.`);
}
}
/**
* Utility function to report diagnostics to the console.
*/
function reportDiagnostics(diagnostics: ts.Diagnostic[]) {
const reportable = ReportableDiagnostics.fromDiagnostics(diagnostics);
reportReportableDiagnostics(reportable);
}
function reportReportableDiagnostics(reportable: ReportableDiagnostics) {
console.error(reportable.formatDiagnosticsWithColorAndContext());
}
/**
* Utility function to report diagnostics to the console.
*/
function handleDiagnostics<T>(result: DiagnosticsWithoutLocationResult<T>): T {
if (result.kind === "ERROR") {
const reportable = ReportableDiagnostics.fromDiagnostics(result.err);
console.error(reportable.formatDiagnosticsWithColorAndContext());
process.exit(1);
}
return result.value;
}
// Locate and read the tsconfig.json file
function getTsConfig(tsconfig?: string): DiagnosticsWithoutLocationResult<{
configPath: string;
config: ParsedCommandLineGrats;
}> {
const cwd = process.cwd();
const configPath = tsconfig || ts.findConfigFile(cwd, ts.sys.fileExists);
if (configPath == null) {
return err([locationlessErr(E.tsConfigNotFound(cwd))]);
}
const optionsResult = getParsedTsConfig(configPath);
if (optionsResult.kind === "ERROR") {
return err(optionsResult.err);
}
return ok({ configPath, config: optionsResult.value });
}
// Format a location for printing to the console. Tools like VS Code and iTerm
// will automatically turn this into a clickable link.
export function formatLoc(loc: Location) {
return `${loc.source.name}:${loc.startToken.line + 1}:${
loc.startToken.column + 1
}`;
}