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 args types #142

Merged
merged 3 commits into from
Apr 4, 2021
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
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Gro changelog

## 0.11.2

- add a generic to `Task` to type its `TaskArgs`
([#142](https://github.com/feltcoop/gro/pull/142))

## 0.11.1

- track and clean up child processes in `src/utils/process.ts` helpers
Expand Down
20 changes: 14 additions & 6 deletions src/build.task.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import {pathExists} from './fs/nodeFs.js';
import type {Task} from './task/task.js';
import {createBuild} from './project/build.js';
import type {MapInputOptions, MapOutputOptions, MapWatchOptions} from './project/build.js';
import {getDefaultEsbuildOptions} from './build/esbuildBuildHelpers.js';
import {isThisProjectGro, toBuildOutPath} from './paths.js';
import {Timings} from './utils/time.js';
import {loadGroConfig} from './config/config.js';
import type {GroConfig} from './config/config.js';
import {configureLogLevel} from './utils/log.js';
import type {BuildConfig} from './config/buildConfig.js';

export const task: Task = {
export interface TaskArgs {
watch?: boolean;
mapInputOptions?: MapInputOptions;
mapOutputOptions?: MapOutputOptions;
mapWatchOptions?: MapWatchOptions;
onCreateConfig?: (config: GroConfig) => void;
}

export const task: Task<TaskArgs> = {
description: 'build the project',
dev: false,
run: async ({dev, log, args, invokeTask}): Promise<void> => {
Expand All @@ -25,16 +35,14 @@ export const task: Task = {
if (dev) {
log.warn('building in development mode; normally this is only for diagnostics');
}
const watch: boolean = (args.watch as any) || false;
const mapInputOptions = args.mapInputOptions as any;
const mapOutputOptions = args.mapOutputOptions as any;
const mapWatchOptions = args.mapWatchOptions as any;
const watch = args.watch ?? false;
const {mapInputOptions, mapOutputOptions, mapWatchOptions} = args;

const timingToLoadConfig = timings.start('load config');
const config = await loadGroConfig(dev);
configureLogLevel(config.logLevel);
timingToLoadConfig();
args.oncreateconfig && (args as any).oncreateconfig(config);
if (args.onCreateConfig) args.onCreateConfig(config);

const esbuildOptions = getDefaultEsbuildOptions(config.target, config.sourcemap, dev);

Expand Down
8 changes: 6 additions & 2 deletions src/cert.task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import {pathExists} from './fs/nodeFs.js';
import type {Task} from './task/task.js';
import {spawnProcess} from './utils/process.js';

export const task: Task = {
export interface TaskArgs {
host?: string;
}

export const task: Task<TaskArgs> = {
description: 'creates a self-signed cert for https with openssl',
run: async ({args}) => {
const host = (args.host as string) || 'localhost';
const host = args.host || 'localhost';
const certFile = `${host}-cert.pem`;
const keyFile = `${host}-privkey.pem`;
if (await pathExists(certFile)) throw Error(`File ${certFile} already exists. Aborting.`);
Expand Down
9 changes: 8 additions & 1 deletion src/clean.task.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import type {Task} from './task/task.js';
import {clean} from './project/clean.js';

export const task: Task = {
export interface TaskArgs {
B?: boolean; // !build
D?: boolean; // !dist
s?: boolean; // .svelte
n?: boolean; // node_modules
}

export const task: Task<TaskArgs> = {
description:
'remove files: build/ (unless -B), dist/ (unless -D), and optionally .svelte/ (-s) and node_modules/ (-n)',
run: async ({log, args}): Promise<void> => {
Expand Down
2 changes: 1 addition & 1 deletion src/cli/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ sourcemapSupport.install({

import mri from 'mri';

import type {Args} from './types.js';
import type {Args} from '../task/task.js';
import {invokeTask} from '../task/invokeTask.js';

/*
Expand Down
9 changes: 0 additions & 9 deletions src/cli/types.ts

This file was deleted.

6 changes: 5 additions & 1 deletion src/deploy.task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {copy, pathExists} from './fs/nodeFs.js';
import {paths} from './paths.js';
import {printError, printPath} from './utils/print.js';

export interface TaskArgs {
dry?: boolean;
}

// TODO customize
const distDirName = basename(paths.dist);
const deploymentBranch = 'gh-pages';
Expand All @@ -16,7 +20,7 @@ const TEMP_PREFIX = '__TEMP__';
// TODO support other kinds of deployments
// TODO add a flag to delete the existing deployment branch to avoid bloat (and maybe run `git gc --auto`)

export const task: Task = {
export const task: Task<TaskArgs> = {
description: 'deploy to gh-pages',
run: async ({invokeTask, args, log}): Promise<void> => {
const {dry} = args;
Expand Down
48 changes: 27 additions & 21 deletions src/dev.task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,37 @@ import {printTiming} from './utils/print.js';
import {Timings} from './utils/time.js';
import {createDefaultBuilder} from './build/defaultBuilder.js';
import {paths, toBuildOutPath, SERVER_BUILD_BASE_PATH, isThisProjectGro} from './paths.js';
import {createDevServer} from './server/server.js';
import {createGroServer} from './server/server.js';
import type {GroServer} from './server/server.js';
import {GroConfig, loadGroConfig} from './config/config.js';
import {configureLogLevel} from './utils/log.js';
import type {ServedDirPartial} from './build/ServedDir.js';
import {loadHttpsCredentials} from './server/https.js';
import {createRestartableProcess} from './utils/process.js';
import {hasGroServerConfig, SERVER_BUILD_CONFIG_NAME} from './config/defaultBuildConfig.js';

export const task: Task = {
export interface TaskArgs {
nocert?: boolean;
certfile: string;
certkeyfile: string;
onCreateConfig?: (config: GroConfig) => void;
onCreateFiler?: (file: Filer, config: GroConfig) => void;
onCreateServer?: (server: GroServer) => void;
onInitFiler?: (filer: Filer) => void;
onStartServer?: (server: GroServer) => void;
onReady?: (server: GroServer, filer: Filer, config: GroConfig) => void;
}

export const task: Task<TaskArgs> = {
description: 'start dev server',
run: async ({dev, log, args}) => {
// TODO handle these properly
// args.oncreateconfig
// args.oncreatefiler
// args.oncreateserver
// args.oninitfiler
// args.onstartserver
// args.onready
const timings = new Timings();

const timingToLoadConfig = timings.start('load config');
const config = await loadGroConfig(dev);
configureLogLevel(config.logLevel);
timingToLoadConfig();
args.oncreateconfig && (args as any).oncreateconfig(config);
if (args.onCreateConfig) args.onCreateConfig(config);

const timingToCreateFiler = timings.start('create filer');
const filer = new Filer({
Expand All @@ -40,34 +46,34 @@ export const task: Task = {
sourcemap: config.sourcemap,
});
timingToCreateFiler();
args.oncreatefiler && (args as any).oncreatefiler(filer);
if (args.onCreateFiler) args.onCreateFiler(filer, config);

// TODO restart functionality
const timingToCreateDevServer = timings.start('create dev server');
const timingToCreateGroServer = timings.start('create dev server');
// TODO write docs and validate args, maybe refactor, see also `serve.task.ts`
const https = args.nocert
? null
: await loadHttpsCredentials(log, args.certfile as string, args.certkeyfile as string);
const server = createDevServer({filer, host: config.host, port: config.port, https});
timingToCreateDevServer();
args.oncreateserver && (args as any).oncreateserver(server);
: await loadHttpsCredentials(log, args.certfile, args.certkeyfile);
const server = createGroServer({filer, host: config.host, port: config.port, https});
timingToCreateGroServer();
if (args.onCreateServer) args.onCreateServer(server);

await Promise.all([
(async () => {
const timingToInitFiler = timings.start('init filer');
await filer.init();
timingToInitFiler();
args.oninitfiler && (args as any).oninitfiler(filer);
if (args.onInitFiler) args.onInitFiler(filer);
})(),
(async () => {
const timingToStartDevServer = timings.start('start dev server');
const timingToStartGroServer = timings.start('start dev server');
await server.start();
timingToStartDevServer();
args.onstartserver && (args as any).onstartserver(server);
timingToStartGroServer();
if (args.onStartServer) args.onStartServer(server);
})(),
]);

args.onready && (args as any).onready(filer, server);
if (args.onReady) args.onReady(server, filer, config);

// Support the Gro server pattern by default.
// Normal user projects will hit this code path right here:
Expand Down
8 changes: 6 additions & 2 deletions src/format.task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import {formatDirectory} from './build/formatDirectory.js';
import {paths} from './paths.js';
import {printSpawnResult} from './utils/process.js';

export const task: Task = {
export interface TaskArgs {
check?: boolean;
}

export const task: Task<TaskArgs> = {
description: 'format source files',
run: async ({args}) => {
const check = !!args.check; // TODO args declaration and validation
const check = !!args.check;
const formatResult = await formatDirectory(paths.source, check);
if (!formatResult.ok) {
throw new TaskError(
Expand Down
9 changes: 7 additions & 2 deletions src/gen.task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ import {createStopwatch, Timings} from './utils/time.js';
import {loadModules} from './fs/modules.js';
import {formatFile} from './build/formatFile.js';

export interface TaskArgs {
_: string[];
check?: boolean;
}

// TODO test - especially making sure nothing gets genned
// if there's any validation or import errors
export const task: Task = {
export const task: Task<TaskArgs> = {
description: 'run code generation scripts',
run: async ({log, args}): Promise<void> => {
const rawInputPaths = args._;
const check = !!args.check; // TODO args declaration and validation
const check = !!args.check;

const totalTiming = createStopwatch();
const timings = new Timings();
Expand Down
21 changes: 15 additions & 6 deletions src/serve.task.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import type {Task} from './task/task.js';
import {createDevServer} from './server/server.js';
import {createGroServer} from './server/server.js';
import {Filer} from './build/Filer.js';
import {printPath} from './utils/print.js';
import {loadHttpsCredentials} from './server/https.js';
import {numberFromEnv, stringFromEnv} from './utils/env.js';

export const task: Task = {
export interface TaskArgs {
_: string[];
host?: string;
port?: string;
nocert?: boolean;
certfile?: string;
certkeyfile?: string;
}

export const task: Task<TaskArgs> = {
description: 'start static file server',
run: async ({log, args}): Promise<void> => {
// TODO validate
const host: string | undefined = (args.host as string) || stringFromEnv('HOST');
const port: number | undefined = Number(args.port) || numberFromEnv('PORT');
const host: string | undefined = args.host || stringFromEnv('HOST');
const port: number | undefined = Number(args.port) || numberFromEnv('PORT'); // TODO `numberFromArgs` helper?
const servedDirs: string[] = args._.length ? args._ : ['.'];

// TODO this is inefficient for just serving files in a directory
Expand All @@ -21,9 +30,9 @@ export const task: Task = {
// TODO write docs and validate args, maybe refactor, see also `dev.task.ts`
const https = args.nocert
? null
: await loadHttpsCredentials(log, args.certfile as string, args.certkeyfile as string);
: await loadHttpsCredentials(log, args.certfile, args.certkeyfile);

const server = createDevServer({filer, host, port, https});
const server = createGroServer({filer, host, port, https});
log.info(`serving on ${server.host}:${server.port}`, ...servedDirs.map((d) => printPath(d)));
await server.start();
},
Expand Down
12 changes: 6 additions & 6 deletions src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type Http2StreamHandler = (
flags: number,
) => void;

export interface DevServer {
export interface GroServer {
readonly server: Http1Server | Http2Server;
start(): Promise<void>;
readonly host: string;
Expand Down Expand Up @@ -62,7 +62,7 @@ export const initOptions = (opts: InitialOptions): Options => {
};
};

export const createDevServer = (opts: InitialOptions): DevServer => {
export const createGroServer = (opts: InitialOptions): GroServer => {
// We don't want to have to worry about the security of the dev server.
if (process.env.NODE_ENV === 'production') {
throw Error('The dev server may only be run in development for security reasons.');
Expand All @@ -76,7 +76,7 @@ export const createDevServer = (opts: InitialOptions): DevServer => {
// hacky but w/e - these values are not final until `devServer.start` resolves
finalPort--;
listenOptions.port = finalPort;
(devServer as Assignable<DevServer>).port = finalPort;
(devServer as Assignable<GroServer>).port = finalPort;
};

const listenOptions: ListenOptions = {
Expand Down Expand Up @@ -113,7 +113,7 @@ export const createDevServer = (opts: InitialOptions): DevServer => {

let started = false;

const devServer: DevServer = {
const devServer: GroServer = {
server,
host,
port, // this value is not valid until `start` is complete
Expand Down Expand Up @@ -199,7 +199,7 @@ const to200Headers = async (file: BaseFilerFile): Promise<OutgoingHttpHeaders> =

const toETag = (file: BaseFilerFile): string => `"${getFileContentsHash(file)}"`;

interface DevServerResponse {
interface GroServerResponse {
status: 200 | 304 | 404;
headers: OutgoingHttpHeaders;
data?: string | Buffer | undefined;
Expand All @@ -210,7 +210,7 @@ const toResponse = async (
headers: IncomingHttpHeaders,
filer: Filer,
log: Logger,
): Promise<DevServerResponse> => {
): Promise<GroServerResponse> => {
const url = parseUrl(rawUrl);
const localPath = toLocalPath(url);
log.trace('serving', gray(rawUrl), '→', gray(localPath));
Expand Down
26 changes: 25 additions & 1 deletion src/task/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export const task: Task = {
import type {Task} from '@feltcoop/gro';

export const task: Task = {
dev: false,
dev: false, // tell the task runner to set `dev` to false, updating `process.env.NODE_ENV`
run: async ({dev, invokeTask}) => {
// `dev` is `false` because it's defined two lines up in the task definition,
// unless an ancestor task called `invokeTask` with a `true` value, like this:
Expand All @@ -213,6 +213,30 @@ export const task: Task = {
};
```

### task arg types

The `Task` interface is generic. Its first param is the type of the task context `args`.

Here's the args pattern Gro uses internally:

```ts
// src/some/file.task.ts
import type {Task} from '@feltcoop/gro';

// If needed for uncommon reasons in the task below,
// this can be changed to `export interface TaskArgs extends Args {`
export interface TaskArgs {
onHook?: (thing: any) => void;
}

export const task: Task<TaskArgs> = {
run: async ({args}) => {
// `args` is of type `TaskArgs`
args.onHook({parentTasks: 'canHookIntoThis', byAssigning: 'toArgs'});
},
};
```

## future improvements

- [ ] consider a pattern for declaring and validating CLI args
Expand Down
Loading