From 1ac84a8189a1a6fc3bbefc2e5208d77ed32e8186 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Fri, 4 Feb 2022 13:06:14 -0600 Subject: [PATCH 01/22] add check task args schema --- src/check.schema.ts | 19 +++++++++++++++++++ src/check.task.ts | 17 ++--------------- src/check.ts | 17 +++++++++++++++++ 3 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 src/check.schema.ts create mode 100644 src/check.ts diff --git a/src/check.schema.ts b/src/check.schema.ts new file mode 100644 index 0000000000..0c2323a829 --- /dev/null +++ b/src/check.schema.ts @@ -0,0 +1,19 @@ +export const CheckTaskArgsSchema = { + $id: '/schemas/CheckTaskArgs.json', + type: 'object', + properties: { + _: {type: 'array', items: {type: 'string'}}, + typecheck: {type: 'boolean'}, + 'no-typecheck': {type: 'boolean'}, + test: {type: 'boolean'}, + 'no-test': {type: 'boolean'}, + gen: {type: 'boolean'}, + 'no-gen': {type: 'boolean'}, + format: {type: 'boolean'}, + 'no-format': {type: 'boolean'}, + lint: {type: 'boolean'}, + 'no-lint': {type: 'boolean'}, + }, + required: ['_'], + additionalProperties: false, +}; diff --git a/src/check.task.ts b/src/check.task.ts index d866509f25..d3fe99359f 100644 --- a/src/check.task.ts +++ b/src/check.task.ts @@ -1,22 +1,9 @@ import {type Task} from './task/task.js'; import {TaskError} from './task/task.js'; import {findGenModules} from './gen/genModule.js'; +import {type CheckTaskArgs} from './check.js'; -interface TaskArgs { - _: string[]; - typecheck?: boolean; - 'no-typecheck'?: boolean; - test?: boolean; - 'no-test'?: boolean; - gen?: boolean; - 'no-gen'?: boolean; - format?: boolean; - 'no-format'?: boolean; - lint?: boolean; - 'no-lint'?: boolean; -} - -export const task: Task = { +export const task: Task = { summary: 'check that everything is ready to commit', run: async ({fs, log, args, invokeTask}) => { const { diff --git a/src/check.ts b/src/check.ts new file mode 100644 index 0000000000..cbb2781834 --- /dev/null +++ b/src/check.ts @@ -0,0 +1,17 @@ +// generated by src/check.schema.ts + +export interface CheckTaskArgs { + _: string[]; + typecheck?: boolean; + 'no-typecheck'?: boolean; + test?: boolean; + 'no-test'?: boolean; + gen?: boolean; + 'no-gen'?: boolean; + format?: boolean; + 'no-format'?: boolean; + lint?: boolean; + 'no-lint'?: boolean; +} + +// generated by src/check.schema.ts From f0942b65452bbcfd332b9e7302fbccccd5fa017a Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Fri, 4 Feb 2022 13:17:05 -0600 Subject: [PATCH 02/22] add a todo --- src/check.schema.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/check.schema.ts b/src/check.schema.ts index 0c2323a829..7da4124b7c 100644 --- a/src/check.schema.ts +++ b/src/check.schema.ts @@ -1,3 +1,6 @@ +// TODO what if `.task.` files were used by `gro gen` automatically +// so we could simply export the schema there instead of needing this file? + export const CheckTaskArgsSchema = { $id: '/schemas/CheckTaskArgs.json', type: 'object', From b32b044048b3959c3370936d0eddee88473f0f8f Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Fri, 4 Feb 2022 13:38:30 -0600 Subject: [PATCH 03/22] improve docs --- src/check.task.ts | 2 ++ src/gen/genSchemas.ts | 19 ++++--------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/check.task.ts b/src/check.task.ts index d3fe99359f..3d23d2268d 100644 --- a/src/check.task.ts +++ b/src/check.task.ts @@ -2,9 +2,11 @@ import {type Task} from './task/task.js'; import {TaskError} from './task/task.js'; import {findGenModules} from './gen/genModule.js'; import {type CheckTaskArgs} from './check.js'; +import {CheckTaskArgsSchema} from './check.schema.js'; export const task: Task = { summary: 'check that everything is ready to commit', + args: CheckTaskArgsSchema, run: async ({fs, log, args, invokeTask}) => { const { typecheck = true, diff --git a/src/gen/genSchemas.ts b/src/gen/genSchemas.ts index ec387e3d75..1b914bd0cf 100644 --- a/src/gen/genSchemas.ts +++ b/src/gen/genSchemas.ts @@ -50,30 +50,19 @@ export const runSchemaGen = async ( return {imports, types}; }; -// This is like the ajv `SchemaObject` except that it requires `$id`. -// We may want to loosen this restriction, -// but for now it seems like a convenient way to disambiguate schemas from other objects -// while ensuring they can be registered with ajv and referenced by other schemas. -export interface SchemaObject { - $id: string; - [key: string]: unknown; -} - -const isSchema = (value: unknown): value is SchemaObject => - !!value && typeof value === 'object' && '$id' in value; - // TODO upstream to Felt? /** - * Performs a depth-first traversal of an object, calling `cb` for every key and value. + * Performs a depth-first traversal of an object's enumerable properties, + * calling `cb` for every key and value. * @param obj Any object with enumerable properties. * @param cb Receives the key and value for every enumerable property on `obj` and its descendents. * @returns */ -const traverse = (obj: any, cb: (key: string, value: any) => void): void => { +const traverse = (obj: any, cb: (key: string, value: any, obj: any) => void): void => { if (!obj || typeof obj !== 'object') return; for (const k in obj) { const v = obj[k]; - cb(k, v); + cb(k, v, obj); traverse(v, cb); } }; From 6b33ebad191a898e0d3fbece2602f2c4e60a5e1d Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Fri, 4 Feb 2022 13:43:46 -0600 Subject: [PATCH 04/22] fixes --- src/gen/genSchemas.ts | 1 + src/task/task.ts | 2 ++ src/utils/schema.ts | 11 +++++++++++ 3 files changed, 14 insertions(+) create mode 100644 src/utils/schema.ts diff --git a/src/gen/genSchemas.ts b/src/gen/genSchemas.ts index 1b914bd0cf..883c59143f 100644 --- a/src/gen/genSchemas.ts +++ b/src/gen/genSchemas.ts @@ -5,6 +5,7 @@ import {type GenContext, type RawGenResult} from './gen.js'; import {type SchemaGenModule} from './genModule.js'; import {renderTsHeaderAndFooter} from './helpers/ts.js'; import {normalizeTsImports} from './helpers/tsImport.js'; +import {isSchema} from '../utils/schema.js'; export const genSchemas = async (mod: SchemaGenModule, ctx: GenContext): Promise => { const {imports, types} = await runSchemaGen(ctx, mod); diff --git a/src/task/task.ts b/src/task/task.ts index 7c33b80475..91db20b80c 100644 --- a/src/task/task.ts +++ b/src/task/task.ts @@ -3,9 +3,11 @@ import {type EventEmitter} from 'events'; import {type Logger} from '@feltcoop/felt/util/log.js'; import {type Filesystem} from '../fs/filesystem.js'; +import {type SchemaObject} from '../utils/schema.js'; export interface Task { run: (ctx: TaskContext) => Promise; // TODO return value (make generic, forward it..how?) + args?: SchemaObject; summary?: string; production?: boolean; } diff --git a/src/utils/schema.ts b/src/utils/schema.ts new file mode 100644 index 0000000000..c465ff1d93 --- /dev/null +++ b/src/utils/schema.ts @@ -0,0 +1,11 @@ +// This is like the ajv `SchemaObject` except that it requires `$id`. +// We may want to loosen this restriction, +// but for now it seems like a convenient way to disambiguate schemas from other objects +// while ensuring they can be registered with ajv and referenced by other schemas. +export interface SchemaObject { + $id: string; + [key: string]: unknown; +} + +export const isSchema = (value: unknown): value is SchemaObject => + !!value && typeof value === 'object' && '$id' in value; From 334c1fafd2ec60b35f53ec4583f07ca4606c2bca Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Fri, 4 Feb 2022 19:50:52 -0600 Subject: [PATCH 05/22] print info --- package-lock.json | 1 + package.json | 1 + src/check.schema.ts | 25 +++++------ src/check.task.ts | 2 +- src/gen/fixtures/someTestObject.schema.ts | 4 +- src/gen/genModule.ts | 5 ++- src/gen/genSchemas.ts | 4 +- src/task/runTask.ts | 52 +++++++++++++++++++++-- src/task/task.ts | 20 ++++++++- src/utils/schema.ts | 11 ++--- 10 files changed, 94 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index c191ae8c82..2103d53eae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@rollup/pluginutils": "^4.1.2", "@ryanatkn/json-schema-to-typescript": "^11.1.2", + "@types/json-schema": "^7.0.9", "@types/source-map-support": "^0.5.4", "cheap-watch": "^1.0.4", "dequal": "^2.0.2", diff --git a/package.json b/package.json index abdd7206b8..42040b66ad 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "dependencies": { "@rollup/pluginutils": "^4.1.2", "@ryanatkn/json-schema-to-typescript": "^11.1.2", + "@types/json-schema": "^7.0.9", "@types/source-map-support": "^0.5.4", "cheap-watch": "^1.0.4", "dequal": "^2.0.2", diff --git a/src/check.schema.ts b/src/check.schema.ts index 7da4124b7c..e77b9c9407 100644 --- a/src/check.schema.ts +++ b/src/check.schema.ts @@ -1,21 +1,22 @@ +import type {ArgsSchema} from './task/task.js'; + // TODO what if `.task.` files were used by `gro gen` automatically // so we could simply export the schema there instead of needing this file? -export const CheckTaskArgsSchema = { +export const CheckTaskArgsSchema: ArgsSchema = { $id: '/schemas/CheckTaskArgs.json', type: 'object', properties: { - _: {type: 'array', items: {type: 'string'}}, - typecheck: {type: 'boolean'}, - 'no-typecheck': {type: 'boolean'}, - test: {type: 'boolean'}, - 'no-test': {type: 'boolean'}, - gen: {type: 'boolean'}, - 'no-gen': {type: 'boolean'}, - format: {type: 'boolean'}, - 'no-format': {type: 'boolean'}, - lint: {type: 'boolean'}, - 'no-lint': {type: 'boolean'}, + typecheck: {type: 'boolean', default: true, description: ''}, + 'no-typecheck': {type: 'boolean', default: false, description: 'opt out of typechecking'}, + test: {type: 'boolean', default: true, description: ''}, + 'no-test': {type: 'boolean', default: false, description: 'opt out of running tests'}, + gen: {type: 'boolean', default: true, description: ''}, + 'no-gen': {type: 'boolean', default: false, description: 'opt out of gen check'}, + format: {type: 'boolean', default: true, description: ''}, + 'no-format': {type: 'boolean', default: false, description: 'opt out of format check'}, + lint: {type: 'boolean', default: true, description: ''}, + 'no-lint': {type: 'boolean', default: false, description: 'opt out of linting'}, }, required: ['_'], additionalProperties: false, diff --git a/src/check.task.ts b/src/check.task.ts index 3d23d2268d..47b831a0a9 100644 --- a/src/check.task.ts +++ b/src/check.task.ts @@ -14,7 +14,7 @@ export const task: Task = { gen = true, format = true, lint = true, - ...restArgs + ...restArgs // TODO change this, is unsafe and not explicit } = args; if (typecheck) { diff --git a/src/gen/fixtures/someTestObject.schema.ts b/src/gen/fixtures/someTestObject.schema.ts index 8df2fbc3a4..acb5f29ebd 100644 --- a/src/gen/fixtures/someTestObject.schema.ts +++ b/src/gen/fixtures/someTestObject.schema.ts @@ -1,4 +1,6 @@ -export const SomeTestObjectSchema = { +import {type JSONSchema} from '@ryanatkn/json-schema-to-typescript'; + +export const SomeTestObjectSchema: JSONSchema = { $id: 'https://grocode.org/schemas/SomeTestObject.json', type: 'object', properties: { diff --git a/src/gen/genModule.ts b/src/gen/genModule.ts index 3d6898521e..ba7dcb4a79 100644 --- a/src/gen/genModule.ts +++ b/src/gen/genModule.ts @@ -1,9 +1,10 @@ +import {type JSONSchema} from '@ryanatkn/json-schema-to-typescript'; + import {type ModuleMeta, loadModule, type LoadModuleResult, findModules} from '../fs/modules.js'; import {type Gen, type GenResults, type GenFile} from './gen.js'; import {getPossibleSourceIds} from '../fs/inputPath.js'; import {paths} from '../paths.js'; import {type Filesystem} from '../fs/filesystem.js'; -import {type SchemaObject} from './genSchemas.js'; export const SEPARATOR = '.'; @@ -19,7 +20,7 @@ export interface BasicGenModule { gen: Gen; } export interface SchemaGenModule { - [key: string]: SchemaObject | unknown; + [key: string]: JSONSchema | unknown; } export const toGenModuleType = (filename: string): GenModuleType => diff --git a/src/gen/genSchemas.ts b/src/gen/genSchemas.ts index 883c59143f..4503f8b304 100644 --- a/src/gen/genSchemas.ts +++ b/src/gen/genSchemas.ts @@ -5,7 +5,7 @@ import {type GenContext, type RawGenResult} from './gen.js'; import {type SchemaGenModule} from './genModule.js'; import {renderTsHeaderAndFooter} from './helpers/ts.js'; import {normalizeTsImports} from './helpers/tsImport.js'; -import {isSchema} from '../utils/schema.js'; +import {isVocabSchema} from '../utils/schema.js'; export const genSchemas = async (mod: SchemaGenModule, ctx: GenContext): Promise => { const {imports, types} = await runSchemaGen(ctx, mod); @@ -27,7 +27,7 @@ export const runSchemaGen = async ( for (const identifier in mod) { const value = mod[identifier]; - if (!isSchema(value)) continue; + if (!isVocabSchema(value)) continue; // Compile the schema to TypeScript. const finalIdentifier = stripEnd(identifier, 'Schema'); // convenient to avoid name collisions diff --git a/src/task/runTask.ts b/src/task/runTask.ts index 52857ae886..7dd29f8e64 100644 --- a/src/task/runTask.ts +++ b/src/task/runTask.ts @@ -1,9 +1,10 @@ import {type EventEmitter} from 'events'; -import {cyan, red} from 'kleur/colors'; -import {printLogLabel, SystemLogger} from '@feltcoop/felt/util/log.js'; +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 {type TaskModuleMeta} from './taskModule.js'; -import {TaskError, type Args} from './task.js'; +import {TaskError, type Args, type ArgSchema, type ArgsSchema} from './task.js'; import {type invokeTask as InvokeTaskFunction} from './invokeTask.js'; import {type Filesystem} from '../fs/filesystem.js'; @@ -26,10 +27,15 @@ export const runTask = async ( invokeTask: typeof InvokeTaskFunction, ): Promise => { const {task} = taskMeta.mod; + const log = new SystemLogger(printLogLabel(taskMeta.name)); const dev = process.env.NODE_ENV !== 'production'; // TODO should this use `fromEnv`? '$app/env'? if (dev && task.production) { throw new TaskError(`The task "${taskMeta.name}" cannot be run in development`); } + if (args.help) { + logTaskHelp(log, taskMeta); + return {ok: true, output: null}; + } let output: unknown; try { output = await task.run({ @@ -37,7 +43,7 @@ export const runTask = async ( dev, args, events, - log: new SystemLogger(printLogLabel(taskMeta.name)), + log, invokeTask: (invokedTaskName, invokedArgs = args, invokedEvents = events, invokedFs = fs) => invokeTask(invokedFs, invokedTaskName, invokedArgs, invokedEvents), }); @@ -56,3 +62,41 @@ 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; +}; diff --git a/src/task/task.ts b/src/task/task.ts index 91db20b80c..a316987b8b 100644 --- a/src/task/task.ts +++ b/src/task/task.ts @@ -1,15 +1,15 @@ import type StrictEventEmitter from 'strict-event-emitter-types'; import {type EventEmitter} from 'events'; import {type Logger} from '@feltcoop/felt/util/log.js'; +import {type JSONSchema} from '@ryanatkn/json-schema-to-typescript'; import {type Filesystem} from '../fs/filesystem.js'; -import {type SchemaObject} from '../utils/schema.js'; export interface Task { run: (ctx: TaskContext) => Promise; // TODO return value (make generic, forward it..how?) - args?: SchemaObject; summary?: string; production?: boolean; + args?: ArgsSchema; } export interface TaskContext { @@ -66,3 +66,19 @@ export const serializeArgs = (args: Args): string[] => { } return _ ? [...result, ..._] : result; }; + +export type ArgsProperties = Record & { + _?: {type: 'array'; items: {type: 'string'}; default: []}; +}; + +// TODO should this extend `VocabSchema` so we get `$id`? +export interface ArgsSchema extends JSONSchema { + type: 'object'; + properties: ArgsProperties; +} + +export interface ArgSchema extends JSONSchema { + type: 'boolean' | 'string' | 'number' | 'array'; + // TODO how to use this? + default: boolean | string | number | any[]; +} diff --git a/src/utils/schema.ts b/src/utils/schema.ts index c465ff1d93..dc6fe41a94 100644 --- a/src/utils/schema.ts +++ b/src/utils/schema.ts @@ -1,11 +1,8 @@ -// This is like the ajv `SchemaObject` except that it requires `$id`. -// We may want to loosen this restriction, -// but for now it seems like a convenient way to disambiguate schemas from other objects -// while ensuring they can be registered with ajv and referenced by other schemas. -export interface SchemaObject { +import {type JSONSchema} from '@ryanatkn/json-schema-to-typescript'; + +export interface VocabSchema extends JSONSchema { $id: string; - [key: string]: unknown; } -export const isSchema = (value: unknown): value is SchemaObject => +export const isVocabSchema = (value: unknown): value is VocabSchema => !!value && typeof value === 'object' && '$id' in value; From 54d30fab0033be6e97046cac7fe2600425401650 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Fri, 4 Feb 2022 21:07:35 -0600 Subject: [PATCH 06/22] log task descriptions --- src/task/invokeTask.ts | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/task/invokeTask.ts b/src/task/invokeTask.ts index 0749232de2..528afe0d39 100644 --- a/src/task/invokeTask.ts +++ b/src/task/invokeTask.ts @@ -120,6 +120,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'}, @@ -135,6 +136,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) { @@ -153,10 +155,14 @@ export const invokeTask = async ( // The input path matches a directory. Log the tasks but don't run them. 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, @@ -176,14 +182,18 @@ 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, + ); } } } else if (findModulesResult.type === 'inputDirectoriesWithNoFiles') { @@ -208,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, @@ -230,16 +240,25 @@ export const invokeTask = async ( log.info(`🕒 ${printMs(totalTiming())}`); }; -const logAvailableTasks = ( +const logAvailableTasks = async ( log: Logger, dirLabel: string, sourceIdsByInputPath: Map, -): void => { +): Promise => { const sourceIds = Array.from(sourceIdsByInputPath.values()).flat(); if (sourceIds.length) { + // Load all of the tasks so we can print their decription. + const loadModulesResult = await loadModules(sourceIdsByInputPath, true, loadTaskModule); + if (!loadModulesResult.ok) { + logErrorReasons(log, loadModulesResult.reasons); + process.exit(1); + } log.info(`${sourceIds.length} task${plural(sourceIds.length)} in ${dirLabel}:`); - for (const sourceId of sourceIds) { - log.info('\t' + cyan(toTaskName(sourceIdToBasePath(sourceId, pathsFromId(sourceId))))); + for (const mod of loadModulesResult.modules) { + log.info( + ' ' + cyan(toTaskName(sourceIdToBasePath(mod.id, pathsFromId(mod.id)))), + mod.mod.task.summary || '', + ); } } else { log.info(`No tasks found in ${dirLabel}.`); From a4a29e7526fdbafc84a0aa09fd7fb5022ec85544 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Fri, 4 Feb 2022 22:00:03 -0600 Subject: [PATCH 07/22] better logging --- src/check.ts | 16 +++++++++- src/task/invokeTask.ts | 44 +++++--------------------- src/task/logTask.ts | 71 ++++++++++++++++++++++++++++++++++++++++++ src/task/runTask.ts | 48 +++------------------------- src/task/task.ts | 15 +++++++++ 5 files changed, 114 insertions(+), 80 deletions(-) create mode 100644 src/task/logTask.ts diff --git a/src/check.ts b/src/check.ts index cbb2781834..d86d0990a5 100644 --- a/src/check.ts +++ b/src/check.ts @@ -1,16 +1,30 @@ // generated by src/check.schema.ts export interface CheckTaskArgs { - _: string[]; typecheck?: boolean; + /** + * opt out of typechecking + */ 'no-typecheck'?: boolean; test?: boolean; + /** + * opt out of running tests + */ 'no-test'?: boolean; gen?: boolean; + /** + * opt out of gen check + */ 'no-gen'?: boolean; format?: boolean; + /** + * opt out of format check + */ 'no-format'?: boolean; lint?: boolean; + /** + * opt out of linting + */ 'no-lint'?: boolean; } diff --git a/src/task/invokeTask.ts b/src/task/invokeTask.ts index 528afe0d39..df60c61e44 100644 --- a/src/task/invokeTask.ts +++ b/src/task/invokeTask.ts @@ -1,21 +1,18 @@ import {cyan, red, gray} from 'kleur/colors'; -import {SystemLogger, 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, @@ -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'; /* @@ -159,6 +157,7 @@ export const invokeTask = async ( log, printPath(pathData.id), findModulesResult.sourceIdsByInputPath, + args, ); } else if (isGroId(pathData.id)) { // Does the Gro directory contain the matching files? Log them. @@ -166,6 +165,7 @@ export const invokeTask = async ( log, printPathOrGroPath(pathData.id), findModulesResult.sourceIdsByInputPath, + args, ); } else { // The Gro directory is not the same as the cwd @@ -186,6 +186,7 @@ export const invokeTask = async ( log, printPathOrGroPath(groPathData.id), groDirFindModulesResult.sourceIdsByInputPath, + args, ); } // Then log the current working directory matches. @@ -193,6 +194,7 @@ export const invokeTask = async ( log, printPath(pathData.id), findModulesResult.sourceIdsByInputPath, + args, ); } } @@ -222,6 +224,7 @@ export const invokeTask = async ( log, printPathOrGroPath(groPathData.id), groDirFindModulesResult.sourceIdsByInputPath, + args, ); } else { // Log the original errors, not the Gro-specific ones. @@ -240,37 +243,6 @@ export const invokeTask = async ( log.info(`🕒 ${printMs(totalTiming())}`); }; -const logAvailableTasks = async ( - log: Logger, - dirLabel: string, - sourceIdsByInputPath: Map, -): Promise => { - const sourceIds = Array.from(sourceIdsByInputPath.values()).flat(); - if (sourceIds.length) { - // Load all of the tasks so we can print their decription. - const loadModulesResult = await loadModules(sourceIdsByInputPath, true, loadTaskModule); - if (!loadModulesResult.ok) { - logErrorReasons(log, loadModulesResult.reasons); - process.exit(1); - } - log.info(`${sourceIds.length} task${plural(sourceIds.length)} in ${dirLabel}:`); - for (const mod of loadModulesResult.modules) { - log.info( - ' ' + cyan(toTaskName(sourceIdToBasePath(mod.id, pathsFromId(mod.id)))), - mod.mod.task.summary || '', - ); - } - } 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. diff --git a/src/task/logTask.ts b/src/task/logTask.ts new file mode 100644 index 0000000000..2be3228eba --- /dev/null +++ b/src/task/logTask.ts @@ -0,0 +1,71 @@ +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 {toArgProperties, type Args} 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, + args: Args, +): Promise => { + 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[] = [ + `${sourceIds.length} task${plural(sourceIds.length)} in ${dirLabel}:${args.help ? '' : '\n'}`, + ]; + for (const meta of loadModulesResult.modules) { + if (args.help) { + printed.push('\n\n' + printTaskHelp(meta).join('')); + } else { + printed.push('\n' + cyan(meta.name), ' ', meta.mod.task.summary || ''); + } + } + log.info(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); + } +}; + +// TODO format output in a table +export const printTaskHelp = (meta: TaskModuleMeta): string[] => { + const { + name, + mod: {task}, + } = meta; + const printed: string[] = [cyan(name), '\n', task.summary || '(no summary available)']; + if (!task.args) { + printed.push('\n', '(no args schema available)'); + } else { + for (const property of toArgProperties(task.args)) { + const name = property.name === '_' ? '[...args]' : property.name; + printed.push( + '\n', + green(name), + ' ', + gray(property.schema.type), + ' ', + printValue(property.schema.default) as string, + ' ', + property.schema.description || '(no description available)', + ); + } + } + return printed; +}; diff --git a/src/task/runTask.ts b/src/task/runTask.ts index 7dd29f8e64..be24ed8e9a 100644 --- a/src/task/runTask.ts +++ b/src/task/runTask.ts @@ -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 {printTaskHelp} from './logTask.js'; export type RunTaskResult = | { @@ -33,7 +33,7 @@ export const runTask = async ( throw new TaskError(`The task "${taskMeta.name}" cannot be run in development`); } if (args.help) { - logTaskHelp(log, taskMeta); + log.info('help:', '\n\n', ...printTaskHelp(taskMeta), '\n'); return {ok: true, output: null}; } let output: unknown; @@ -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; -}; diff --git a/src/task/task.ts b/src/task/task.ts index a316987b8b..1bf28b82f4 100644 --- a/src/task/task.ts +++ b/src/task/task.ts @@ -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 } @@ -82,3 +83,17 @@ export interface ArgSchema extends JSONSchema { // TODO how to use this? default: boolean | string | number | any[]; } + +interface ArgSchemaProperty { + name: string; + schema: ArgSchema; +} + +export 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; +}; From 2c7d77928df36646dd8b3aaa7f3392e127450f26 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Fri, 4 Feb 2022 22:18:22 -0600 Subject: [PATCH 08/22] add help task --- src/docs/tasks.md | 1 + src/help.task.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 src/help.task.ts diff --git a/src/docs/tasks.md b/src/docs/tasks.md index cd812071f9..1c455d463e 100644 --- a/src/docs/tasks.md +++ b/src/docs/tasks.md @@ -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 --help` - [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 diff --git a/src/help.task.ts b/src/help.task.ts new file mode 100644 index 0000000000..c4f79c4624 --- /dev/null +++ b/src/help.task.ts @@ -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 --help`', + run: async (): Promise => { + await spawn('npx', ['gro', '--help']); + }, +}; From 08fff0c2487224eea0dadf08d684443aba7941b0 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Fri, 4 Feb 2022 22:22:29 -0600 Subject: [PATCH 09/22] improve docs --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b816073f5e..8246075589 100644 --- a/README.md +++ b/README.md @@ -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 ``` Gro has a number of builtin tasks that you can run with the CLI. From aac9fdf5b5552ef82a0e35882ea9d47fae2ae89b Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Fri, 4 Feb 2022 22:22:47 -0600 Subject: [PATCH 10/22] tweak docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8246075589..df7623d810 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ 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 +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. From 5d37f4b03ef699ffaef856735ac39eb959d06b7d Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Fri, 4 Feb 2022 22:23:58 -0600 Subject: [PATCH 11/22] remove json-schema dep --- package-lock.json | 1 - package.json | 1 - 2 files changed, 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2103d53eae..c191ae8c82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@rollup/pluginutils": "^4.1.2", "@ryanatkn/json-schema-to-typescript": "^11.1.2", - "@types/json-schema": "^7.0.9", "@types/source-map-support": "^0.5.4", "cheap-watch": "^1.0.4", "dequal": "^2.0.2", diff --git a/package.json b/package.json index 42040b66ad..abdd7206b8 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "dependencies": { "@rollup/pluginutils": "^4.1.2", "@ryanatkn/json-schema-to-typescript": "^11.1.2", - "@types/json-schema": "^7.0.9", "@types/source-map-support": "^0.5.4", "cheap-watch": "^1.0.4", "dequal": "^2.0.2", From d438e777ba5d54be040bdc9472e4fb5faddb29d0 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Sat, 5 Feb 2022 01:25:20 -0600 Subject: [PATCH 12/22] changelog --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index d370fdc565..9da4cb6745 100644 --- a/changelog.md +++ b/changelog.md @@ -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` From d03c3a79f9113102c65189400ceffbbb2f53cffe Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Sat, 5 Feb 2022 01:29:01 -0600 Subject: [PATCH 13/22] fix merge --- src/task/logTask.ts | 16 ++++++++++++++- src/task/runTask.ts | 48 +++++---------------------------------------- 2 files changed, 20 insertions(+), 44 deletions(-) diff --git a/src/task/logTask.ts b/src/task/logTask.ts index 2be3228eba..e5a5d89c24 100644 --- a/src/task/logTask.ts +++ b/src/task/logTask.ts @@ -3,7 +3,7 @@ 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 {toArgProperties, type Args} from './task.js'; +import {type Args, type ArgSchema, type ArgsSchema} from './task.js'; import {loadModules} from '../fs/modules.js'; import {loadTaskModule, type TaskModuleMeta} from './taskModule.js'; @@ -69,3 +69,17 @@ export const printTaskHelp = (meta: TaskModuleMeta): string[] => { } return 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; +}; diff --git a/src/task/runTask.ts b/src/task/runTask.ts index 7dd29f8e64..f5ff344056 100644 --- a/src/task/runTask.ts +++ b/src/task/runTask.ts @@ -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 {printTaskHelp} from './logTask.js'; export type RunTaskResult = | { @@ -33,7 +33,7 @@ export const runTask = async ( throw new TaskError(`The task "${taskMeta.name}" cannot be run in development`); } if (args.help) { - logTaskHelp(log, taskMeta); + log.info(...printTaskHelp(taskMeta)); return {ok: true, output: null}; } let output: unknown; @@ -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; -}; From f247d5baeba198689d1b35461cabf439c8fb8403 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Sat, 5 Feb 2022 01:41:26 -0600 Subject: [PATCH 14/22] corerce string --- src/task/logTask.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/task/logTask.ts b/src/task/logTask.ts index e5a5d89c24..0c465b7cc5 100644 --- a/src/task/logTask.ts +++ b/src/task/logTask.ts @@ -61,7 +61,7 @@ export const printTaskHelp = (meta: TaskModuleMeta): string[] => { ' ', gray(property.schema.type), ' ', - printValue(property.schema.default) as string, + printValue(property.schema.default) + '', ' ', property.schema.description || '(no description available)', ); From 34c5b89067d88272e8e5246ee798c1a6a20925ee Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Sat, 5 Feb 2022 01:41:55 -0600 Subject: [PATCH 15/22] dont coerce so itll be linted when fixed --- src/task/logTask.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/task/logTask.ts b/src/task/logTask.ts index 0c465b7cc5..e5a5d89c24 100644 --- a/src/task/logTask.ts +++ b/src/task/logTask.ts @@ -61,7 +61,7 @@ export const printTaskHelp = (meta: TaskModuleMeta): string[] => { ' ', gray(property.schema.type), ' ', - printValue(property.schema.default) + '', + printValue(property.schema.default) as string, ' ', property.schema.description || '(no description available)', ); From f9760df5b785e169a1510237e25de5c12d20d998 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Mon, 7 Feb 2022 10:01:05 -0600 Subject: [PATCH 16/22] log nothing for args if no schema --- src/task/logTask.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/task/logTask.ts b/src/task/logTask.ts index e5a5d89c24..27bd582aa8 100644 --- a/src/task/logTask.ts +++ b/src/task/logTask.ts @@ -50,9 +50,7 @@ export const printTaskHelp = (meta: TaskModuleMeta): string[] => { mod: {task}, } = meta; const printed: string[] = [cyan(name), '\n', task.summary || '(no summary available)']; - if (!task.args) { - printed.push('\n', '(no args schema available)'); - } else { + if (task.args) { for (const property of toArgProperties(task.args)) { const name = property.name === '_' ? '[...args]' : property.name; printed.push( From 0ed0cc8183b44e24e0799121769ec88d4e7038a6 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Mon, 7 Feb 2022 18:52:50 -0600 Subject: [PATCH 17/22] rework help --- src/docs/tasks.md | 2 +- src/help.task.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/docs/tasks.md b/src/docs/tasks.md index 1c455d463e..a65098fad9 100644 --- a/src/docs/tasks.md +++ b/src/docs/tasks.md @@ -14,7 +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 --help` +- [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 diff --git a/src/help.task.ts b/src/help.task.ts index c4f79c4624..e129426dc2 100644 --- a/src/help.task.ts +++ b/src/help.task.ts @@ -3,8 +3,8 @@ import {spawn} from '@feltcoop/felt/util/process.js'; import {type Task} from './task/task.js'; export const task: Task = { - summary: 'alias for `gro --help`', + summary: 'alias for `gro` with no task name provided', run: async (): Promise => { - await spawn('npx', ['gro', '--help']); + await spawn('npx', ['gro']); }, }; From 34a66f1265fe5ecfaf43bb830797329f974e5e34 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Mon, 7 Feb 2022 19:04:16 -0600 Subject: [PATCH 18/22] fix help printing --- src/task/logTask.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/task/logTask.ts b/src/task/logTask.ts index 27bd582aa8..c34e1962e3 100644 --- a/src/task/logTask.ts +++ b/src/task/logTask.ts @@ -22,14 +22,14 @@ export const logAvailableTasks = async ( process.exit(1); } const printed: string[] = [ - `${sourceIds.length} task${plural(sourceIds.length)} in ${dirLabel}:${args.help ? '' : '\n'}`, + `\n\n${gray('Run a task:')} gro [name]`, + `\n${gray('View help:')} gro [name] --help`, + `\n\n${sourceIds.length} task${plural(sourceIds.length)} in ${dirLabel}:${ + args.help ? '' : '\n' + }`, ]; for (const meta of loadModulesResult.modules) { - if (args.help) { - printed.push('\n\n' + printTaskHelp(meta).join('')); - } else { - printed.push('\n' + cyan(meta.name), ' ', meta.mod.task.summary || ''); - } + printed.push('\n' + cyan(meta.name), ' ', meta.mod.task.summary || ''); } log.info(printed.join('') + '\n'); } else { From f86b759b208a86e3216eb57230cb504f45f1bb3e Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Mon, 7 Feb 2022 19:19:18 -0600 Subject: [PATCH 19/22] add a space --- src/task/logTask.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/task/logTask.ts b/src/task/logTask.ts index c34e1962e3..709881c7d6 100644 --- a/src/task/logTask.ts +++ b/src/task/logTask.ts @@ -24,9 +24,7 @@ export const logAvailableTasks = async ( const printed: string[] = [ `\n\n${gray('Run a task:')} gro [name]`, `\n${gray('View help:')} gro [name] --help`, - `\n\n${sourceIds.length} task${plural(sourceIds.length)} in ${dirLabel}:${ - args.help ? '' : '\n' - }`, + `\n\n${sourceIds.length} task${plural(sourceIds.length)} in ${dirLabel}:\n`, ]; for (const meta of loadModulesResult.modules) { printed.push('\n' + cyan(meta.name), ' ', meta.mod.task.summary || ''); From e2739e1cba5b42916e5a28ea0b749b2c0cb9d288 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Mon, 7 Feb 2022 19:35:02 -0600 Subject: [PATCH 20/22] add formatting --- src/task/invokeTask.ts | 5 ----- src/task/logTask.ts | 33 +++++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/task/invokeTask.ts b/src/task/invokeTask.ts index 816cca20c5..d5829634d4 100644 --- a/src/task/invokeTask.ts +++ b/src/task/invokeTask.ts @@ -158,7 +158,6 @@ export const invokeTask = async ( log, printPath(pathData.id), findModulesResult.sourceIdsByInputPath, - args, ); } else if (isGroId(pathData.id)) { // Does the Gro directory contain the matching files? Log them. @@ -166,7 +165,6 @@ export const invokeTask = async ( log, printPathOrGroPath(pathData.id), findModulesResult.sourceIdsByInputPath, - args, ); } else { // The Gro directory is not the same as the cwd @@ -187,7 +185,6 @@ export const invokeTask = async ( log, printPathOrGroPath(groPathData.id), groDirFindModulesResult.sourceIdsByInputPath, - args, ); } // Then log the current working directory matches. @@ -195,7 +192,6 @@ export const invokeTask = async ( log, printPath(pathData.id), findModulesResult.sourceIdsByInputPath, - args, ); } } @@ -225,7 +221,6 @@ export const invokeTask = async ( log, printPathOrGroPath(groPathData.id), groDirFindModulesResult.sourceIdsByInputPath, - args, ); } else { // Log the original errors, not the Gro-specific ones. diff --git a/src/task/logTask.ts b/src/task/logTask.ts index 709881c7d6..4663b6dcba 100644 --- a/src/task/logTask.ts +++ b/src/task/logTask.ts @@ -3,7 +3,7 @@ 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 Args, type ArgSchema, type ArgsSchema} from './task.js'; +import {type ArgSchema, type ArgsSchema} from './task.js'; import {loadModules} from '../fs/modules.js'; import {loadTaskModule, type TaskModuleMeta} from './taskModule.js'; @@ -11,7 +11,6 @@ export const logAvailableTasks = async ( log: Logger, dirLabel: string, sourceIdsByInputPath: Map, - args: Args, ): Promise => { const sourceIds = Array.from(sourceIdsByInputPath.values()).flat(); if (sourceIds.length) { @@ -26,8 +25,9 @@ export const logAvailableTasks = async ( `\n${gray('View help:')} gro [name] --help`, `\n\n${sourceIds.length} task${plural(sourceIds.length)} in ${dirLabel}:\n`, ]; + const longestTaskName = toMaxLength(loadModulesResult.modules, (m) => m.name); for (const meta of loadModulesResult.modules) { - printed.push('\n' + cyan(meta.name), ' ', meta.mod.task.summary || ''); + printed.push('\n' + cyan(pad(meta.name, longestTaskName)), ' ', meta.mod.task.summary || ''); } log.info(printed.join('') + '\n'); } else { @@ -41,23 +41,31 @@ export const logErrorReasons = (log: Logger, reasons: string[]): void => { } }; +const ARGS_PROPERTY_NAME = '[...args]'; + // TODO format output in a table export const printTaskHelp = (meta: TaskModuleMeta): string[] => { const { name, mod: {task}, } = meta; - const printed: string[] = [cyan(name), '\n', task.summary || '(no summary available)']; + const printed: string[] = [cyan(name), '\n' + task.summary || '(no summary available)']; if (task.args) { - for (const property of toArgProperties(task.args)) { - const name = property.name === '_' ? '[...args]' : property.name; + 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(name), + `\n${green(pad(name, longestTaskName))}`, ' ', - gray(property.schema.type), + gray(pad(property.schema.type, longestType)), ' ', - printValue(property.schema.default) as string, + pad(printValue(property.schema.default) as string, longestDefault), ' ', property.schema.description || '(no description available)', ); @@ -79,3 +87,8 @@ const toArgProperties = (schema: ArgsSchema): ArgSchemaProperty[] => { } return properties; }; + +// quick n dirty padding logic +const pad = (s: string, n: number): string => s + ' '.repeat(n - s.length); +const toMaxLength = (items: T[], toString: (item: T) => string) => + items.reduce((max, m) => Math.max(toString(m).length, max), 0); From b087d3d8620291b076c519a1dd1a8ed4a55833e3 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Mon, 7 Feb 2022 19:59:08 -0600 Subject: [PATCH 21/22] improve formatting --- src/task/invokeTask.ts | 1 + src/task/logTask.ts | 31 ++++++++++++++++++------------- src/task/runTask.ts | 4 ++-- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/task/invokeTask.ts b/src/task/invokeTask.ts index d5829634d4..e2bd92a2b8 100644 --- a/src/task/invokeTask.ts +++ b/src/task/invokeTask.ts @@ -192,6 +192,7 @@ export const invokeTask = async ( log, printPath(pathData.id), findModulesResult.sourceIdsByInputPath, + !groDirFindModulesResult.ok, ); } } diff --git a/src/task/logTask.ts b/src/task/logTask.ts index 4663b6dcba..4629c656c0 100644 --- a/src/task/logTask.ts +++ b/src/task/logTask.ts @@ -11,6 +11,7 @@ export const logAvailableTasks = async ( log: Logger, dirLabel: string, sourceIdsByInputPath: Map, + printIntro = true, ): Promise => { const sourceIds = Array.from(sourceIdsByInputPath.values()).flat(); if (sourceIds.length) { @@ -21,15 +22,21 @@ export const logAvailableTasks = async ( process.exit(1); } const printed: string[] = [ - `\n\n${gray('Run a task:')} gro [name]`, - `\n${gray('View help:')} gro [name] --help`, - `\n\n${sourceIds.length} task${plural(sourceIds.length)} in ${dirLabel}:\n`, + `${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.info(printed.join('') + '\n'); + log[printIntro ? 'info' : 'plain'](printed.join('') + '\n'); } else { log.info(`No tasks found in ${dirLabel}.`); } @@ -44,12 +51,13 @@ export const logErrorReasons = (log: Logger, reasons: string[]): void => { const ARGS_PROPERTY_NAME = '[...args]'; // TODO format output in a table -export const printTaskHelp = (meta: TaskModuleMeta): string[] => { +export const logTaskHelp = (log: Logger, meta: TaskModuleMeta): void => { const { name, mod: {task}, } = meta; - const printed: string[] = [cyan(name), '\n' + task.summary || '(no summary available)']; + const printed: string[] = []; + printed.push(cyan(name), '\n' + task.summary || '(no summary available)'); if (task.args) { const properties = toArgProperties(task.args); const longestTaskName = Math.max( @@ -61,17 +69,14 @@ export const printTaskHelp = (meta: TaskModuleMeta): string[] => { 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), - ' ', + `\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)', ); } } - return printed; + log.info(...printed); }; interface ArgSchemaProperty { diff --git a/src/task/runTask.ts b/src/task/runTask.ts index f5ff344056..76a891e564 100644 --- a/src/task/runTask.ts +++ b/src/task/runTask.ts @@ -6,7 +6,7 @@ import {type TaskModuleMeta} from './taskModule.js'; import {TaskError, type Args} from './task.js'; import {type invokeTask as InvokeTaskFunction} from './invokeTask.js'; import {type Filesystem} from '../fs/filesystem.js'; -import {printTaskHelp} from './logTask.js'; +import {logTaskHelp} from './logTask.js'; export type RunTaskResult = | { @@ -33,7 +33,7 @@ export const runTask = async ( throw new TaskError(`The task "${taskMeta.name}" cannot be run in development`); } if (args.help) { - log.info(...printTaskHelp(taskMeta)); + logTaskHelp(log, taskMeta); return {ok: true, output: null}; } let output: unknown; From 4599292846d07217e708d4a1bdaf4e95d02cb82a Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Mon, 7 Feb 2022 20:13:15 -0600 Subject: [PATCH 22/22] add help text --- src/task/logTask.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/task/logTask.ts b/src/task/logTask.ts index 4629c656c0..40a6dcd17b 100644 --- a/src/task/logTask.ts +++ b/src/task/logTask.ts @@ -57,7 +57,7 @@ export const logTaskHelp = (log: Logger, meta: TaskModuleMeta): void => { mod: {task}, } = meta; const printed: string[] = []; - printed.push(cyan(name), '\n' + task.summary || '(no summary available)'); + printed.push(cyan(name), 'help', '\n' + task.summary || '(no summary available)'); if (task.args) { const properties = toArgProperties(task.args); const longestTaskName = Math.max(