chore: replace eslint & prettier w/ biomejs (#21)

* chore: replace eslint & prettier w/ biomejs

* fix syntax error in ci.yml workflow

* ensure that build CI jobs only run if check job succeeds to save resources
This commit is contained in:
M. George Hansen 2024-07-15 08:36:52 -07:00
parent 73135dd4b5
commit 5118a8174b
Signed by: mgeorgehansen
SSH key fingerprint: SHA256:JlIGiQLPyQ2RHTH3a2oVlb20Xkh9Glr8DUF4YTXHJxM
44 changed files with 2408 additions and 5691 deletions

View file

@ -1,2 +0,0 @@
node_modules/
dist/

View file

@ -1,23 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "prettier"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:prettier/recommended"
],
"settings": {
"react": {
"pragma": "createElement"
}
},
"rules": {
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/ban-types": "off",
"react/prop-types": "off",
"react/jsx-key": "off"
}
}

View file

@ -6,7 +6,16 @@ on:
branches: [mainline]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run check
build:
needs: check
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]

View file

@ -1,2 +0,0 @@
node_modules/
dist/

View file

@ -1,7 +0,0 @@
{
"endOfLine": "lf",
"singleQuote": false,
"trailingComma": "all",
"tabWidth": 4,
"semi": true
}

36
biome.json Normal file
View file

@ -0,0 +1,36 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"organizeImports": {
"enabled": true,
"ignore": ["dist", "node_modules", ".temp"]
},
"formatter": {
"enabled": true,
"ignore": ["dist", "node_modules", ".temp"]
},
"linter": {
"enabled": true,
"ignore": ["dist", "node_modules", ".temp"],
"rules": {
"recommended": true,
"style": {
"useShorthandFunctionType": "off"
},
"correctness": {
"useJsxKeyInIterable": "off"
}
}
},
"overrides": [
{
"include": ["test"],
"linter": {
"rules": {
"a11y": {
"useHtmlLang": "off"
}
}
}
}
]
}

View file

@ -1,5 +1,5 @@
import { stylesheet } from "typestyle";
import { Component, createElement } from "websnacks";
import { type Component, createElement } from "websnacks";
const styles = stylesheet({
header: {

View file

@ -1,6 +1,6 @@
import { normalize } from "csstips";
import { stylesheet } from "typestyle";
import { Component, createElement } from "websnacks";
import { type Component, createElement } from "websnacks";
import { stylesheetPath } from "../config";
import { Header } from "./header";
@ -46,10 +46,7 @@ export const Layout: Component<LayoutProps> = ({ children, headline }) => (
{headline && ` | ${headline}`}
</title>
<meta name="description" content="" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href={stylesheetPath} />
</head>

View file

@ -1,5 +1,5 @@
import { stylesheet } from "typestyle";
import { Component, createElement } from "websnacks";
import { type Component, createElement } from "websnacks";
const styles = stylesheet({
navbar: {

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
{
"name": "websnacks-example-personal-site",
"type": "module",
"scripts": {
"build": "websnacks -r ts-node/register build",
"dev": "websnacks -r ts-node/register dev"

View file

@ -1,22 +1,21 @@
import { Component, createElement } from "websnacks";
import { type Component, createElement } from "websnacks";
import { Layout } from "../components/layout";
export const page: Component = () => (
<Layout>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur
dapibus condimentum mauris et egestas. Quisque orci nulla, consequat
at erat laoreet, malesuada sodales nisi. Sed in lorem semper lorem
placerat fermentum a id arcu. Curabitur non aliquam tellus, sed
auctor lacus. Nunc sit amet lectus ultrices, sodales nisl sit amet,
luctus nisl. Nunc mollis imperdiet quam, eget sollicitudin leo
tincidunt vel. Duis felis dui, imperdiet aliquam bibendum sed,
auctor et dolor. Vivamus odio ipsum, venenatis in felis sed, aliquam
dictum turpis. Pellentesque pellentesque consequat neque, id
imperdiet diam molestie nec. Nullam ut vestibulum est. Pellentesque
orci urna, porta vel porta quis, semper ut enim. Donec sit amet urna
arcu. Nam tincidunt fermentum ligula a pharetra.{" "}
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur dapibus
condimentum mauris et egestas. Quisque orci nulla, consequat at erat
laoreet, malesuada sodales nisi. Sed in lorem semper lorem placerat
fermentum a id arcu. Curabitur non aliquam tellus, sed auctor lacus. Nunc
sit amet lectus ultrices, sodales nisl sit amet, luctus nisl. Nunc mollis
imperdiet quam, eget sollicitudin leo tincidunt vel. Duis felis dui,
imperdiet aliquam bibendum sed, auctor et dolor. Vivamus odio ipsum,
venenatis in felis sed, aliquam dictum turpis. Pellentesque pellentesque
consequat neque, id imperdiet diam molestie nec. Nullam ut vestibulum est.
Pellentesque orci urna, porta vel porta quis, semper ut enim. Donec sit
amet urna arcu. Nam tincidunt fermentum ligula a pharetra.{" "}
</p>
</Layout>
);

View file

@ -1,5 +1,5 @@
import { stylesheet } from "typestyle";
import { Component, createElement } from "websnacks";
import { type Component, createElement } from "websnacks";
import { Layout } from "../components/layout";

View file

@ -1,6 +1,6 @@
import { promises as fs } from "fs";
import * as path from "path";
import { Config } from "websnacks";
import { promises as fs } from "node:fs";
import * as path from "node:path";
import type { Config } from "websnacks";
import { stylesheetPath } from "./config";
@ -23,4 +23,3 @@ const config: Config = {
},
},
};
export = config;

2404
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -24,6 +24,7 @@
],
"scripts": {
"build": "tsc",
"check": "biome check .",
"clean": "ts-node scripts/clean.ts",
"prepublishOnly": "npm run reset && npm test",
"pretest": "npm run build",
@ -35,15 +36,9 @@
"test:e2e": "cd test && ts-node --script-mode ./run-e2e.ts"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@types/node": "~18",
"@types/ws": "^7.4.0",
"@typescript-eslint/eslint-plugin": "^4.15.2",
"@typescript-eslint/parser": "^4.15.2",
"eslint": "^7.21.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"prettier": "=2.2.1",
"ts-node": "^10.9.2",
"typescript": "~4.9.5"
},

View file

@ -3,8 +3,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import * as fs from "fs";
import * as path from "path";
import * as fs from "node:fs";
import * as path from "node:path";
const ROOT_DIR = path.resolve(__dirname, "..");
const DIST_DIR = path.join(ROOT_DIR, "dist");

View file

@ -3,10 +3,10 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { promises as fs } from "fs";
import * as path from "path";
import { promises as fs } from "node:fs";
import * as path from "node:path";
import { Config, ConfigPaths } from "./config";
import type { Config, ConfigPaths } from "./config";
import { renderPage } from "./render";
import { decacheModule, walkDir } from "./utils";
@ -30,7 +30,7 @@ const renderPagesToHtml = async ({
`page source at ${srcPath} does not export a "page" variable`,
);
}
let compiledHtml;
let compiledHtml: string;
try {
compiledHtml = renderPage(pageSrc.page());
} catch (error) {

View file

@ -5,7 +5,7 @@
import { renderSite } from "../../build";
import { loadConfig } from "../../config";
import { Command, UsageError } from "../types";
import { type Command, UsageError } from "../types";
const helpText = `\
Usage: websnacks build [ROOT_DIR]

View file

@ -3,14 +3,14 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { existsSync, promises as fs, watch } from "fs";
import * as http from "http";
import * as path from "path";
import { promises as fs, existsSync, watch } from "node:fs";
import * as http from "node:http";
import * as path from "node:path";
import { renderSite } from "../../build";
import { Config, loadConfig } from "../../config";
import { type Config, loadConfig } from "../../config";
import { isErrnoException } from "../../utils/error";
import { Command, UsageError } from "../types";
import { type Command, UsageError } from "../types";
const DEFAULT_SERVER_PORT = 8080;
@ -30,7 +30,7 @@ const injectLiveReloadScript = (htmlContents: string, port: number): string =>
);
const guessMimeType = (ext: string): string => {
let mimeType;
let mimeType: string;
switch (ext) {
case ".apng":
mimeType = "image/apng";
@ -149,7 +149,7 @@ const startHttpServer = async (publicDir: string): Promise<http.Server> => {
reqExt = ".html";
}
let contents;
let contents: Buffer | string;
try {
contents = await fs.readFile(path.join(publicDir, reqPath));
} catch (error) {
@ -196,7 +196,7 @@ const startWebSocketServer = async (
httpServer: http.Server,
): Promise<import("ws").Server | undefined> => {
// Attempt to load the ws module, aborting if it isn't available.
let ws;
let ws: typeof import("ws");
try {
ws = await import("ws");
} catch (error) {
@ -237,7 +237,7 @@ const watchFolders = async (
}
console.warn(
`'node-watch' module not found, falling back to fs.watch (may ` +
`result in file watch issues on some OSes)`,
"result in file watch issues on some OSes)",
);
}
// NOTE: fs.watch has significant cross-platform issues, including
@ -298,12 +298,10 @@ const devCommand: Command = {
await watchFolders(watchedFolders, async (event, filePath) => {
const filePathForLog = filePath || "<UNKNOWN_FILE>";
const eventForLog = event || "<UNKNOWN_EVENT>";
console.log(
`${filePathForLog}:${eventForLog} triggering rebuild...`,
);
console.log(`${filePathForLog}:${eventForLog} triggering rebuild...`);
await rebuild();
if (wsServer != null) {
console.log(`rebuild finished, reloading browsers...`);
console.log("rebuild finished, reloading browsers...");
for (const ws of wsServer.clients) {
ws.send("reload");
}

View file

@ -3,7 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Command, UsageError } from "./types";
import { type Command, UsageError } from "./types";
const globalHelpText = `\
Usage: websnacks [...globalOptions] <command>
@ -45,7 +45,7 @@ const parseArgs = (
const moduleName = args.shift();
if (moduleName == null) {
throw new UsageError(
`-r requires a valid module name`,
"-r requires a valid module name",
globalHelpText,
);
}
@ -67,7 +67,7 @@ const _main = async (args: string[]): Promise<void> => {
return;
}
if (commandName == null) {
throw new UsageError(`must specify a valid command`, globalHelpText);
throw new UsageError("must specify a valid command", globalHelpText);
}
for (const moduleName of options.require) {
await import(moduleName);
@ -82,10 +82,7 @@ const _main = async (args: string[]): Promise<void> => {
command = await import("./commands/dev");
break;
default:
throw new UsageError(
`unknown command ${commandName}`,
globalHelpText,
);
throw new UsageError(`unknown command ${commandName}`, globalHelpText);
}
// NOTE: Should this just delegate to the command?
for (const arg of commandArgs) {

View file

@ -37,7 +37,7 @@ export type Element =
/**
* Custom HTMLElement factory that can be parameterized by props.
*/
export interface Component<P extends object = {}> {
export interface Component<P extends object = Record<string, unknown>> {
(
props: P & {
children?: Element[];
@ -45,7 +45,7 @@ export interface Component<P extends object = {}> {
): HTMLElement;
}
export const Fragment: Component<{}> = ({ children }) => ({
export const Fragment: Component = ({ children }) => ({
tag: "#fragment",
attributes: {},
children: children || [],

View file

@ -3,7 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import * as path from "path";
import * as path from "node:path";
import { decacheModule } from "./utils";
@ -58,7 +58,7 @@ const noop = () => {};
* @return Fully-realized configuration.
*/
export const loadConfig = async (rootDir: string): Promise<Config> => {
let configPath;
let configPath = "";
let userConfig: UserConfig = {};
// Attempt to load a websnacks.ts/js file in rootDir.
try {
@ -74,7 +74,7 @@ export const loadConfig = async (rootDir: string): Promise<Config> => {
const staticAssetsDir = path.join(rootDir, "static");
const watch = [pagesDir, staticAssetsDir];
if (configPath != null) {
if (configPath) {
watch.push(path.relative(rootDir, configPath));
}
if (userConfig.watch != null) {
@ -97,3 +97,5 @@ export const loadConfig = async (rootDir: string): Promise<Config> => {
watch,
};
};
export const defineConfig = (userConfig: UserConfig): UserConfig => userConfig;

View file

@ -3,8 +3,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Component, Element, HTMLElement } from "./component";
import { HTMLAttributes } from "./jsx";
import type { Component, Element, HTMLElement } from "./component";
import type { HTMLAttributes } from "./jsx";
import { flatDeep } from "./utils";
/**
@ -38,8 +38,7 @@ export function createElement(
...children: Element[]
): HTMLElement;
export function createElement(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: string | Component<any>,
type: string | Component,
props: object | null,
...children: Element[]
): HTMLElement {
@ -68,9 +67,7 @@ export function createElement(
typeof value !== "number" &&
typeof value !== "boolean"
) {
console.warn(
`non-primitive attribute ${key} = ${JSON.stringify(value)}`,
);
console.warn(`non-primitive attribute ${key} = ${JSON.stringify(value)}`);
continue;
}
attrs[key] = value;

View file

@ -4,6 +4,6 @@
*/
export { HTMLElement, Component, Fragment } from "./component";
export { UserConfig as Config } from "./config";
export { UserConfig as Config, defineConfig } from "./config";
export { createElement } from "./create-element";
export * from "./jsx";

View file

@ -4,7 +4,7 @@
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { HTMLElement } from "./component";
import type { HTMLElement } from "./component";
export interface RdfaAttributes {
about?: string;

View file

@ -3,7 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Element, HTMLElement } from "./component";
import type { Element, HTMLElement } from "./component";
const HTML_ESCAPES: { [char: string]: string } = {
"&": "&amp;",
@ -71,9 +71,7 @@ const startTag = (elem: HTMLElement): string => {
if (attrValue === true) {
output += ` ${normalizedAttrName}=""`;
} else {
output += ` ${normalizedAttrName}="${escapeAttr(
attrValue.toString(),
)}"`;
output += ` ${normalizedAttrName}="${escapeAttr(attrValue.toString())}"`;
}
}
@ -97,8 +95,8 @@ const endTag = (elem: HTMLElement): string => {
* @return Fully rendered HTML document as a string.
*/
export const renderPage = (rootElem: Element): string => {
if (rootElem == undefined) {
throw new Error(`root page element cannot be null`);
if (rootElem == null) {
throw new Error("root page element cannot be null");
}
if (typeof rootElem !== "object" || !("tag" in rootElem)) {
throw new Error(

View file

@ -3,8 +3,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { promises as fs } from "fs";
import * as path from "path";
import { promises as fs } from "node:fs";
import * as path from "node:path";
export { decacheModule } from "./decache-module";

View file

@ -3,14 +3,14 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { promises as fs } from "fs";
import * as path from "path";
import { promises as fs } from "node:fs";
import * as path from "node:path";
import {
npmCmd,
runCommand,
WEBSNACKS_BIN_PATH,
WEBSNACKS_REPO_ROOT,
npmCmd,
runCommand,
withTempDir,
} from "../helpers/e2e";
import { testSuite } from "../lib";

View file

@ -3,15 +3,15 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { promises as fs } from "fs";
import * as path from "path";
import { promises as fs } from "node:fs";
import * as path from "node:path";
import {
WEBSNACKS_BIN_PATH,
WEBSNACKS_REPO_ROOT,
npmCmd,
runCommand,
wait,
WEBSNACKS_BIN_PATH,
WEBSNACKS_REPO_ROOT,
withTempDir,
} from "../helpers/e2e";
import { testSuite } from "../lib";

View file

@ -3,10 +3,10 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { ChildProcess, spawn } from "child_process";
import { promises as fs } from "fs";
import * as os from "os";
import * as path from "path";
import { type ChildProcess, spawn } from "node:child_process";
import { promises as fs } from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
/**
* Set a timeout and wait for at least the specified number of milliseconds,
@ -137,9 +137,7 @@ export const runCommand = (
threwError = true;
process.kill();
reject(
new Error(
`max timeout of ${optionsWithDefaults.timeoutMs}ms reached`,
),
new Error(`max timeout of ${optionsWithDefaults.timeoutMs}ms reached`),
);
}, optionsWithDefaults.timeoutMs);
process.on("exit", (code) => {

View file

@ -48,7 +48,7 @@ export class Expect<T> {
public toEqual(expected: T): void {
if (!areEqual(this.value, expected)) {
throw new ExpectError(
`value does not equal expected`,
"value does not equal expected",
expected,
this.value,
);
@ -71,7 +71,7 @@ export class StringExpect extends Expect<string> {
public toMatch(pattern: RegExp): void {
if (!this.value.match(pattern)) {
throw new ExpectError(
`value does not match expected pattern`,
"value does not match expected pattern",
pattern,
this.value,
);
@ -89,7 +89,7 @@ export class StringExpect extends Expect<string> {
public toStartWith(prefix: string): void {
if (!this.value.startsWith(prefix)) {
throw new ExpectError(
`value does not start with expected prefix`,
"value does not start with expected prefix",
prefix,
this.value,
);
@ -107,7 +107,7 @@ export class StringExpect extends Expect<string> {
public toEndWith(suffix: string): void {
if (!this.value.endsWith(suffix)) {
throw new ExpectError(
`value does not end with expected suffix`,
"value does not end with expected suffix",
suffix,
this.value,
);
@ -135,11 +135,7 @@ export class FunctionExpect<T> extends Expect<() => T> {
this.value();
} catch (error) {
if (!(error instanceof Error)) {
throw new ExpectError(
`function threw non-Error value`,
pattern,
error,
);
throw new ExpectError("function threw non-Error value", pattern, error);
}
if (!matches(error.message, pattern)) {
throw new ExpectError(
@ -151,7 +147,7 @@ export class FunctionExpect<T> extends Expect<() => T> {
return;
}
throw new ExpectError(
`function did not throw expected error`,
"function did not throw expected error",
pattern,
null,
);

View file

@ -39,9 +39,7 @@ const runTest = async (test: Test): Promise<TestResult> => {
error:
error instanceof Error
? error
: new Error(
`threw non-error object: ${displayValue(error)}`,
),
: new Error(`threw non-error object: ${displayValue(error)}`),
};
}
return result;

View file

@ -20,7 +20,7 @@ export const shuffle = <T>(arr: T[]): void => {
};
const areArraysEqual = <T>(a: T[], b: T[]): boolean => {
if (a.length != b.length) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {

View file

@ -3,9 +3,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { fork } from "child_process";
import * as fs from "fs";
import * as path from "path";
import { fork } from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import { shuffle } from "./lib/utils";

View file

@ -3,9 +3,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { fork } from "child_process";
import * as fs from "fs";
import * as path from "path";
import { fork } from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import { shuffle } from "./lib/utils";

View file

@ -3,7 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Component, createElement, Fragment } from "../../dist";
import { type Component, Fragment, createElement } from "../../dist";
import { renderPage } from "../../dist/render";
import { testSuite } from "../lib";
@ -20,9 +20,7 @@ testSuite("renderPage", ({ test, expect }) => {
});
test("escapes HTML in tag names", () => {
const html = renderPage(
<html>{createElement("div></div", null)}</html>,
);
const html = renderPage(<html>{createElement("div></div", null)}</html>);
expect(html).toEqual(
"<!DOCTYPE html><html><div&gt;&lt;/div></div&gt;&lt;/div></html>",
);
@ -57,9 +55,7 @@ testSuite("renderPage", ({ test, expect }) => {
test("renders text nodes", () => {
const html = renderPage(<html>There are three lights!</html>);
expect(html).toEqual(
"<!DOCTYPE html><html>There are three lights!</html>",
);
expect(html).toEqual("<!DOCTYPE html><html>There are three lights!</html>");
});
test("renders spliced number nodes", () => {
@ -122,6 +118,7 @@ testSuite("renderPage", ({ test, expect }) => {
const html = renderPage(
<html>
<div
// biome-ignore lint/security/noDangerouslySetInnerHtml: explicit test
dangerouslySetInnerHTML={{
__html: "<div>red alert!</div>",
}}
@ -138,6 +135,8 @@ testSuite("renderPage", ({ test, expect }) => {
renderPage(
<html>
<div
// biome-ignore lint/security/noDangerouslySetInnerHtml: explicit test
// biome-ignore lint/security/noDangerouslySetInnerHtmlWithChildren: explicit test
dangerouslySetInnerHTML={{
__html: "<div>set phasers to kill</div>",
}}