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

TypeScript definitions for config & lifecycles #311

Merged
merged 1 commit into from
Jan 6, 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
111 changes: 111 additions & 0 deletions docs/typescript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# TypeScript Guide

The Gasket team is dedicated to improving productivity for TypeScript users. Here are various tips to make TypeScript integration go smoothly.

## Ensuring visibility of plugin extensions

Because Gasket itself is just a plugin framework, plugins themselves are responsible for augmenting the core Gasket interfaces. This means that the type declarations for the plugins have to be "discovered" in your TypeScript code base. This can be done by ensuring you reference or import your presets or plugins. The sample code snippets below show these imports, but if you can ensure TypeScript knows about all of the plugins from one centralized area of code then you can avoid duplication.

## Validating gasket.config.js

The `@gasket/engine` package supplies a `GasketConfigFile` type that, once you've also imported all your plugins, validates the contents of your gasket config file.

The `gasket.config.js` file cannot be written in TypeScript, but you can configure TypeScript to check JS files or use a `@ts-check` comment along with JSDoc type annotations. Due to JSDoc type checking limitations, you may have to separate the config declaration and your export:

```javascript
//@ts-check
///<reference types="@gasket/preset-nextjs"/>

/** @type {import('@gasket/engine').GasketConfigFile} */
const config = {
plugins: {
presets: ['@gasket/nextjs']
},
compression: true,
http: 8080,
intl: {
defaultLocale: 'en-GB'
}
};

module.exports = config;
```

## Validating lifecycle scripts

The `@gasket/engine` package supplies a `Hook` type that can be used to validate lifecycle scripts and plugin hooks. It takes a string type parameter for the name of the lifecycle.

```typescript
import { Hook } from '@gasket/engine';
import '@gasket/plugin-intl';

const intlLocaleHandler: Hook<'intlLocale'> = (gasket, locale, { req, res }) => {
// return 3; - does not pass validation
return getLocaleFromHost(req.headers.host);
}
```

Gasket does not currently allow you to author `/lifecycles/*` files in TypeScript. To add type checking to these scripts you must either write them in TypeScript and make sure they get compiled to JavaScript or use JSDoc comments to enable checking. Because of JSDoc limitations with generics syntax, writing in JavaScript requires a workaround using a separate `@typedef` declaration.

```javascript
// @ts-check
///<reference types="@gasket/plugin-intl"/>

/**
* @typedef {import('@gasket/engine').Hook<'intlLocale'>} IntlLocaleHandler
*/

/** @type {IntlLocaleHandler} */
const handler = (gasket, currentLocale, { req, res }) => {
// return 3; - does not pass validation
return getLocaleFromHost(req.headers.host);
};

module.exports = handler;
```

## Authoring plugins

The `@gasket/engine` package exports a `Plugin` type which can be used to validate your plugin definitions. If your plugin is introducing more config properties and lifecycles you should also extend the `GasketConfig` and `HookExecTypes` interfaces. Look for useful helper types in `@gasket/engine` as well.

```typescript
import type {
Plugin, GasketConfig, HookExecTypes, MaybeAsync
} from '@gasket/engine';

// Ensure TypeScript knows about the lifecycles you're hooking
import '@gasket/plugin-express';

const plugin: Plugin = {
name: "my-plugin",
hooks: {
middleware(gasket, express) {
return [
async (req, res, next) => {
const headerValue = await gasket.execWaterfall(
'customHeader',
gasket.config.customHeader);

if (customHeader) {
res.set('x-silly-header', customHeader);
}

next();
}
];
}
}
};

declare module '@gasket/engine' {
export interface GasketConfig {
customHeader?: string
}

export interface HookExecTypes {
customHeader(currentValue: string): MaybeAsync<string>;
}
}

export = plugin;
```
1 change: 1 addition & 0 deletions packages/gasket-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "6.8.1",
"description": "CLI for rapid application development with gasket",
"main": "src/index.js",
"types": "src/index.d.ts",
"bin": {
"gasket": "./bin/boot"
},
Expand Down
211 changes: 211 additions & 0 deletions packages/gasket-cli/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import type { GasketConfigFile } from "@gasket/engine";
import type { PackageManager } from '@gasket/utils';
import type { Config } from "@oclif/config";
import { Inquirer } from "inquirer";

export interface Dependencies {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
}

export interface PackageJson extends Dependencies {
name: string;
version: string;
description?: string;
license?: string;
repository?:
| string
| {
type: "git";
url: string;
};
scripts?: Record<string, string>;
optionalDependencies?: Record<string, string>;
}

export interface ModuleInfo {
name: string;
module: any;
path?: string;
package?: PackageJson;
version?: string;
name?: string;
}

export interface PresetInfo extends ModuleInfo {}

export interface PluginInfo extends ModuleInfo {}

export interface ConfigBuilder<Config> {
/**
* Adds all `[key, value]` pairs in the `fields` provided.
* @param fields - Object to merge. Can be a function that accepts the current fields and object to merge.
* @param source - Plugin to blame if conflicts arise from this operation.
*/
extend(
fields: Partial<Config> | ((current: Config) => Partial<Config>),
source: ModuleInfo
): void;

/**
* Performs an intelligent, domain-aware merge of the `value` for
* the given `key` into the package.json fields associated with this instance.
*
* @param key - Field in package.json to add or extend.
* @param value - Target value to set for key provided.
* @param source - Plugin to blame if conflicts arise from this operation.
* @param [options] - Optional arguments for add behavior
* @param [options.force] - Should the semver version override other attempts
*
* Adapted from @vue/cli under MIT License:
* https://github.com/vuejs/vue-cli/blob/f09722c/packages/%40vue/cli/lib/GeneratorAPI.js#L117-L150
*/
add<Key extends keyof Config>(
key: Key,
value: Config[Key],
source: ModuleInfo,
options?: { force?: boolean }
): void;
}

export interface PackageJsonBuilder extends ConfigBuilder<PackageJson> {
/**
* Checks if a dependency has been already added
* @param key - Dependency bucket
* @param value - Dependency to search
* @returns True if the dependency exists on the bucket
*/
has(key: keyof Dependencies, value: string): boolean;
}

export interface Files {
add(args: { globs: Array<string>, source: ModuleInfo }): void
}

export interface CreateContext {
/** Short name of the app */
appName: string;

/** Current work directory */
cwd: string;

/** Path to the target app (Default: cwd/appName) */
dest: string;

/** Relative path to the target app */
relDest: string;

/** Whether or not target directory already exists */
extant: boolean;

/** paths to the local presets packages */
localPresets: Array<string>;

/** Raw preset desc from args. Can include version constraint. Added by
* load-preset if using localPresets. */
rawPresets: Array<string>;

/** Raw plugin desc from flags, prompts, etc. Can include constraints. */
rawPlugins: Array<string>;

/** Short names of plugins */
plugins: Array<string>;

/** Local packages that should be linked */
pkgLinks: Array<string>;

/** Path to npmconfig file */
npmconfig: string;

/** non-error/warning messages to report */
messages: Array<string>;

/** warnings messages to report */
warnings: Array<string>;

/** error messages to report but do not exit process */
errors: Array<string>;

/** any next steps to report for user */
nextSteps: Array<string>;

/** any generated files to show in report */
generatedFiles: Set<string>;

// Added by `global-prompts`

/** Description of app */
appDescription: string;

/** Should a git repo be initialized and first commit */
gitInit: boolean;

/** Name of the plugin for unit tests */
testPlugin: string;

/** Which package manager to use (Default: 'npm') */
packageManager: string;

/** Derived install command (Default: 'npm install') */
installCmd: string;

/** Derived local run command (Default: 'npx gasket local') */
localCmd: string;

/** Whether or not the user wants to override an extant directory */
destOverride: boolean;

// Added by `load-preset`

/** Short name of presets */
presets: Array<string>;

/** Shallow load of presets with meta data */
presetInfos: Array<PresetInfo>;

// Added by `cli-version`

/** Version of current CLI used to issue `create` command */
cliVersion: string;

/** Version of CLI to install, either current or min compatible version
* required by preset(s) */
cliVersionRequired: string;

// Added by `setup-pkg`

/** package.json builder */
pkg: PackageJsonBuilder;

/** manager to execute npm or yarn commands */
pkgManager: PackageManager;

// Added by `setup-gasket-config`

/** gasket.config builder */
gasketConfig: ConfigBuilder<GasketConfigFile>;

// Added by `create-hooks`

/** Use to add files and templates to generate */
files: Files;
}

declare module "@gasket/engine" {
export interface HookExecTypes {
initOclif(args: { oclifConfig: Config }): MaybeAsync<void>;
prompt(
context: CreateContext,
utils: {
prompt: Inquirer,
addPlugins: (plugins: Array<string>) => Promise<void>
}
): MaybeAsync<CreateContext>;
create(context: CreateContext): MaybeAsync<void>;
postCreate(
context: CreateContext,
utils: { runScript: (script: string) => Promise<void>
}): MaybeAsync<void>;
}
}
Loading