Skip to content

Commit

Permalink
feat(eslint-config): add initial typescript support (#30)
Browse files Browse the repository at this point in the history
Signed-off-by: Dirk de Visser <[email protected]>
  • Loading branch information
dirkdev98 authored Mar 15, 2024
1 parent e20da7f commit 4670394
Show file tree
Hide file tree
Showing 19 changed files with 562 additions and 84 deletions.
8 changes: 4 additions & 4 deletions apps/backend/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import assert from "node:assert/strict";
import { test } from "node:test";
import { buildApp } from "./app.js";

test("app", async (t) => {
let app = await buildApp();
void test("app", async (t) => {
const app = await buildApp();

t.after(() => {
return app.close();
});

await t.test("foo", async (t) => {
await t.test("foo", async () => {
const response = await app.inject({
method: "GET",
path: "/",
Expand All @@ -19,7 +19,7 @@ test("app", async (t) => {
assert.deepEqual(response.json(), { hello: "Worlds" });
});

await t.test("validation", async (t) => {
await t.test("validation", async () => {
const response = await app.inject({
method: "POST",
path: "/test-validation",
Expand Down
23 changes: 9 additions & 14 deletions apps/backend/app.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { Multipart } from "@fastify/multipart";
import fastify, { FastifyBaseLogger, FastifyHttpOptions, FastifyRequest } from "fastify";
import {
serializerCompiler,
validatorCompiler,
ZodTypeProvider,
} from "fastify-type-provider-zod";
import fastify, { FastifyHttpOptions, FastifyRequest } from "fastify";
import { ZodTypeProvider } from "fastify-type-provider-zod";
import * as http from "node:http";
import { z } from "zod";
import { basePlugin } from "./plugins/base.js";

export async function buildApp(opts: FastifyHttpOptions<http.Server> = {}) {
const app = fastify<
http.Server,
http.IncomingMessage,
http.ServerResponse,
FastifyBaseLogger
>(opts).withTypeProvider<ZodTypeProvider>();
const app = fastify<http.Server, http.IncomingMessage, http.ServerResponse>(
opts,
).withTypeProvider<ZodTypeProvider>();

app.register(basePlugin);
await app.register(basePlugin);

app.route({
method: "post",
Expand All @@ -36,10 +29,12 @@ export async function buildApp(opts: FastifyHttpOptions<http.Server> = {}) {
return reply.send(
Object.keys(request.body).map((key) => ({
key,

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
value:
request.body[key]?.type === "file" ?
request.body[key]?.mimetype
: // @ts-expect-error
: // @ts-expect-error TODO: not sure why yet.
request.body[key]?.value,
})),
);
Expand Down
10 changes: 5 additions & 5 deletions apps/backend/plugins/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { test } from "node:test";

import { buildApp } from "../app.js";

test("base", async (t) => {
let app = await buildApp();
void test("base", async (t) => {
const app = await buildApp();
const server = await app.listen({
port: 0,
});
Expand All @@ -14,7 +14,7 @@ test("base", async (t) => {
return app.close();
});

await t.test("multipart", async (t) => {
await t.test("multipart", async () => {
const form = new FormData();
form.append("foo", "bar");
form.append("file1", await fs.openAsBlob("package.json"), "package.json");
Expand All @@ -40,7 +40,7 @@ test("base", async (t) => {
});

await t.test("health check", async (t) => {
await t.test("success", async (t) => {
await t.test("success", async () => {
const response = await app.inject({
method: "get",
path: "/health",
Expand All @@ -50,7 +50,7 @@ test("base", async (t) => {
assert.equal(response.json().healthChecks.label, "HEALTHY");
});

await t.test("fail", async (t) => {
await t.test("fail", async () => {
app.addHealthCheck("label2", () => false, { value: true });
const response = await app.inject({
method: "get",
Expand Down
8 changes: 4 additions & 4 deletions apps/backend/plugins/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async function base(
// TODO: should only be enabled for specific plugin contexts. So we may want to expose a
// function with these defaults at some point?

app.register(fastifyMultipart, {
await app.register(fastifyMultipart, {
limits: {
// Max field name size in bytes
fieldNameSize: 100,
Expand Down Expand Up @@ -57,7 +57,7 @@ async function base(
...options.multipart,
});

app.register(fastifyHelmet, {
await app.register(fastifyHelmet, {
global: true,
contentSecurityPolicy: {
// See https://infosec.mozilla.org/guidelines/web_security#content-security-policy:~:text=recommended%20for%20APIs%20to%20use
Expand All @@ -72,8 +72,8 @@ async function base(
xDownloadOptions: false,
});

// TODO: Why do we need `as any` here?
app.register(fastifyCustomHealthCheck as any, {
// @ts-expect-error TODO: Why do we need this here?
await app.register(fastifyCustomHealthCheck, {
// TODO: we should allow configuring one or multiple routes

path: "/health",
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ const closeGracefully = closeWithGrace({ delay: 500 }, async (options) => {
await app.close();
});

app.addHook("onClose", (instance, done) => {
app.addHook("onClose", (_, done) => {
closeGracefully.uninstall();
return done();
done();
});

const port = process.env.PORT ? Number(process.env.PORT) : 3001;
Expand Down
3 changes: 2 additions & 1 deletion apps/backend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"extends": "@lightbase/tsconfig/node-package.json",
"compilerOptions": {
"outDir": "dist"
}
},
"include": ["**/*"]
}
5 changes: 2 additions & 3 deletions apps/backend/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { FastifyBaseLogger, FastifyInstance } from "fastify";
import type { FastifyInstance } from "fastify";
import http from "node:http";

export type FastifyBase = FastifyInstance<
http.Server,
http.IncomingMessage,
http.ServerResponse,
FastifyBaseLogger
http.ServerResponse
>;
93 changes: 74 additions & 19 deletions packages/eslint-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The following dependencies are automatically installed as part of `peerDependenc
however custom versions can be installed via

```shell
npm install --save-dev --exact eslint prettier
npm install --save-dev --exact eslint typescript-eslint
```

Some configurations require manually installed plugins.
Expand Down Expand Up @@ -58,11 +58,23 @@ alpha.

## Default configuration and options

### Markdown
### Custom configuration

`defineConfig` accepts custom ESLint configuration as the 'rest' parameter. For example:

A Markdown parser is installed by default. One of its purposes is to extract code-blocks
and present them as virtual files. Other configurations will automatically handle these
since they glob on `**/`.
```js
import { defineConfig } from "@lightbase/eslint-config";

export default defineConfig(
{
// Define config options, explained below.
},
{
// Ignore the packages/ directory.
ignores: ["packages/**"],
},
);
```

### Prettier

Expand All @@ -87,20 +99,64 @@ export default defineConfig({
});
```

None of these options are required.
### Typescript

## Custom configuration
[Typescript ESLint](http://typescript-eslint.io/) is automatically enabled if either
`tsconfig.eslint.json` or `tsconfig.json` is present, preferring to use the former.

`defineConfig` accepts custom ESLint configuration as the 'rest' parameter. For example:
```js
import { defineConfig } from "@lightbase/eslint-config";

export default defineConfig(
{},
{
// Apply custom rules
files: ["**/*.ts"],
rules: {
"@typescript-eslint/no-unused-vars": "off",
},
},
);
```

Providing a custom tsconfig location is possible as well:

```js
import { defineConfig } from "@lightbase/eslint-config";

export default defineConfig({
typescript: {
project: "./tsconfig.test.json",
},
});
```

Or explicitly disabling Typescript support

```js
import { defineConfig } from "@lightbase/eslint-config";

export default defineConfig({
typescript: false,
});
```

### Markdown

A Markdown processor is installed by default. Its purpose is to extract code-blocks and
present them as virtual files. This means that markdown code-blocks can receive custom
rules as follows:

```js
import { defineConfig } from "@lightbase/eslint-config";

export default defineConfig(
{},
{
// Ignore the packages/ directory.
ignores: ["packages/**"],
files: ["**/*.md/*.js"],
rules: {
"no-unused-vars": "off",
},
},
);
```
Expand All @@ -117,15 +173,14 @@ Configuring Webstorm to use this config can be done as follows:
- Select `Run eslint --fix on save`
- Click on `Apply` & `OK`

Note that WebStorm sometimes doesn't pick up on an updated configuration. To solve this,
select `Disable ESLint configuration`, click on `Apply` and select
`Automatic ESLint configuration` again.

## Notes

We fully run Prettier as an ESLint rule on all common file types (`md`, `json`, `yml`
etc.). This allows you to have a single configuration file for all options and prevents
conflicts between multiple tools that run on save.
> [!NOTE] WebStorm sometimes doesn't pick up on an updated ESLint configuration. A restart
> of the background services fixes this.
>
> - In versions `2023.3` and below, go to the ESLint settings in your preferences
> according to the steps above. Select `Disable ESLint configuration`, click on `Apply`
> and select `Automatic ESLint configuration` again.
> - In versions `20241.1` and above use `Help` -> `Find action` ->
> `Restart ESLint Service`.
## Credits

Expand Down
13 changes: 10 additions & 3 deletions packages/eslint-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,30 @@
},
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"files": ["README.md", "LICENSE", "dist/"],
"files": ["README.md", "LICENSE", "dist/src"],
"engines": {
"node": ">=20"
},
"scripts": {
"build": "tsc -p ./tsconfig.json",
"lint": "eslint . --fix --cache --cache-strategy content --cache-location .cache/eslint/ --color",
"lint:ci": "eslint ."
"lint:ci": "eslint .",
"pretest": "npm run build",
"test": "node --test"
},
"dependencies": {
"@eslint/js": "^8.57.0",
"eslint-plugin-format": "^0.1.0",
"eslint-plugin-markdown": "^3.0.1"
},
"peerDependencies": {
"eslint": ">=8.56.0"
"eslint": ">=8.56.0",
"typescript-eslint": ">=7.2.0"
},
"peerDependenciesMeta": {},
"devDependencies": {
"@types/eslint": "^8.56.2",
"@types/eslint__js": "^8.42.3",
"@types/eslint-plugin-markdown": "^2.0.2"
}
}
48 changes: 48 additions & 0 deletions packages/eslint-config/src/globs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* We need to keep track of used globs to prevent conflicts between language parsers and the
* Prettier config.
*/
const USED_GLOBS = new Set();

export const GLOBS = {
javascript: "**/*.?(c|m)js?(x)",
typescript: "**/*.?(c|m)ts?(x)",

markdown: "**/*.md",

yaml: "**/*.y?(a)ml",
json: "**/*.json?(5|c)",
};

/**
* Keep track of all used globs. We need this to use custom globs for running Prettier in
* ESLint.
*/
export function globUse(globs: string[]) {
for (const glob of globs) {
USED_GLOBS.add(glob);
}

return globs;
}

/**
* Report if a glob is used.
*/
export function globIsUsed(glob: string) {
return USED_GLOBS.has(glob);
}

/**
* Convert any glob to a .format glob to be used with the automatically enabled format config.
*/
export function globAsFormat(glob: string) {
return `${glob}.format`;
}

/**
* Apply custom rules to snippets in markdown files.
*/
export function globMarkdownSnippetFromGlob(glob: string) {
return `**/*.md/*.${glob.split("/").pop()}`;
}
Loading

0 comments on commit 4670394

Please sign in to comment.