Skip to content

Commit

Permalink
feat: Improve CLI styling output for doctor command (#193)
Browse files Browse the repository at this point in the history
  • Loading branch information
kitten authored Apr 12, 2024
1 parent 6ed2325 commit df302c7
Show file tree
Hide file tree
Showing 11 changed files with 1,085 additions and 61 deletions.
5 changes: 5 additions & 0 deletions .changeset/strange-ways-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@gql.tada/cli-utils": patch
---

Improve log output of `doctor` command.
48 changes: 48 additions & 0 deletions packages/cli-utils/LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,30 @@ The above copyright notice and this permission notice shall be included in all c

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.

## @0no-co/graphql.web

MIT License

Copyright (c) 0no.co

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.

## @clack/prompts

MIT License
Expand Down Expand Up @@ -132,6 +156,30 @@ 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.

## wonka

MIT License

Copyright (c) 0no.co

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.

## cross-spawn

The MIT License (MIT)
Expand Down
9 changes: 5 additions & 4 deletions packages/cli-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@
"sade": "^1.8.1",
"semiver": "^1.1.0",
"type-fest": "^4.10.2",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"wonka": "^6.3.4"
},
"dependencies": {
"@gql.tada/internal": "workspace:*",
"@0no-co/graphqlsp": "^1.9.1",
"ts-morph": "~22.0.0",
"graphql": "^16.8.1"
"@gql.tada/internal": "workspace:*",
"graphql": "^16.8.1",
"ts-morph": "~22.0.0"
},
"peerDependencies": {
"typescript": "^5.0.0"
Expand Down
234 changes: 177 additions & 57 deletions packages/cli-utils/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,24 @@ import type { TsConfigJson } from 'type-fest';
import { resolveTypeScriptRootDir } from '@gql.tada/internal';
import { existsSync } from 'node:fs';

import { getGraphQLSPConfig } from '../lsp';
import { initTTY } from '../term';
import * as logger from '../loggers/check';

// NOTE: Currently, most tasks in this command complete too quickly
// We slow them down to make the CLI output easier to follow along to
const delay = (ms = 700) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});

const enum Messages {
TITLE = 'Doctor',
DESCRIPTION = 'Detects problems with your setup',
CHECK_TS_VERSION = 'Checking TypeScript version',
CHECK_DEPENDENCIES = 'Checking installed dependencies',
CHECK_TSCONFIG = 'Checking tsconfig.json',
CHECK_SCHEMA = 'Checking schema',
}

const MINIMUM_VERSIONS = {
typescript: '4.1.0',
Expand All @@ -15,21 +32,31 @@ const MINIMUM_VERSIONS = {
};

export async function executeTadaDoctor() {
await initTTY().start(run());
}

async function* run() {
yield logger.title(Messages.TITLE, Messages.DESCRIPTION);
yield logger.runningTask(Messages.CHECK_TS_VERSION);
await delay();

// Check TypeScript version
const cwd = process.cwd();
const packageJsonPath = path.resolve(cwd, 'package.json');
let packageJsonContents: {
dependencies: Record<string, string>;
devDependencies: Record<string, string>;
};

try {
const file = path.resolve(packageJsonPath);
packageJsonContents = JSON.parse(await fs.readFile(file, 'utf-8'));
} catch (error) {
console.error(
'Failed to read package.json in current working directory, try running the doctor command in your workspace folder.'
} catch (_error) {
yield logger.failedTask(Messages.CHECK_TS_VERSION);
throw logger.errorMessage(
`A ${logger.code('package.json')} file was not found in the current working directory.\n` +
logger.hint('Try running the doctor command in your workspace folder.')
);
return;
}

const deps = Object.entries({
Expand All @@ -39,96 +66,189 @@ export async function executeTadaDoctor() {

const typeScriptVersion = deps.find((x) => x[0] === 'typescript');
if (!typeScriptVersion) {
console.error('Failed to find a "typescript" installation, try installing one.');
return;
yield logger.failedTask(Messages.CHECK_TS_VERSION);
throw logger.errorMessage(
`A version of ${logger.code('typescript')} was not found in your dependencies.\n` +
logger.hint(`Is ${logger.code('typescript')} installed in this package?`)
);
} else if (semiver(typeScriptVersion[1], MINIMUM_VERSIONS.typescript) === -1) {
// TypeScript version lower than v4.1 which is when they introduced template lits
console.error(
`Found an outdated "TypeScript" version, gql.tada requires at least ${MINIMUM_VERSIONS.typescript}.`
yield logger.failedTask(Messages.CHECK_TS_VERSION);
throw logger.errorMessage(
`The version of ${logger.code('typescript')} in your dependencies is out of date.\n` +
logger.hint(
`${logger.code('gql.tada')} requires at least ${logger.bold(MINIMUM_VERSIONS.typescript)}`
)
);
return;
}

const gqlspVersion = deps.find((x) => x[0] === "'@0no-co/graphqlsp'");
yield logger.completedTask(Messages.CHECK_TS_VERSION);
yield logger.runningTask(Messages.CHECK_DEPENDENCIES);
await delay();

const gqlspVersion = deps.find((x) => x[0] === '@0no-co/graphqlsp');
if (!gqlspVersion) {
console.error('Failed to find a "@0no-co/graphqlsp" installation, try installing one.');
return;
yield logger.failedTask(Messages.CHECK_DEPENDENCIES);
throw logger.errorMessage(
`A version of ${logger.code('@0no-co/graphqlsp')} was not found in your dependencies.\n` +
logger.hint(`Is ${logger.code('@0no-co/graphqlsp')} installed?`)
);
} else if (semiver(gqlspVersion[1], MINIMUM_VERSIONS.lsp) === -1) {
console.error(
`Found an outdated "@0no-co/graphqlsp" version, gql.tada requires at least ${MINIMUM_VERSIONS.lsp}.`
yield logger.failedTask(Messages.CHECK_DEPENDENCIES);
throw logger.errorMessage(
`The version of ${logger.code('@0no-co/graphqlsp')} in your dependencies is out of date.\n` +
logger.hint(
`${logger.code('gql.tada')} requires at least ${logger.bold(MINIMUM_VERSIONS.lsp)}`
)
);
return;
}

const gqlTadaVersion = deps.find((x) => x[0] === "'gql.tada'");
const gqlTadaVersion = deps.find((x) => x[0] === 'gql.tada');
if (!gqlTadaVersion) {
console.error('Failed to find a "gql.tada" installation, try installing one.');
return;
yield logger.failedTask(Messages.CHECK_DEPENDENCIES);
throw logger.errorMessage(
`A version of ${logger.code('gql.tada')} was not found in your dependencies.\n` +
logger.hint(`Is ${logger.code('gql.tada')} installed?`)
);
} else if (semiver(gqlTadaVersion[1], '1.0.0') === -1) {
console.error(
`Found an outdated "gql.tada" version, gql.tada requires at least ${MINIMUM_VERSIONS.tada}.`
yield logger.failedTask(Messages.CHECK_DEPENDENCIES);
throw logger.errorMessage(
`The version of ${logger.code('gql.tada')} in your dependencies is out of date.\n` +
logger.hint(
`It's recommended to upgrade ${logger.code('gql.tada')} to at least ${logger.bold(
MINIMUM_VERSIONS.lsp
)}`
)
);
return;
}

yield logger.completedTask(Messages.CHECK_DEPENDENCIES);
yield logger.runningTask(Messages.CHECK_TSCONFIG);
await delay();

const tsconfigpath = path.resolve(cwd, 'tsconfig.json');
const root = (await resolveTypeScriptRootDir(tsconfigpath)) || cwd;

let tsconfigContents: string;
try {
const file = path.resolve(root, 'tsconfig.json');
tsconfigContents = await fs.readFile(file, 'utf-8');
} catch (error) {
console.error(
'Failed to read tsconfig.json in current working directory, try adding a "tsconfig.json".'
tsconfigContents = await fs.readFile(tsconfigpath, 'utf-8');
} catch (_error) {
yield logger.failedTask(Messages.CHECK_TSCONFIG);
throw logger.errorMessage(
`A ${logger.code('tsconfig.json')} file was not found in the current working directory.\n` +
logger.hint(
`Set up a new ${logger.code('tsconfig.json')} containing ${logger.code(
'@0no-co/graphqlp'
)}.`
)
);
return;
}

let tsConfig: TsConfigJson;
try {
tsConfig = parse(tsconfigContents) as TsConfigJson;
} catch (err) {
console.error('Unable to parse tsconfig.json in current working directory.', err);
return;
} catch (error: any) {
yield logger.failedTask(Messages.CHECK_TSCONFIG);
throw logger.errorMessage(
`Your ${logger.code('tsconfig.json')} file could not be parsed.\n` +
logger.console(error.message)
);
}

let root: string;
try {
root = (await resolveTypeScriptRootDir(tsconfigpath)) || cwd;
} catch (error: any) {
yield logger.failedTask(Messages.CHECK_TSCONFIG);
throw logger.errorMessage(
`Failed to resolve a ${logger.code('"extends"')} reference in your ${logger.code(
'tsconfig.json'
)}.\n` + logger.console(error.message)
);
}

if (root !== cwd) {
try {
tsconfigContents = await fs.readFile(path.resolve(root, 'tsconfig.json'), 'utf-8');
tsConfig = parse(tsconfigContents) as TsConfigJson;
} catch (error: any) {
const relative = path.relative(process.cwd(), root);
yield logger.failedTask(Messages.CHECK_TSCONFIG);
throw logger.errorMessage(
`The ${logger.code('tsconfig.json')} file at ${logger.code(
relative
)} could not be loaded.\n` + logger.console(error.message)
);
}
}

// Check GraphQLSP version, later on we can check if a ts version is > 5.5.0 to use gql.tada/lsp instead of
// the LSP package.
const config = getGraphQLSPConfig(tsConfig);
const config =
tsConfig &&
tsConfig.compilerOptions &&
tsConfig.compilerOptions.plugins &&
(tsConfig.compilerOptions.plugins.find(
(plugin) => plugin.name === '@0no-co/graphqlsp' || plugin.name === 'gql.tada/lsp'
) as any);
if (!config) {
console.error(`Missing a "@0no-co/graphqlsp" plugin in your tsconfig.`);
return;
yield logger.failedTask(Messages.CHECK_TSCONFIG);
throw logger.errorMessage(
`No ${logger.code('"@0no-co/graphqlsp"')} plugin was found in your ${logger.code(
'tsconfig.json'
)}.\n` + logger.hint(`Have you set up ${logger.code('"@0no-co/graphqlsp"')} yet?`)
);
}

// TODO: this is optional I guess with the CLI being there and all
if (!config.tadaOutputLocation) {
console.error(`Missing a "tadaOutputLocation" setting in your GraphQLSP configuration.`);
return;
yield logger.failedTask(Messages.CHECK_TSCONFIG);
throw logger.errorMessage(
`No ${logger.code('"tadaOutputLocation"')} option was found in your configuration.\n` +
logger.hint(
`Have you chosen an output path for ${logger.code('gql.tada')}'s declaration file yet?`
)
);
}

if (!config.schema) {
console.error(`Missing a "schema" setting in your GraphQLSP configuration.`);
return;
yield logger.failedTask(Messages.CHECK_TSCONFIG);
throw logger.errorMessage(
`No ${logger.code('"schema"')} option was found in your configuration.\n` +
logger.hint(`Have you specified your SDL file or URL in your configuration yet?`)
);
}

yield logger.completedTask(Messages.CHECK_TSCONFIG);
yield logger.runningTask(Messages.CHECK_SCHEMA);
await delay();

// TODO: This doesn't match laoders. Should we just use loaders here?
const isFile =
typeof config.schema === 'string' &&
(config.schema.endsWith('.json') || config.schema.endsWith('.graphql'));
if (isFile) {
const resolvedFile = path.resolve(root, config.schema as string);
if (!existsSync(resolvedFile)) {
yield logger.failedTask(Messages.CHECK_TSCONFIG);
throw logger.errorMessage(
`Could not find the SDL file that ${logger.code('"schema"')} is specifying.\n` +
logger.hint(`Have you specified a valid SDL file in your configuration?`)
);
}
} else {
const isFile =
typeof config.schema === 'string' &&
(config.schema.endsWith('.json') || config.schema.endsWith('.graphql'));
if (isFile) {
const resolvedFile = path.resolve(root, config.schema as string);
if (!existsSync(resolvedFile)) {
console.error(`The schema setting does not point at an existing file "${resolvedFile}"`);
return;
}
} else {
try {
typeof config.schema === 'string' ? new URL(config.schema) : new URL(config.schema.url);
} catch (e) {
console.error(
`The schema setting does not point at a valid URL "${JSON.stringify(config.schema)}"`
);
return;
}
try {
typeof config.schema === 'string' ? new URL(config.schema) : new URL(config.schema.url);
} catch (_error) {
yield logger.failedTask(Messages.CHECK_TSCONFIG);
throw logger.errorMessage(
`The ${logger.code('"schema"')} option is neither a valid URL nor a valid file.\n` +
logger.hint(`Have you specified a valid URL in your configuration?`)
);
}
}

yield logger.completedTask(Messages.CHECK_SCHEMA, true);
await delay();

yield logger.success();
}
Loading

0 comments on commit df302c7

Please sign in to comment.