Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add task summary logging #307

Merged
merged 24 commits into from
Feb 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,19 @@ It's handy to install globally too:

```bash
npm i -g @feltcoop/gro
gro # should print some stuff - defers to the project's locally installed version of Gro
gro # prints available tasks - defers to the project's locally installed version of Gro
```

## usage

```bash
gro # list all available tasks with the pattern `*.task.ts`
gro # print all available tasks with the pattern `*.task.ts`
gro --help # print more info about each available task
gro some/dir # list all tasks inside `src/some/dir`
gro some/file # run `src/some/file.task.ts`
gro some/file.task.ts # same as above
gro test # run `src/test.task.ts` if it exists, falling back to Gro's builtin
gro test --help # print info about the "test" task; works for every task
```

Gro has a number of builtin tasks that you can run with the CLI.
Expand Down
2 changes: 1 addition & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
- add schema information to task args
([#306](https://github.com/feltcoop/gro/pull/306))
- add `--help` flag to `gro` and `gro taskname`, along with `gro help` alias
([#306](https://github.com/feltcoop/gro/pull/306))
([#306](https://github.com/feltcoop/gro/pull/306), [#307](https://github.com/feltcoop/gro/pull/307))
- combine imports in generated schema types
([#304](https://github.com/feltcoop/gro/pull/304))
- add CLI opt-outs to `gro check` for `no-typecheck`, `no-test`, `no-gen`, `no-format`, & `no-lint`
Expand Down
1 change: 1 addition & 0 deletions src/docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ What is a `Task`? See [`tasks.md`](./task.md).
- [dev](../dev.task.ts) - start dev server
- [format](../format.task.ts) - format source files
- [gen](../gen.task.ts) - run code generation scripts
- [help](../help.task.ts) - alias for `gro` with no task name provided
- [lint](../lint.task.ts) - run eslint on the source files
- [publish](../publish.task.ts) - bump version, publish to npm, and git push
- [serve](../serve.task.ts) - start static file server
Expand Down
10 changes: 10 additions & 0 deletions src/help.task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {spawn} from '@feltcoop/felt/util/process.js';

import {type Task} from './task/task.js';

export const task: Task = {
summary: 'alias for `gro` with no task name provided',
run: async (): Promise<void> => {
await spawn('npx', ['gro']);
},
};
51 changes: 19 additions & 32 deletions src/task/invokeTask.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import {cyan, red, gray} from 'kleur/colors';
import {SystemLogger, type Logger, printLogLabel} from '@feltcoop/felt/util/log.js';
import {SystemLogger, printLogLabel} from '@feltcoop/felt/util/log.js';
import {EventEmitter} from 'events';
import {createStopwatch, Timings} from '@feltcoop/felt/util/timings.js';
import {printMs, printTimings} from '@feltcoop/felt/util/print.js';
import {plural} from '@feltcoop/felt/util/string.js';
import {spawn} from '@feltcoop/felt/util/process.js';

import {serializeArgs, type Args} from '../task/task.js';
import {runTask} from './runTask.js';
import {resolveRawInputPath, getPossibleSourceIds} from '../fs/inputPath.js';
import {TASK_FILE_SUFFIX, isTaskPath, toTaskName} from './task.js';
import {TASK_FILE_SUFFIX, isTaskPath} from './task.js';
import {
paths,
groPaths,
sourceIdToBasePath,
replaceRootDir,
pathsFromId,
isGroId,
toImportId,
isThisProjectGro,
Expand All @@ -27,6 +24,7 @@ import {loadTaskModule} from './taskModule.js';
import {loadGroPackageJson} from '../utils/packageJson.js';
import {SYSTEM_BUILD_NAME} from '../build/buildConfigDefaults.js';
import {type Filesystem} from '../fs/filesystem.js';
import {logAvailableTasks, logErrorReasons} from './logTask.js';

/*

Expand Down Expand Up @@ -120,6 +118,7 @@ export const invokeTask = async (
);
const timingToRunTask = timings.start('run task');
const dev = process.env.NODE_ENV !== 'production'; // TODO should this use `fromEnv`? '$app/env'?
// If we're in dev mode but the task is only for production, run it in a new process.
if (dev && task.mod.task.production) {
const result = await spawn('npx', ['gro', taskName, ...serializeArgs(args)], {
env: {...process.env, NODE_ENV: 'production'},
Expand All @@ -135,6 +134,7 @@ export const invokeTask = async (
throw Error('Spawned task failed');
}
} else {
// Run the task in the current process.
const result = await runTask(fs, task, args, events, invokeTask);
timingToRunTask();
if (result.ok) {
Expand All @@ -154,10 +154,14 @@ export const invokeTask = async (
// eslint-disable-next-line no-lonely-if
if (isThisProjectGro) {
// Is the Gro directory the same as the cwd? Log the matching files.
logAvailableTasks(log, printPath(pathData.id), findModulesResult.sourceIdsByInputPath);
await logAvailableTasks(
log,
printPath(pathData.id),
findModulesResult.sourceIdsByInputPath,
);
} else if (isGroId(pathData.id)) {
// Does the Gro directory contain the matching files? Log them.
logAvailableTasks(
await logAvailableTasks(
log,
printPathOrGroPath(pathData.id),
findModulesResult.sourceIdsByInputPath,
Expand All @@ -177,14 +181,19 @@ export const invokeTask = async (
const groPathData =
groDirFindModulesResult.sourceIdPathDataByInputPath.get(groDirInputPath)!;
// First log the Gro matches.
logAvailableTasks(
await logAvailableTasks(
log,
printPathOrGroPath(groPathData.id),
groDirFindModulesResult.sourceIdsByInputPath,
);
}
// Then log the current working directory matches.
logAvailableTasks(log, printPath(pathData.id), findModulesResult.sourceIdsByInputPath);
await logAvailableTasks(
log,
printPath(pathData.id),
findModulesResult.sourceIdsByInputPath,
!groDirFindModulesResult.ok,
);
}
}
} else if (findModulesResult.type === 'inputDirectoriesWithNoFiles') {
Expand All @@ -209,7 +218,7 @@ export const invokeTask = async (
const groPathData =
groDirFindModulesResult.sourceIdPathDataByInputPath.get(groDirInputPath)!;
// Log the Gro matches.
logAvailableTasks(
await logAvailableTasks(
log,
printPathOrGroPath(groPathData.id),
groDirFindModulesResult.sourceIdsByInputPath,
Expand All @@ -231,28 +240,6 @@ export const invokeTask = async (
log.info(`🕒 ${printMs(totalTiming())}`);
};

const logAvailableTasks = (
log: Logger,
dirLabel: string,
sourceIdsByInputPath: Map<string, string[]>,
): void => {
const sourceIds = Array.from(sourceIdsByInputPath.values()).flat();
if (sourceIds.length) {
log.info(`${sourceIds.length} task${plural(sourceIds.length)} in ${dirLabel}:`);
for (const sourceId of sourceIds) {
log.info('\t' + cyan(toTaskName(sourceIdToBasePath(sourceId, pathsFromId(sourceId)))));
}
} else {
log.info(`No tasks found in ${dirLabel}.`);
}
};

const logErrorReasons = (log: Logger, reasons: string[]): void => {
for (const reason of reasons) {
log.error(reason);
}
};

// This is a best-effort heuristic that quickly detects if
// we should compile a project's TypeScript when invoking a task.
// Properly detecting this is too expensive and would slow task startup time significantly.
Expand Down
99 changes: 99 additions & 0 deletions src/task/logTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {cyan, gray, green} from 'kleur/colors';
import {Logger} from '@feltcoop/felt/util/log.js';
import {plural} from '@feltcoop/felt/util/string.js';
import {printValue} from '@feltcoop/felt/util/print.js';

import {type ArgSchema, type ArgsSchema} from './task.js';
import {loadModules} from '../fs/modules.js';
import {loadTaskModule, type TaskModuleMeta} from './taskModule.js';

export const logAvailableTasks = async (
log: Logger,
dirLabel: string,
sourceIdsByInputPath: Map<string, string[]>,
printIntro = true,
): Promise<void> => {
const sourceIds = Array.from(sourceIdsByInputPath.values()).flat();
if (sourceIds.length) {
// Load all of the tasks so we can print their summary, and args for the `--help` flag.
const loadModulesResult = await loadModules(sourceIdsByInputPath, true, loadTaskModule);
if (!loadModulesResult.ok) {
logErrorReasons(log, loadModulesResult.reasons);
process.exit(1);
}
const printed: string[] = [
`${printIntro ? '\n\n' : ''}${sourceIds.length} task${plural(
sourceIds.length,
)} in ${dirLabel}:\n`,
];
if (printIntro) {
printed.unshift(
`\n\n${gray('Run a task:')} gro [name]`,
`\n${gray('View help:')} gro [name] --help`,
);
}
const longestTaskName = toMaxLength(loadModulesResult.modules, (m) => m.name);
for (const meta of loadModulesResult.modules) {
printed.push('\n' + cyan(pad(meta.name, longestTaskName)), ' ', meta.mod.task.summary || '');
}
log[printIntro ? 'info' : 'plain'](printed.join('') + '\n');
} else {
log.info(`No tasks found in ${dirLabel}.`);
}
};

export const logErrorReasons = (log: Logger, reasons: string[]): void => {
for (const reason of reasons) {
log.error(reason);
}
};

const ARGS_PROPERTY_NAME = '[...args]';

// TODO format output in a table
export const logTaskHelp = (log: Logger, meta: TaskModuleMeta): void => {
const {
name,
mod: {task},
} = meta;
const printed: string[] = [];
printed.push(cyan(name), 'help', '\n' + task.summary || '(no summary available)');
if (task.args) {
const properties = toArgProperties(task.args);
const longestTaskName = Math.max(
ARGS_PROPERTY_NAME.length,
toMaxLength(properties, (p) => p.name),
);
const longestType = toMaxLength(properties, (p) => p.schema.type);
const longestDefault = toMaxLength(properties, (p) => p.schema.default + '');
for (const property of properties) {
const name = property.name === '_' ? ARGS_PROPERTY_NAME : property.name;
printed.push(
`\n${green(pad(name, longestTaskName))} `,
gray(pad(property.schema.type, longestType)) + ' ',
pad(printValue(property.schema.default) as string, longestDefault) + ' ',
property.schema.description || '(no description available)',
);
}
}
log.info(...printed);
};

interface ArgSchemaProperty {
name: string;
schema: ArgSchema;
}

const toArgProperties = (schema: ArgsSchema): ArgSchemaProperty[] => {
const properties: ArgSchemaProperty[] = [];
for (const name in schema.properties) {
if ('no-' + name in schema.properties) continue;
properties.push({name, schema: schema.properties[name]});
}
return properties;
};

// quick n dirty padding logic
const pad = (s: string, n: number): string => s + ' '.repeat(n - s.length);
const toMaxLength = <T>(items: T[], toString: (item: T) => string) =>
items.reduce((max, m) => Math.max(toString(m).length, max), 0);
46 changes: 4 additions & 42 deletions src/task/runTask.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {type EventEmitter} from 'events';
import {cyan, gray, green, red} from 'kleur/colors';
import {type Logger, printLogLabel, SystemLogger} from '@feltcoop/felt/util/log.js';
import {printValue} from '@feltcoop/felt/util/print.js';
import {cyan, red} from 'kleur/colors';
import {printLogLabel, SystemLogger} from '@feltcoop/felt/util/log.js';

import {type TaskModuleMeta} from './taskModule.js';
import {TaskError, type Args, type ArgSchema, type ArgsSchema} from './task.js';
import {TaskError, type Args} from './task.js';
import {type invokeTask as InvokeTaskFunction} from './invokeTask.js';
import {type Filesystem} from '../fs/filesystem.js';
import {logTaskHelp} from './logTask.js';

export type RunTaskResult =
| {
Expand Down Expand Up @@ -62,41 +62,3 @@ export const runTask = async (
}
return {ok: true, output};
};

// TODO format output in a table
const logTaskHelp = (log: Logger, meta: TaskModuleMeta) => {
const {
name,
mod: {task},
} = meta;
const strs: string[] = ['help', '\n', cyan(name), '\n', task.summary || '(no summary available)'];
if (!task.args) {
strs.push('\n', '(no args schema available)');
} else {
for (const property of toArgProperties(task.args)) {
const name = property.name === '_' ? '[...args]' : property.name;
strs.push(
'\n',
green(name),
gray(property.schema.type),
printValue(property.schema.default) as string,
property.schema.description || '(no description available)',
);
}
}
log.info(...strs);
};

interface ArgSchemaProperty {
name: string;
schema: ArgSchema;
}

const toArgProperties = (schema: ArgsSchema): ArgSchemaProperty[] => {
const properties: ArgSchemaProperty[] = [];
for (const name in schema.properties) {
if ('no-' + name in schema.properties) continue;
properties.push({name, schema: schema.properties[name]});
}
return properties;
};
1 change: 1 addition & 0 deletions src/task/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class TaskError extends Error {}
// The raw CLI ares are handled by `mri` - https://github.com/lukeed/mri
export interface Args {
_: string[];
help?: boolean;
[key: string]: unknown; // can assign anything to `args` in tasks
}

Expand Down