Compare commits
No commits in common. "main" and "v0.2.0" have entirely different histories.
47 changed files with 5041 additions and 2765 deletions
2
.eslintignore
Normal file
2
.eslintignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
23
.eslintrc.json
Normal file
23
.eslintrc.json
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: node-24
|
|
||||||
steps:
|
|
||||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
|
||||||
- name: Cache npm dependencies
|
|
||||||
uses: https://code.forgejo.org/actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: node-24-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run check
|
|
||||||
|
|
||||||
build:
|
|
||||||
needs: check
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node: [22, 24]
|
|
||||||
runs-on: node-${{ matrix.node }}
|
|
||||||
steps:
|
|
||||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
|
||||||
- name: Cache npm dependencies
|
|
||||||
uses: https://code.forgejo.org/actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: node-${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run build
|
|
||||||
- run: npm test
|
|
||||||
2
.nvmrc
2
.nvmrc
|
|
@ -1 +1 @@
|
||||||
18
|
lts/erbium
|
||||||
|
|
|
||||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": true
|
||||||
|
}
|
||||||
12
.travis.yml
Normal file
12
.travis.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
git:
|
||||||
|
autocrlf: input
|
||||||
|
language: node_js
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
- osx
|
||||||
|
- windows
|
||||||
|
node_js:
|
||||||
|
- node
|
||||||
|
- lts/*
|
||||||
|
- 12
|
||||||
|
- 10
|
||||||
19
README.md
19
README.md
|
|
@ -1,9 +1,20 @@
|
||||||
# websnacks: Minimal Dependency Server-Side JSX for Static Sites
|
# websnacks: Minimal Dependency Server-Side JSX for Static Sites
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/@websnacksjs/websnacks "NPM release")
|
<div>
|
||||||
[](https://www.mozilla.org/en-US/MPL/2.0/FAQ/ "License info")
|
|
||||||
[](https://git.theinnerlimit.ch/websnacksjs/websnacks/actions?workflow=ci.yml "CI status for main branch")
|
[](https://www.npmjs.com/package/@websnacksjs/websnacks "NPM release")
|
||||||

|
[](https://www.mozilla.org/en-US/MPL/2.0/FAQ/ "License info")
|
||||||
|
[](https://travis-ci.com/websnacksjs/websnacks "Build status for mainline branch")
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
[](https://david-dm.org/websnacksjs/websnacks "Dependency status")
|
||||||
|
[](https://david-dm.org/websnacksjs/websnacks?type=optional "Optional dependency status")
|
||||||
|
[](https://david-dm.org/websnacksjs/websnacks?type=dev "Dev dependency status")
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
Develop fully static websites using typesafe JSX templates on the server without the complex build system and dependency management of server-side rendered React frameworks.
|
Develop fully static websites using typesafe JSX templates on the server without the complex build system and dependency management of server-side rendered React frameworks.
|
||||||
|
|
||||||
|
|
|
||||||
41
biome.json
41
biome.json
|
|
@ -1,41 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
|
|
||||||
"files": {
|
|
||||||
"includes": ["**", "!dist", "!node_modules", "!.temp"]
|
|
||||||
},
|
|
||||||
"assist": {
|
|
||||||
"enabled": true,
|
|
||||||
"actions": {
|
|
||||||
"source": {
|
|
||||||
"organizeImports": "on"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true,
|
|
||||||
"style": {
|
|
||||||
"useShorthandFunctionType": "off"
|
|
||||||
},
|
|
||||||
"correctness": {
|
|
||||||
"useJsxKeyInIterable": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"includes": ["test/**"],
|
|
||||||
"linter": {
|
|
||||||
"rules": {
|
|
||||||
"a11y": {
|
|
||||||
"useHtmlLang": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import { stylesheet } from "typestyle";
|
import { stylesheet } from "typestyle";
|
||||||
import {
|
import { Component, createElement } from "websnacks";
|
||||||
type Component,
|
|
||||||
// biome-ignore lint/correctness/noUnusedImports: required to support JSX
|
|
||||||
createElement,
|
|
||||||
} from "websnacks";
|
|
||||||
|
|
||||||
const styles = stylesheet({
|
const styles = stylesheet({
|
||||||
header: {
|
header: {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
import { normalize } from "csstips";
|
import { normalize } from "csstips";
|
||||||
import { stylesheet } from "typestyle";
|
import { stylesheet } from "typestyle";
|
||||||
import {
|
import { Component, createElement } from "websnacks";
|
||||||
type Component,
|
|
||||||
// biome-ignore lint/correctness/noUnusedImports: required to support JSX
|
|
||||||
createElement,
|
|
||||||
} from "websnacks";
|
|
||||||
|
|
||||||
import { stylesheetPath } from "../config";
|
import { stylesheetPath } from "../config";
|
||||||
import { Header } from "./header";
|
import { Header } from "./header";
|
||||||
|
|
@ -50,7 +46,10 @@ export const Layout: Component<LayoutProps> = ({ children, headline }) => (
|
||||||
{headline && ` | ${headline}`}
|
{headline && ` | ${headline}`}
|
||||||
</title>
|
</title>
|
||||||
<meta name="description" content="" />
|
<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} />
|
<link rel="stylesheet" href={stylesheetPath} />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import { stylesheet } from "typestyle";
|
import { stylesheet } from "typestyle";
|
||||||
import {
|
import { Component, createElement } from "websnacks";
|
||||||
type Component,
|
|
||||||
// biome-ignore lint/correctness/noUnusedImports: required to support JSX
|
|
||||||
createElement,
|
|
||||||
} from "websnacks";
|
|
||||||
|
|
||||||
const styles = stylesheet({
|
const styles = stylesheet({
|
||||||
navbar: {
|
navbar: {
|
||||||
|
|
|
||||||
1367
examples/personal-site/package-lock.json
generated
1367
examples/personal-site/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "websnacks-example-personal-site",
|
"name": "websnacks-example-personal-site",
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "websnacks -r ts-node/register build",
|
"build": "websnacks -r ts-node/register build",
|
||||||
"dev": "websnacks -r ts-node/register dev"
|
"dev": "websnacks -r ts-node/register dev"
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,22 @@
|
||||||
import {
|
import { Component, createElement } from "websnacks";
|
||||||
type Component,
|
|
||||||
// biome-ignore lint/correctness/noUnusedImports: required to support JSX
|
|
||||||
createElement,
|
|
||||||
} from "websnacks";
|
|
||||||
|
|
||||||
import { Layout } from "../components/layout";
|
import { Layout } from "../components/layout";
|
||||||
|
|
||||||
export const page: Component = () => (
|
export const page: Component = () => (
|
||||||
<Layout>
|
<Layout>
|
||||||
<p>
|
<p>
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur dapibus
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur
|
||||||
condimentum mauris et egestas. Quisque orci nulla, consequat at erat
|
dapibus condimentum mauris et egestas. Quisque orci nulla, consequat
|
||||||
laoreet, malesuada sodales nisi. Sed in lorem semper lorem placerat
|
at erat laoreet, malesuada sodales nisi. Sed in lorem semper lorem
|
||||||
fermentum a id arcu. Curabitur non aliquam tellus, sed auctor lacus. Nunc
|
placerat fermentum a id arcu. Curabitur non aliquam tellus, sed
|
||||||
sit amet lectus ultrices, sodales nisl sit amet, luctus nisl. Nunc mollis
|
auctor lacus. Nunc sit amet lectus ultrices, sodales nisl sit amet,
|
||||||
imperdiet quam, eget sollicitudin leo tincidunt vel. Duis felis dui,
|
luctus nisl. Nunc mollis imperdiet quam, eget sollicitudin leo
|
||||||
imperdiet aliquam bibendum sed, auctor et dolor. Vivamus odio ipsum,
|
tincidunt vel. Duis felis dui, imperdiet aliquam bibendum sed,
|
||||||
venenatis in felis sed, aliquam dictum turpis. Pellentesque pellentesque
|
auctor et dolor. Vivamus odio ipsum, venenatis in felis sed, aliquam
|
||||||
consequat neque, id imperdiet diam molestie nec. Nullam ut vestibulum est.
|
dictum turpis. Pellentesque pellentesque consequat neque, id
|
||||||
Pellentesque orci urna, porta vel porta quis, semper ut enim. Donec sit
|
imperdiet diam molestie nec. Nullam ut vestibulum est. Pellentesque
|
||||||
amet urna arcu. Nam tincidunt fermentum ligula a pharetra.{" "}
|
orci urna, porta vel porta quis, semper ut enim. Donec sit amet urna
|
||||||
|
arcu. Nam tincidunt fermentum ligula a pharetra.{" "}
|
||||||
</p>
|
</p>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import { stylesheet } from "typestyle";
|
import { stylesheet } from "typestyle";
|
||||||
import {
|
import { Component, createElement } from "websnacks";
|
||||||
type Component,
|
|
||||||
// biome-ignore lint/correctness/noUnusedImports: required to support JSX
|
|
||||||
createElement,
|
|
||||||
} from "websnacks";
|
|
||||||
|
|
||||||
import { Layout } from "../components/layout";
|
import { Layout } from "../components/layout";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "fs";
|
||||||
import * as path from "node:path";
|
import * as path from "path";
|
||||||
import type { Config } from "websnacks";
|
import { Config } from "websnacks";
|
||||||
|
|
||||||
import { stylesheetPath } from "./config";
|
import { stylesheetPath } from "./config";
|
||||||
|
|
||||||
const _config: Config = {
|
const config: Config = {
|
||||||
// Watch additional files and folders for changes when the dev server is
|
// Watch additional files and folders for changes when the dev server is
|
||||||
// running.
|
// running.
|
||||||
watch: ["components/", "config.ts"],
|
watch: ["components/", "config.ts"],
|
||||||
|
|
@ -23,3 +23,4 @@ const _config: Config = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
export = config;
|
||||||
|
|
|
||||||
2037
package-lock.json
generated
2037
package-lock.json
generated
File diff suppressed because it is too large
Load diff
23
package.json
23
package.json
|
|
@ -9,7 +9,7 @@
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"repository": "github:websnacksjs/websnacks",
|
"repository": "github:websnacksjs/websnacks",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "types.d.ts",
|
"types": "types.d.ts",
|
||||||
|
|
@ -24,7 +24,6 @@
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"check": "biome check .",
|
|
||||||
"clean": "ts-node scripts/clean.ts",
|
"clean": "ts-node scripts/clean.ts",
|
||||||
"prepublishOnly": "npm run reset && npm test",
|
"prepublishOnly": "npm run reset && npm test",
|
||||||
"pretest": "npm run build",
|
"pretest": "npm run build",
|
||||||
|
|
@ -36,14 +35,20 @@
|
||||||
"test:e2e": "cd test && ts-node --script-mode ./run-e2e.ts"
|
"test:e2e": "cd test && ts-node --script-mode ./run-e2e.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.4.14",
|
"@types/node": "~10",
|
||||||
"@types/node": "~18",
|
"@types/ws": "^7.4.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@typescript-eslint/eslint-plugin": "^4.15.2",
|
||||||
"ts-node": "^10.9.2",
|
"@typescript-eslint/parser": "^4.15.2",
|
||||||
"typescript": "~4.9.5"
|
"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": "^9.1.1",
|
||||||
|
"typescript": "~4.2.2"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"node-watch": "^0.7.4",
|
"node-watch": "^0.7.1",
|
||||||
"ws": "^8.20.0"
|
"ws": "^7.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from "node:fs";
|
import * as fs from "fs";
|
||||||
import * as path from "node:path";
|
import * as path from "path";
|
||||||
|
|
||||||
const ROOT_DIR = path.resolve(__dirname, "..");
|
const ROOT_DIR = path.resolve(__dirname, "..");
|
||||||
const DIST_DIR = path.join(ROOT_DIR, "dist");
|
const DIST_DIR = path.join(ROOT_DIR, "dist");
|
||||||
|
|
|
||||||
14
src/build.ts
14
src/build.ts
|
|
@ -3,10 +3,10 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "fs";
|
||||||
import * as path from "node:path";
|
import * as path from "path";
|
||||||
|
|
||||||
import type { Config, ConfigPaths } from "./config";
|
import { Config, ConfigPaths } from "./config";
|
||||||
import { renderPage } from "./render";
|
import { renderPage } from "./render";
|
||||||
import { decacheModule, walkDir } from "./utils";
|
import { decacheModule, walkDir } from "./utils";
|
||||||
|
|
||||||
|
|
@ -30,11 +30,13 @@ const renderPagesToHtml = async ({
|
||||||
`page source at ${srcPath} does not export a "page" variable`,
|
`page source at ${srcPath} does not export a "page" variable`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let compiledHtml: string;
|
let compiledHtml;
|
||||||
try {
|
try {
|
||||||
compiledHtml = renderPage(pageSrc.page());
|
compiledHtml = renderPage(pageSrc.page());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`failed to compile ${srcPath}: ${error}`);
|
throw new Error(
|
||||||
|
`failed to compile ${srcPath}: ${error.stack ?? error}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const relPath = path.relative(pagesDir, path.dirname(srcPath));
|
const relPath = path.relative(pagesDir, path.dirname(srcPath));
|
||||||
let baseName = path.basename(srcPath, ".tsx");
|
let baseName = path.basename(srcPath, ".tsx");
|
||||||
|
|
@ -58,7 +60,7 @@ const copyStaticAssets = async ({
|
||||||
}: ConfigPaths): Promise<void> => {
|
}: ConfigPaths): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await fs.access(staticAssetsDir);
|
await fs.access(staticAssetsDir);
|
||||||
} catch (_error) {
|
} catch (error) {
|
||||||
// Static assets folder doesn't exist, so no-op.
|
// Static assets folder doesn't exist, so no-op.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { renderSite } from "../../build";
|
import { renderSite } from "../../build";
|
||||||
import { loadConfig } from "../../config";
|
import { loadConfig } from "../../config";
|
||||||
import { type Command, UsageError } from "../types";
|
import { Command, UsageError } from "../types";
|
||||||
|
|
||||||
const helpText = `\
|
const helpText = `\
|
||||||
Usage: websnacks build [ROOT_DIR]
|
Usage: websnacks build [ROOT_DIR]
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, promises as fs, watch } from "node:fs";
|
import { existsSync, promises as fs, watch } from "fs";
|
||||||
import * as http from "node:http";
|
import * as http from "http";
|
||||||
import * as path from "node:path";
|
import * as net from "net";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
import { renderSite } from "../../build";
|
import { renderSite } from "../../build";
|
||||||
import { type Config, loadConfig } from "../../config";
|
import { 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;
|
const DEFAULT_SERVER_PORT = 8080;
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ const injectLiveReloadScript = (htmlContents: string, port: number): string =>
|
||||||
);
|
);
|
||||||
|
|
||||||
const guessMimeType = (ext: string): string => {
|
const guessMimeType = (ext: string): string => {
|
||||||
let mimeType: string;
|
let mimeType;
|
||||||
switch (ext) {
|
switch (ext) {
|
||||||
case ".apng":
|
case ".apng":
|
||||||
mimeType = "image/apng";
|
mimeType = "image/apng";
|
||||||
|
|
@ -119,16 +119,14 @@ const guessMimeType = (ext: string): string => {
|
||||||
return mimeType;
|
return mimeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const portFromServer = (
|
const portFromServer = (server: Pick<net.Server, "address">): number => {
|
||||||
addrInfo: { port: number } | object | string | undefined | null,
|
const addrInfo = server.address();
|
||||||
): number => {
|
if (addrInfo == null) {
|
||||||
if (
|
throw new Error(`server address is null (this should never happen!)`);
|
||||||
typeof addrInfo !== "object" ||
|
}
|
||||||
addrInfo == null ||
|
if (typeof addrInfo === "string") {
|
||||||
!("port" in addrInfo)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"server address does not have a valid port (this should never happen!)",
|
`server address is a string (this should never happen!)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return addrInfo.port;
|
return addrInfo.port;
|
||||||
|
|
@ -149,10 +147,10 @@ const startHttpServer = async (publicDir: string): Promise<http.Server> => {
|
||||||
reqExt = ".html";
|
reqExt = ".html";
|
||||||
}
|
}
|
||||||
|
|
||||||
let contents: Buffer | string;
|
let contents;
|
||||||
try {
|
try {
|
||||||
contents = await fs.readFile(path.join(publicDir, reqPath));
|
contents = await fs.readFile(path.join(publicDir, reqPath));
|
||||||
} catch (_error) {
|
} catch (error) {
|
||||||
console.error(`unable to load file ${reqPath}`);
|
console.error(`unable to load file ${reqPath}`);
|
||||||
res.writeHead(404);
|
res.writeHead(404);
|
||||||
res.end();
|
res.end();
|
||||||
|
|
@ -160,7 +158,7 @@ const startHttpServer = async (publicDir: string): Promise<http.Server> => {
|
||||||
}
|
}
|
||||||
const mimeType = guessMimeType(reqExt);
|
const mimeType = guessMimeType(reqExt);
|
||||||
if (mimeType === "text/html") {
|
if (mimeType === "text/html") {
|
||||||
const port = portFromServer(req.socket.address());
|
const port = portFromServer(req.socket);
|
||||||
contents = injectLiveReloadScript(contents.toString("utf8"), port);
|
contents = injectLiveReloadScript(contents.toString("utf8"), port);
|
||||||
}
|
}
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
|
|
@ -178,16 +176,12 @@ const startHttpServer = async (publicDir: string): Promise<http.Server> => {
|
||||||
try {
|
try {
|
||||||
await listen(DEFAULT_SERVER_PORT);
|
await listen(DEFAULT_SERVER_PORT);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (
|
if (error.code !== "EADDRINUSE") {
|
||||||
error instanceof Error &&
|
|
||||||
isErrnoException(error) &&
|
|
||||||
error.code !== "EADDRINUSE"
|
|
||||||
) {
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
await listen();
|
await listen();
|
||||||
}
|
}
|
||||||
const port = portFromServer(httpServer.address());
|
const port = portFromServer(httpServer);
|
||||||
console.log(`Listening at http://127.0.0.1:${port}`);
|
console.log(`Listening at http://127.0.0.1:${port}`);
|
||||||
return httpServer;
|
return httpServer;
|
||||||
};
|
};
|
||||||
|
|
@ -196,15 +190,11 @@ const startWebSocketServer = async (
|
||||||
httpServer: http.Server,
|
httpServer: http.Server,
|
||||||
): Promise<import("ws").Server | undefined> => {
|
): Promise<import("ws").Server | undefined> => {
|
||||||
// Attempt to load the ws module, aborting if it isn't available.
|
// Attempt to load the ws module, aborting if it isn't available.
|
||||||
let ws: typeof import("ws");
|
let ws;
|
||||||
try {
|
try {
|
||||||
ws = await import("ws");
|
ws = await import("ws");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (
|
if (error.code !== "MODULE_NOT_FOUND") {
|
||||||
error instanceof Error &&
|
|
||||||
isErrnoException(error) &&
|
|
||||||
error.code !== "MODULE_NOT_FOUND"
|
|
||||||
) {
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
console.warn(`'ws' module not found, live-reloading will be disabled`);
|
console.warn(`'ws' module not found, live-reloading will be disabled`);
|
||||||
|
|
@ -228,23 +218,19 @@ const watchFolders = async (
|
||||||
nodeWatch.default(folders, { recursive: true }, listener);
|
nodeWatch.default(folders, { recursive: true }, listener);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (
|
if (error.code !== "MODULE_NOT_FOUND") {
|
||||||
error instanceof Error &&
|
|
||||||
isErrnoException(error) &&
|
|
||||||
error.code !== "MODULE_NOT_FOUND"
|
|
||||||
) {
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
console.warn(
|
console.warn(
|
||||||
`'node-watch' module not found, falling back to fs.watch (may ` +
|
`'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
|
// NOTE: fs.watch has significant cross-platform issues, including
|
||||||
// triggering duplicate file events on some systems.
|
// triggering duplicate file events on some systems.
|
||||||
for (const folder of folders) {
|
for (const folder of folders) {
|
||||||
watch(folder, { recursive: true }, (_, fileName) => {
|
watch(folder, { recursive: true }, (_, fileName) => {
|
||||||
listener("update", fileName || undefined);
|
listener("update", fileName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -298,10 +284,12 @@ const devCommand: Command = {
|
||||||
await watchFolders(watchedFolders, async (event, filePath) => {
|
await watchFolders(watchedFolders, async (event, filePath) => {
|
||||||
const filePathForLog = filePath || "<UNKNOWN_FILE>";
|
const filePathForLog = filePath || "<UNKNOWN_FILE>";
|
||||||
const eventForLog = event || "<UNKNOWN_EVENT>";
|
const eventForLog = event || "<UNKNOWN_EVENT>";
|
||||||
console.log(`${filePathForLog}:${eventForLog} triggering rebuild...`);
|
console.log(
|
||||||
|
`${filePathForLog}:${eventForLog} triggering rebuild...`,
|
||||||
|
);
|
||||||
await rebuild();
|
await rebuild();
|
||||||
if (wsServer != null) {
|
if (wsServer != null) {
|
||||||
console.log("rebuild finished, reloading browsers...");
|
console.log(`rebuild finished, reloading browsers...`);
|
||||||
for (const ws of wsServer.clients) {
|
for (const ws of wsServer.clients) {
|
||||||
ws.send("reload");
|
ws.send("reload");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type Command, UsageError } from "./types";
|
import { Command, UsageError } from "./types";
|
||||||
|
|
||||||
const globalHelpText = `\
|
const globalHelpText = `\
|
||||||
Usage: websnacks [...globalOptions] <command>
|
Usage: websnacks [...globalOptions] <command>
|
||||||
|
|
@ -45,7 +45,7 @@ const parseArgs = (
|
||||||
const moduleName = args.shift();
|
const moduleName = args.shift();
|
||||||
if (moduleName == null) {
|
if (moduleName == null) {
|
||||||
throw new UsageError(
|
throw new UsageError(
|
||||||
"-r requires a valid module name",
|
`-r requires a valid module name`,
|
||||||
globalHelpText,
|
globalHelpText,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +67,7 @@ const _main = async (args: string[]): Promise<void> => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (commandName == null) {
|
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) {
|
for (const moduleName of options.require) {
|
||||||
await import(moduleName);
|
await import(moduleName);
|
||||||
|
|
@ -82,7 +82,10 @@ const _main = async (args: string[]): Promise<void> => {
|
||||||
command = await import("./commands/dev");
|
command = await import("./commands/dev");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new UsageError(`unknown command ${commandName}`, globalHelpText);
|
throw new UsageError(
|
||||||
|
`unknown command ${commandName}`,
|
||||||
|
globalHelpText,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// NOTE: Should this just delegate to the command?
|
// NOTE: Should this just delegate to the command?
|
||||||
for (const arg of commandArgs) {
|
for (const arg of commandArgs) {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export type Element =
|
||||||
/**
|
/**
|
||||||
* Custom HTMLElement factory that can be parameterized by props.
|
* Custom HTMLElement factory that can be parameterized by props.
|
||||||
*/
|
*/
|
||||||
export interface Component<P extends object = Record<string, unknown>> {
|
export interface Component<P extends object = {}> {
|
||||||
(
|
(
|
||||||
props: P & {
|
props: P & {
|
||||||
children?: Element[];
|
children?: Element[];
|
||||||
|
|
@ -45,7 +45,7 @@ export interface Component<P extends object = Record<string, unknown>> {
|
||||||
): HTMLElement;
|
): HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Fragment: Component = ({ children }) => ({
|
export const Fragment: Component<{}> = ({ children }) => ({
|
||||||
tag: "#fragment",
|
tag: "#fragment",
|
||||||
attributes: {},
|
attributes: {},
|
||||||
children: children || [],
|
children: children || [],
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as path from "node:path";
|
import * as path from "path";
|
||||||
|
|
||||||
import { decacheModule } from "./utils";
|
import { decacheModule } from "./utils";
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ const noop = () => {};
|
||||||
* @return Fully-realized configuration.
|
* @return Fully-realized configuration.
|
||||||
*/
|
*/
|
||||||
export const loadConfig = async (rootDir: string): Promise<Config> => {
|
export const loadConfig = async (rootDir: string): Promise<Config> => {
|
||||||
let configPath = "";
|
let configPath;
|
||||||
let userConfig: UserConfig = {};
|
let userConfig: UserConfig = {};
|
||||||
// Attempt to load a websnacks.ts/js file in rootDir.
|
// Attempt to load a websnacks.ts/js file in rootDir.
|
||||||
try {
|
try {
|
||||||
|
|
@ -66,7 +66,7 @@ export const loadConfig = async (rootDir: string): Promise<Config> => {
|
||||||
decacheModule(configPath);
|
decacheModule(configPath);
|
||||||
// TODO: validate user config.
|
// TODO: validate user config.
|
||||||
userConfig = await import(configPath);
|
userConfig = await import(configPath);
|
||||||
} catch (_error) {
|
} catch (error) {
|
||||||
// Use default config;
|
// Use default config;
|
||||||
}
|
}
|
||||||
const outDir = path.join(rootDir, "public");
|
const outDir = path.join(rootDir, "public");
|
||||||
|
|
@ -74,7 +74,7 @@ export const loadConfig = async (rootDir: string): Promise<Config> => {
|
||||||
const staticAssetsDir = path.join(rootDir, "static");
|
const staticAssetsDir = path.join(rootDir, "static");
|
||||||
|
|
||||||
const watch = [pagesDir, staticAssetsDir];
|
const watch = [pagesDir, staticAssetsDir];
|
||||||
if (configPath) {
|
if (configPath != null) {
|
||||||
watch.push(path.relative(rootDir, configPath));
|
watch.push(path.relative(rootDir, configPath));
|
||||||
}
|
}
|
||||||
if (userConfig.watch != null) {
|
if (userConfig.watch != null) {
|
||||||
|
|
@ -97,5 +97,3 @@ export const loadConfig = async (rootDir: string): Promise<Config> => {
|
||||||
watch,
|
watch,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defineConfig = (userConfig: UserConfig): UserConfig => userConfig;
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Component, Element, HTMLElement } from "./component";
|
import { Component, Element, HTMLElement } from "./component";
|
||||||
import type { HTMLAttributes } from "./jsx";
|
import { HTMLAttributes } from "./jsx";
|
||||||
import { flatDeep } from "./utils";
|
import { flatDeep } from "./utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -38,7 +38,8 @@ export function createElement(
|
||||||
...children: Element[]
|
...children: Element[]
|
||||||
): HTMLElement;
|
): HTMLElement;
|
||||||
export function createElement(
|
export function createElement(
|
||||||
type: string | Component,
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type: string | Component<any>,
|
||||||
props: object | null,
|
props: object | null,
|
||||||
...children: Element[]
|
...children: Element[]
|
||||||
): HTMLElement {
|
): HTMLElement {
|
||||||
|
|
@ -67,7 +68,9 @@ export function createElement(
|
||||||
typeof value !== "number" &&
|
typeof value !== "number" &&
|
||||||
typeof value !== "boolean"
|
typeof value !== "boolean"
|
||||||
) {
|
) {
|
||||||
console.warn(`non-primitive attribute ${key} = ${JSON.stringify(value)}`);
|
console.warn(
|
||||||
|
`non-primitive attribute ${key} = ${JSON.stringify(value)}`,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
attrs[key] = value;
|
attrs[key] = value;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { Component, Fragment, HTMLElement } from "./component";
|
export { HTMLElement, Component, Fragment } from "./component";
|
||||||
export { defineConfig, UserConfig as Config } from "./config";
|
export { UserConfig as Config } from "./config";
|
||||||
export { createElement } from "./create-element";
|
export { createElement } from "./create-element";
|
||||||
export * from "./jsx";
|
export * from "./jsx";
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
import type { HTMLElement } from "./component";
|
import { HTMLElement } from "./component";
|
||||||
|
|
||||||
export interface RdfaAttributes {
|
export interface RdfaAttributes {
|
||||||
about?: string;
|
about?: string;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Element, HTMLElement } from "./component";
|
import { Element, HTMLElement } from "./component";
|
||||||
|
|
||||||
const HTML_ESCAPES: { [char: string]: string } = {
|
const HTML_ESCAPES: { [char: string]: string } = {
|
||||||
"&": "&",
|
"&": "&",
|
||||||
|
|
@ -71,7 +71,9 @@ const startTag = (elem: HTMLElement): string => {
|
||||||
if (attrValue === true) {
|
if (attrValue === true) {
|
||||||
output += ` ${normalizedAttrName}=""`;
|
output += ` ${normalizedAttrName}=""`;
|
||||||
} else {
|
} else {
|
||||||
output += ` ${normalizedAttrName}="${escapeAttr(attrValue.toString())}"`;
|
output += ` ${normalizedAttrName}="${escapeAttr(
|
||||||
|
attrValue.toString(),
|
||||||
|
)}"`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,8 +97,8 @@ const endTag = (elem: HTMLElement): string => {
|
||||||
* @return Fully rendered HTML document as a string.
|
* @return Fully rendered HTML document as a string.
|
||||||
*/
|
*/
|
||||||
export const renderPage = (rootElem: Element): string => {
|
export const renderPage = (rootElem: Element): string => {
|
||||||
if (rootElem == null) {
|
if (rootElem == undefined) {
|
||||||
throw new Error("root page element cannot be null");
|
throw new Error(`root page element cannot be null`);
|
||||||
}
|
}
|
||||||
if (typeof rootElem !== "object" || !("tag" in rootElem)) {
|
if (typeof rootElem !== "object" || !("tag" in rootElem)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,11 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isErrnoException } from "./error";
|
|
||||||
|
|
||||||
const resolveModulePath = (importPath: string): string | undefined => {
|
const resolveModulePath = (importPath: string): string | undefined => {
|
||||||
try {
|
try {
|
||||||
return require.resolve(importPath);
|
return require.resolve(importPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (
|
if (error.code === "MODULE_NOT_FOUND") {
|
||||||
error instanceof Error &&
|
|
||||||
isErrnoException(error) &&
|
|
||||||
error.code === "MODULE_NOT_FOUND"
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export const isErrnoException = (
|
|
||||||
error: Error,
|
|
||||||
): error is NodeJS.ErrnoException => "code" in error;
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "fs";
|
||||||
import * as path from "node:path";
|
import * as path from "path";
|
||||||
|
|
||||||
export { decacheModule } from "./decache-module";
|
export { decacheModule } from "./decache-module";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "fs";
|
||||||
import * as path from "node:path";
|
import * as path from "path";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
npmCmd,
|
npmCmd,
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,15 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "fs";
|
||||||
import * as path from "node:path";
|
import * as path from "path";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
npmCmd,
|
npmCmd,
|
||||||
runCommand,
|
runCommand,
|
||||||
|
wait,
|
||||||
WEBSNACKS_BIN_PATH,
|
WEBSNACKS_BIN_PATH,
|
||||||
WEBSNACKS_REPO_ROOT,
|
WEBSNACKS_REPO_ROOT,
|
||||||
wait,
|
|
||||||
withTempDir,
|
withTempDir,
|
||||||
} from "../helpers/e2e";
|
} from "../helpers/e2e";
|
||||||
import { testSuite } from "../lib";
|
import { testSuite } from "../lib";
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type ChildProcess, spawn } from "node:child_process";
|
import { ChildProcess, spawn } from "child_process";
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "fs";
|
||||||
import * as os from "node:os";
|
import * as os from "os";
|
||||||
import * as path from "node:path";
|
import * as path from "path";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a timeout and wait for at least the specified number of milliseconds,
|
* Set a timeout and wait for at least the specified number of milliseconds,
|
||||||
|
|
@ -137,7 +137,9 @@ export const runCommand = (
|
||||||
threwError = true;
|
threwError = true;
|
||||||
process.kill();
|
process.kill();
|
||||||
reject(
|
reject(
|
||||||
new Error(`max timeout of ${optionsWithDefaults.timeoutMs}ms reached`),
|
new Error(
|
||||||
|
`max timeout of ${optionsWithDefaults.timeoutMs}ms reached`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}, optionsWithDefaults.timeoutMs);
|
}, optionsWithDefaults.timeoutMs);
|
||||||
process.on("exit", (code) => {
|
process.on("exit", (code) => {
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export class Expect<T> {
|
||||||
public toEqual(expected: T): void {
|
public toEqual(expected: T): void {
|
||||||
if (!areEqual(this.value, expected)) {
|
if (!areEqual(this.value, expected)) {
|
||||||
throw new ExpectError(
|
throw new ExpectError(
|
||||||
"value does not equal expected",
|
`value does not equal expected`,
|
||||||
expected,
|
expected,
|
||||||
this.value,
|
this.value,
|
||||||
);
|
);
|
||||||
|
|
@ -71,7 +71,7 @@ export class StringExpect extends Expect<string> {
|
||||||
public toMatch(pattern: RegExp): void {
|
public toMatch(pattern: RegExp): void {
|
||||||
if (!this.value.match(pattern)) {
|
if (!this.value.match(pattern)) {
|
||||||
throw new ExpectError(
|
throw new ExpectError(
|
||||||
"value does not match expected pattern",
|
`value does not match expected pattern`,
|
||||||
pattern,
|
pattern,
|
||||||
this.value,
|
this.value,
|
||||||
);
|
);
|
||||||
|
|
@ -89,7 +89,7 @@ export class StringExpect extends Expect<string> {
|
||||||
public toStartWith(prefix: string): void {
|
public toStartWith(prefix: string): void {
|
||||||
if (!this.value.startsWith(prefix)) {
|
if (!this.value.startsWith(prefix)) {
|
||||||
throw new ExpectError(
|
throw new ExpectError(
|
||||||
"value does not start with expected prefix",
|
`value does not start with expected prefix`,
|
||||||
prefix,
|
prefix,
|
||||||
this.value,
|
this.value,
|
||||||
);
|
);
|
||||||
|
|
@ -107,7 +107,7 @@ export class StringExpect extends Expect<string> {
|
||||||
public toEndWith(suffix: string): void {
|
public toEndWith(suffix: string): void {
|
||||||
if (!this.value.endsWith(suffix)) {
|
if (!this.value.endsWith(suffix)) {
|
||||||
throw new ExpectError(
|
throw new ExpectError(
|
||||||
"value does not end with expected suffix",
|
`value does not end with expected suffix`,
|
||||||
suffix,
|
suffix,
|
||||||
this.value,
|
this.value,
|
||||||
);
|
);
|
||||||
|
|
@ -135,7 +135,11 @@ export class FunctionExpect<T> extends Expect<() => T> {
|
||||||
this.value();
|
this.value();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error instanceof 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)) {
|
if (!matches(error.message, pattern)) {
|
||||||
throw new ExpectError(
|
throw new ExpectError(
|
||||||
|
|
@ -147,7 +151,7 @@ export class FunctionExpect<T> extends Expect<() => T> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new ExpectError(
|
throw new ExpectError(
|
||||||
"function did not throw expected error",
|
`function did not throw expected error`,
|
||||||
pattern,
|
pattern,
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,9 @@ const runTest = async (test: Test): Promise<TestResult> => {
|
||||||
error:
|
error:
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error
|
? error
|
||||||
: new Error(`threw non-error object: ${displayValue(error)}`),
|
: new Error(
|
||||||
|
`threw non-error object: ${displayValue(error)}`,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export const shuffle = <T>(arr: T[]): void => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const areArraysEqual = <T>(a: T[], b: T[]): boolean => {
|
const areArraysEqual = <T>(a: T[], b: T[]): boolean => {
|
||||||
if (a.length !== b.length) {
|
if (a.length != b.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < a.length; i++) {
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fork } from "node:child_process";
|
import { fork } from "child_process";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "fs";
|
||||||
import * as path from "node:path";
|
import * as path from "path";
|
||||||
|
|
||||||
import { shuffle } from "./lib/utils";
|
import { shuffle } from "./lib/utils";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fork } from "node:child_process";
|
import { fork } from "child_process";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "fs";
|
||||||
import * as path from "node:path";
|
import * as path from "path";
|
||||||
|
|
||||||
import { shuffle } from "./lib/utils";
|
import { shuffle } from "./lib/utils";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type Component, createElement, Fragment } from "../../dist";
|
import { Component, createElement, Fragment } from "../../dist";
|
||||||
import { renderPage } from "../../dist/render";
|
import { renderPage } from "../../dist/render";
|
||||||
import { testSuite } from "../lib";
|
import { testSuite } from "../lib";
|
||||||
|
|
||||||
|
|
@ -20,7 +20,9 @@ testSuite("renderPage", ({ test, expect }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("escapes HTML in tag names", () => {
|
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(
|
expect(html).toEqual(
|
||||||
"<!DOCTYPE html><html><div></div></div></div></html>",
|
"<!DOCTYPE html><html><div></div></div></div></html>",
|
||||||
);
|
);
|
||||||
|
|
@ -55,7 +57,9 @@ testSuite("renderPage", ({ test, expect }) => {
|
||||||
|
|
||||||
test("renders text nodes", () => {
|
test("renders text nodes", () => {
|
||||||
const html = renderPage(<html>There are three lights!</html>);
|
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", () => {
|
test("renders spliced number nodes", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue