Skip to content

Commit

Permalink
feat(cli-utils): Complete CLI output, logging, and config refactor (#200
Browse files Browse the repository at this point in the history
)
  • Loading branch information
kitten committed Apr 15, 2024
1 parent 09bac3f commit 36adc75
Show file tree
Hide file tree
Showing 47 changed files with 1,311 additions and 780 deletions.
5 changes: 5 additions & 0 deletions .changeset/mighty-taxis-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@gql.tada/cli-utils": major
---

Add stylised log output and threading to commands.
5 changes: 5 additions & 0 deletions .changeset/thin-bikes-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@gql.tada/cli-utils": minor
---

Add annotations for GitHub actions to command outputs that report diagnostics.
5 changes: 5 additions & 0 deletions .changeset/twelve-kiwis-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@gql.tada/internal": minor
---

Implement new config resolution helpers
26 changes: 0 additions & 26 deletions packages/cli-utils/LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,32 +120,6 @@ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

## json5

MIT License

Copyright (c) 2012-2018 Aseem Kishore, and [others].

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

[others]: https://github.com/json5/json5/contributors

## merge-stream

The MIT License (MIT)
Expand Down
1 change: 0 additions & 1 deletion packages/cli-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
"@types/node": "^20.11.0",
"clipanion": "4.0.0-rc.3",
"execa": "^8.0.1",
"json5": "^2.2.3",
"rollup": "^4.9.4",
"sade": "^1.8.1",
"semiver": "^1.1.0",
Expand Down
34 changes: 8 additions & 26 deletions packages/cli-utils/src/commands/check/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@ import * as t from '../../term';
import type { DiagnosticMessage } from './types';
import type { SeveritySummary } from './types';

const CWD = process.cwd();
export * from '../shared/logger';
import { indent } from '../shared/logger';

export function code(text: string) {
return t.text`${t.cmd(t.CSI.Style, t.Style.Underline)}${text}${t.cmd(
t.CSI.Style,
t.Style.NoUnderline
)}`;
}
const CWD = process.cwd();
const INDENT = ' ';

export function diagnosticFile(filePath: string) {
const relativePath = path.relative(CWD, filePath);
Expand All @@ -26,8 +23,6 @@ export function diagnosticFile(filePath: string) {
}

export function diagnosticMessage(message: DiagnosticMessage) {
const indent = t.Chars.Space.repeat(2);

let color = t.Style.Foreground;
if (message.severity === 'info') {
color = t.Style.BrightBlue;
Expand All @@ -37,21 +32,16 @@ export function diagnosticMessage(message: DiagnosticMessage) {
color = t.Style.BrightRed;
}

let text = message.message.trim();
if (text.includes('\n')) {
text = text.split('\n').join(t.text([t.Chars.Newline, indent, t.Chars.Tab, t.Chars.Tab]));
}

return t.text([
indent,
INDENT,
t.cmd(t.CSI.Style, t.Style.BrightBlack),
`${message.line}:${message.col}`,
t.Chars.Tab,
t.cmd(t.CSI.Style, color),
message.severity,
t.Chars.Tab,
t.cmd(t.CSI.Style, t.Style.Foreground),
text,
indent(message.message.trim(), t.text([INDENT, t.Chars.Tab, t.Chars.Tab])),
t.Chars.Newline,
]);
}
Expand Down Expand Up @@ -95,6 +85,8 @@ export function diagnosticMessageGithub(message: DiagnosticMessage): void {
file: message.file,
line: message.line,
col: message.col,
endLine: message.endLine,
endColumn: message.endColumn,
});
}

Expand All @@ -115,13 +107,3 @@ export function runningDiagnostics(file: number, ofFiles?: number) {
})
);
}

export function errorMessage(message: string) {
return t.error([
'\n',
t.cmd(t.CSI.Style, [t.Style.Red, t.Style.Invert]),
` ${t.Icons.Warning} Error `,
t.cmd(t.CSI.Style, t.Style.NoInvert),
`\n${message.trim()}\n`,
]);
}
45 changes: 15 additions & 30 deletions packages/cli-utils/src/commands/check/runner.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import path from 'node:path';
import type { GraphQLSPConfig, LoadConfigResult } from '@gql.tada/internal';
import { loadConfig, parseConfig } from '@gql.tada/internal';

import { getGraphQLSPConfig } from '../../lsp';
import { getTsConfig } from '../../tsconfig';
import * as logger from './logger';

import type { ComposeInput } from '../../term';
import type { Severity, SeveritySummary } from './types';

Expand Down Expand Up @@ -33,37 +31,24 @@ export interface Options {
}

export async function* run(opts: Options): AsyncIterable<ComposeInput> {
const CWD = process.cwd();
const { runDiagnostics } = await import('./thread');

const tsconfig = await getTsConfig(opts.tsconfig);
if (!tsconfig) {
const relative = opts.tsconfig
? logger.code(path.relative(process.cwd(), opts.tsconfig))
: 'the current working directory';
throw logger.errorMessage(
`The ${logger.code('tsconfig.json')} file at ${relative} could not be loaded.\n`
);
}

const config = getGraphQLSPConfig(tsconfig);
if (!config) {
throw logger.errorMessage(
`No ${logger.code('"@0no-co/graphqlsp"')} plugin was found in your ${logger.code(
'tsconfig.json'
)}.\n`
);
let configResult: LoadConfigResult;
let pluginConfig: GraphQLSPConfig;
try {
configResult = await loadConfig(opts.tsconfig);
pluginConfig = parseConfig(configResult.pluginConfig);
} catch (error) {
throw logger.externalError('Failed to load configuration.', error);
}

let tsconfigPath = opts.tsconfig || CWD;
tsconfigPath =
path.extname(tsconfigPath) !== '.json'
? path.resolve(CWD, tsconfigPath, 'tsconfig.json')
: path.resolve(CWD, tsconfigPath);

const summary: SeveritySummary = { warn: 0, error: 0, info: 0 };
const minSeverity = opts.minSeverity;
const generator = runDiagnostics({ tsconfigPath, config });
const generator = runDiagnostics({
rootPath: configResult.rootPath,
configPath: configResult.configPath,
pluginConfig,
});

let totalFileCount = 0;
let fileCount = 0;
Expand All @@ -90,7 +75,7 @@ export async function* run(opts: Options): AsyncIterable<ComposeInput> {
yield logger.runningDiagnostics(++fileCount, totalFileCount);
}
} catch (error: any) {
throw logger.errorMessage(error.message || `${error}`);
throw logger.externalError('Could not check files', error);
}

// Reset notice count if it's outside of min severity
Expand Down
61 changes: 26 additions & 35 deletions packages/cli-utils/src/commands/check/thread.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
import * as path from 'node:path';
import { Project, ts } from 'ts-morph';

import { load, resolveTypeScriptRootDir } from '@gql.tada/internal';
import type { GraphQLSPConfig } from '@gql.tada/internal';
import { load } from '@gql.tada/internal';
import { init, getGraphQLDiagnostics } from '@0no-co/graphqlsp/api';

import type { GraphQLSPConfig } from '../../lsp';
import { createPluginInfo } from '../../ts/project';
import { createPluginInfo, getFilePosition } from '../../ts';
import { expose } from '../../threads';

import type { Severity, DiagnosticMessage, DiagnosticSignal } from './types';

const getLineCol = (text: string, start: number | undefined): [number, number] => {
if (text && start) {
let counter = 0;
const parts = text.split('\n');
for (let i = 0; i <= parts.length; i++) {
const line = parts[i];
if (counter + line.length > start) {
return [i + 1, start + 1 - counter];
} else {
counter = counter + (line.length + 1);
continue;
}
}
}
return [0, 0];
};

const loadSchema = async (rootPath: string, config: GraphQLSPConfig) => {
const loader = load({ origin: config.schema, rootPath });
const result = await loader.load();
Expand All @@ -35,53 +18,61 @@ const loadSchema = async (rootPath: string, config: GraphQLSPConfig) => {
};

export interface DiagnosticsParams {
config: GraphQLSPConfig;
tsconfigPath: string;
rootPath: string;
configPath: string;
pluginConfig: GraphQLSPConfig;
}

async function* _runDiagnostics(
params: DiagnosticsParams
): AsyncIterableIterator<DiagnosticSignal> {
init({ typescript: ts as any });
const projectPath = path.dirname(params.tsconfigPath);
const rootPath = (await resolveTypeScriptRootDir(params.tsconfigPath)) || params.tsconfigPath;
const schemaRef = await loadSchema(rootPath, params.config);
const project = new Project({ tsConfigFilePath: params.tsconfigPath });
const pluginInfo = createPluginInfo(project, params.config, projectPath);
const sourceFiles = project.getSourceFiles();
const projectPath = path.dirname(params.configPath);
const schemaRef = await loadSchema(projectPath, params.pluginConfig);
const project = new Project({ tsConfigFilePath: params.configPath });
const pluginInfo = createPluginInfo(project, params.pluginConfig, projectPath);

// Filter source files by whether they're under the relevant root path
const sourceFiles = project.getSourceFiles().filter((sourceFile) => {
const filePath = path.resolve(projectPath, sourceFile.getFilePath());
const relative = path.relative(params.rootPath, filePath);
return !relative.startsWith('..');
});

yield {
kind: 'FILE_COUNT',
fileCount: sourceFiles.length,
};

for (const sourceFile of sourceFiles) {
const filePath = sourceFile.getFilePath();
for (const { compilerNode: sourceFile } of sourceFiles) {
const filePath = sourceFile.fileName;
const diagnostics = getGraphQLDiagnostics(filePath, schemaRef, pluginInfo);
const messages: DiagnosticMessage[] = [];

if (diagnostics && diagnostics.length) {
const sourceText = sourceFile.getText();
for (const diagnostic of diagnostics) {
if (
!('messageText' in diagnostic) ||
typeof diagnostic.messageText !== 'string' ||
!diagnostic.file
)
) {
continue;
}
let severity: Severity = 'info';
if (diagnostic.category === ts.DiagnosticCategory.Error) {
severity = 'error';
} else if (diagnostic.category === ts.DiagnosticCategory.Warning) {
severity = 'warn';
}
const [line, col] = getLineCol(sourceText, diagnostic.start);
const position = getFilePosition(sourceFile, diagnostic.start, diagnostic.length);
messages.push({
severity,
message: diagnostic.messageText,
file: diagnostic.file.fileName,
line,
col,
line: position.line,
col: position.col,
endLine: position.endLine,
endColumn: position.endColumn,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-utils/src/commands/check/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export interface DiagnosticMessage {
file: string;
line: number;
col: number;
endLine: number | undefined;
endColumn: number | undefined;
}

export interface FileDiagnosticsSignal {
Expand Down
22 changes: 1 addition & 21 deletions packages/cli-utils/src/commands/doctor/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,7 @@ import { pipe, interval, map } from 'wonka';

import * as t from '../../term';

export function code(text: string) {
return t.text`${t.cmd(t.CSI.Style, t.Style.Underline)}${text}${t.cmd(
t.CSI.Style,
t.Style.NoUnderline
)}`;
}

export function bold(text: string) {
return t.text`${t.cmd(t.CSI.Style, t.Style.Bold)}${text}${t.cmd(t.CSI.Style, t.Style.Normal)}`;
}

export function hint(text: string) {
return t.text([
t.cmd(t.CSI.Style, t.Style.BrightBlack),
`${t.HeavyBox.BottomLeft} `,
t.cmd(t.CSI.Style, t.Style.BrightBlue),
`${t.Icons.Info} `,
t.cmd(t.CSI.Style, t.Style.Blue),
text,
]);
}
export * from '../shared/logger';

export function console(error: any) {
return t.text([
Expand Down
Loading

0 comments on commit 36adc75

Please sign in to comment.