Compare commits

...

2 Commits

Author SHA1 Message Date
Teffen Ellis
57c3ceae77 web: Flesh out e2e. 2025-08-18 15:00:00 +02:00
Teffen Ellis
b58821cb49 web: Flesh out JSX components. 2025-08-17 17:54:30 +02:00
25 changed files with 9908 additions and 93 deletions

View File

@@ -0,0 +1,67 @@
import type { LitNode } from "../types/lit-jsx.js";
import { test as base } from "@playwright/experimental-ct-react";
import { Locator, Page } from "playwright/test";
export { expect } from "@playwright/test";
/* eslint-disable react-hooks/rules-of-hooks */
export interface InnerMountOptions {}
async function innerMount(page: Page, componentRef: unknown, options = {}) {
await page.waitForFunction(() => {
// @ts-ignore
return !!window.playwrightMount;
});
const selector = await page.evaluate(
async ({ component: component2 }) => {
let rootElement = document.getElementById("root");
if (!rootElement) {
rootElement = document.createElement("div");
rootElement.id = "root";
document.body.appendChild(rootElement);
}
rootElement.textContent = "Test 123";
return "#root >> internal:control=component";
},
{ component: componentRef },
);
return selector;
}
interface E2EFixturesTestScope {
render: (component: LitNode, options?: any) => Promise<Locator>;
}
interface E2EWorkerScope {
renderWorker: void;
}
export const test = base.extend<E2EFixturesTestScope, E2EWorkerScope>({
// renderWorker: [async ({ browser }, use, workerInfo) => {}, { scope: "worker" }],
render: async ({ page }, use) => {
await use(async (componentRef, options) => {
const selector = await innerMount(page, componentRef, options);
const locator = page.locator(selector);
// Object.assign(locator, {
// unmount: async () => {
// await locator.evaluate(async () => {
// const rootElement = document.getElementById("root");
// await window.playwrightUnmount(rootElement);
// });
// },
// update: async (options2) => {
// if (isJsxComponent(options2)) return await innerUpdate(page, options2);
// await innerUpdate(page, componentRef, options2);
// },
// });
return locator;
});
},
});

View File

View File

@@ -0,0 +1,39 @@
import { isCustomElementConstructor, isElementFactory } from "./predicates.js";
import { ComponentProps, createElement } from "./utils.js";
import type * as Lit from "@goauthentik/lit-jsx/types/lit-jsx.d.ts";
/**
* JSX factory for Lit elements.
*/
export function jsx(
elementLike: Lit.ElementType | Lit.ElementFactoryLike,
props: ComponentProps,
): Lit.LitNode {
console.log({ elementLike, props });
if (isElementFactory(elementLike)) {
if (isCustomElementConstructor(elementLike)) {
const tagName = customElements.getName(elementLike);
if (!tagName) {
throw new Error(`Custom element ${elementLike.name} is not registered`);
}
// Render the custom web component as any other html element.
return createElement(tagName, props);
}
return elementLike(props);
}
return createElement(elementLike, props);
}
export { jsx as jsxAttr, jsx as jsxDEV, jsx as jsxEscape, jsx as jsxs, jsx as jsxTemplate };
/**
* HTML Fragment factory for Lit elements.
*/
export function Fragment(fragment: Lit.Fragment): Lit.ElementType[] {
return Array.isArray(fragment.children) ? fragment.children : [fragment.children];
}

View File

@@ -0,0 +1,15 @@
/**
* Type predicate to check if a given value is a custom element constructor.
*/
export function isElementFactory(elementLike: unknown): elementLike is Lit.ElementFactoryLike {
return typeof elementLike === "function" && elementLike.prototype instanceof HTMLElement;
}
/**
* Type predicate to check if a given value is a custom element constructor.
*/
export function isCustomElementConstructor<P = unknown, T extends HTMLElement = HTMLElement>(
elementLike: Lit.ElementFactoryLike<P>,
): elementLike is Lit.CustomElementConstructor<P, T> {
return elementLike.prototype instanceof HTMLElement;
}

View File

@@ -0,0 +1,69 @@
import { spread } from "@open-wc/lit-helpers";
import { ifDefined } from "lit/directives/if-defined.js";
import { ref, RefOrCallback } from "lit/directives/ref.js";
import { styleMap } from "lit/directives/style-map.js";
import { html, unsafeStatic } from "lit/static-html.js";
export type CSSProperties = Record<string, string | number>;
/**
*
*/
export function parseProps(tagName: string, props: Record<PropertyKey, unknown>) {
const spreadable: Record<PropertyKey, unknown> = {};
const ElementConstructor = customElements.get(tagName);
for (const [propName, value] of Object.entries(props)) {
if (propName === "htmlFor") {
spreadable.for = value;
continue;
}
if (propName.startsWith("on")) {
const eventName = propName.slice(2).toLowerCase();
spreadable[`@${eventName}`] = value;
} else if (typeof value === "boolean") {
spreadable[`?${propName}`] = value;
} else if (ElementConstructor) {
spreadable[`.${propName}`] = value;
} else {
spreadable[`${propName}`] = value;
}
}
console.log(spreadable);
return spreadable;
}
export interface ComponentProps {
className?: string;
children?: unknown;
ref?: RefOrCallback;
style?: CSSProperties;
[key: PropertyKey]: unknown;
}
/**
*
* @returns
*/
export function createElement<P = unknown>(
tagName: string,
{ className, children, ref: refProp, style, ...props }: ComponentProps & P,
) {
const tag = unsafeStatic(tagName);
const result = html`
<${tag} class=${ifDefined(className)}
${ref(refProp)}
${spread(parseProps(tagName, props))}
style=${ifDefined(style ? styleMap(style) : null)}
>
${children}
</${tag}>
`;
return result;
}

8135
packages/lit-jsx/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
{
"name": "@goauthentik/lit-jsx",
"version": "1.0.0",
"description": "JSX Runtime for Lit",
"license": "MIT",
"scripts": {
"compile": "tsc -b",
"lint": "run-s lint:prettier:check lint:eslint:check",
"lint:eslint:check": "eslint .",
"lint:eslint:fix": "eslint --fix .",
"lint:fix": "run-s lint:prettier:fix lint:eslint:fix",
"lint:prettier": "eslint .",
"lint:prettier:check": "prettier --cache --check -u .",
"lint:prettier:fix": "prettier --cache --write -u .",
"test": "vitest"
},
"main": "tsconfig.json",
"type": "module",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./lib/types/lit-jsx.d.ts"
},
"./jsx-runtime": {
"import": "./out/lib/jsx-runtime.js",
"types": "./types/jsx-runtime.d.ts"
},
"./jsx-dev-runtime": {
"import": "./out/lib/jsx-runtime.js",
"types": "./types/jsx-runtime.d.ts"
},
"./types/*": {
"types": "./types/*"
}
},
"imports": {
"#e2e": "./e2e/index.ts",
"#e2e/*": "./e2e/*.ts"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@goauthentik/eslint-config": "^1.0.5",
"@goauthentik/prettier-config": "^3.1.0",
"@goauthentik/tsconfig": "^1.0.4",
"@lit-labs/ssr": "^3.3.1",
"@open-wc/lit-helpers": "^0.7.0",
"@playwright/experimental-ct-react": "^1.54.2",
"@playwright/test": "^1.54.2",
"@types/jsdom": "^21.1.7",
"@types/node": "^24.3.0",
"@typescript-eslint/eslint-plugin": "^8.39.1",
"@typescript-eslint/parser": "^8.39.1",
"dompurify": "^3.2.6",
"eslint": "^9.33.0",
"jsdom": "^26.1.0",
"lit": "^3.3.1",
"lit-html": "^3.3.1",
"npm-run-all": "^4.1.5",
"playwright": "^1.54.2",
"prettier": "^3.6.2",
"prettier-plugin-packagejson": "^2.5.19",
"trusted-types": "^2.0.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2",
"vitest": "^3.2.4"
},
"peerDependencies": {
"lit": "^3.3.1"
},
"engines": {
"node": ">=24"
},
"prettier": "@goauthentik/prettier-config",
"overrides": {
"format-imports": {
"eslint": "^9.31.0"
}
},
"publishConfig": {
"access": "public"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,43 @@
/**
* @file Playwright configuration.
*
* @see https://playwright.dev/docs/test-configuration
*
*/
import { defineConfig, devices } from "@playwright/test";
const CI = !!process.env.CI;
const baseURL = process.env.AK_TEST_RUNNER_PAGE_URL ?? "http://localhost:9000";
export default defineConfig({
testDir: "./test/browser",
fullyParallel: true,
forbidOnly: CI,
retries: CI ? 2 : 0,
workers: CI ? 1 : undefined,
reporter: CI
? "github"
: [
// ---
["list", { printSteps: true }],
["html", { open: "never" }],
],
use: {
testIdAttribute: "data-test-id",
baseURL,
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
],
});

View File

@@ -0,0 +1,6 @@
<html lang="en">
<body>
<div id="root"></div>
<script type="module" src="./index.ts"></script>
</body>
</html>

View File

@@ -0,0 +1 @@
console.log("Preparing Playwright");

View File

@@ -0,0 +1,12 @@
import { expect, test } from "#e2e";
test.describe("JSX Rendering", () => {
test("Simple static markup can be rendered", async ({ page, render }) => {
await render(
// @ts-ignore - testing
<p style={{ color: "red", fontSize: "12px" }}>Hello World</p>,
);
await expect(page.getByText("Test 123")).toBeVisible();
});
});

View File

@@ -0,0 +1,48 @@
/** @jsxImportSource @goauthentik/lit-jsx */
import { renderVariants } from "../utils.js";
import { test } from "vitest";
import { html } from "@lit-labs/ssr";
import { styleMap } from "lit/directives/style-map.js";
test("Simple static markup can be rendered", async ({ expect }) => {
const [result, comparision] = await renderVariants(
// @ts-ignore - testing
<div>Hello World</div>,
html`<div>Hello World</div>`,
);
expect(result, "JSX element serialized to a matching string").toBe(comparision);
});
test("`className` is rendered to the correct attribute", async ({ expect }) => {
const [result, comparision] = await renderVariants(
// @ts-ignore - testing
<p className="one two">Hello World</p>,
html`<p class="one two">Hello World</p>`,
);
expect(result, "`className` is rendered to the correct attribute").toBe(comparision);
});
test("`style` is rendered to the correct attribute", async ({ expect }) => {
const [result, comparision] = await renderVariants(
// @ts-ignore - testing
<p style={{ color: "red", fontSize: "12px" }}>Hello World</p>,
html`<p style=${styleMap({ color: "red", fontSize: "12px" })}>Hello World</p>`,
);
expect(result, "style is rendered to the correct attribute").toBe(comparision);
});
test("`style` is rendered to the correct attribute", async ({ expect }) => {
const [result, comparision] = await renderVariants(
// @ts-ignore - testing
<p style={{ color: "red", fontSize: "12px" }}>Hello World</p>,
html`<p style=${styleMap({ color: "red", fontSize: "12px" })}>Hello World</p>`,
);
expect(result, "style is rendered to the correct attribute").toBe(comparision);
});

View File

@@ -0,0 +1,36 @@
import type * as Lit from "@goauthentik/lit-jsx/types/lit-jsx.d.ts";
import createDOMPurify, { Config as DOMPurifyConfig, WindowLike } from "dompurify";
import { JSDOM } from "jsdom";
import { format } from "prettier";
import { render, ServerRenderedTemplate } from "@lit-labs/ssr";
import { collectResult } from "@lit-labs/ssr/lib/render-result.js";
export class Purifier {
#window = new JSDOM("").window;
#DOMPurify = createDOMPurify(this.#window as WindowLike);
public sanitize(html: string, config?: DOMPurifyConfig) {
return this.#DOMPurify.sanitize(html, config);
}
}
const purifier = new Purifier();
export async function renderStaticLit(value: unknown) {
const result = await collectResult(render(value));
const sanitized = purifier.sanitize(result);
const formatted = await format(sanitized, {
parser: "html",
});
return formatted.trim();
}
export function renderVariants(
...inputs: Array<ServerRenderedTemplate | Lit.LitNode>
): Promise<string[]> {
return Promise.all(inputs.map((input) => renderStaticLit(input)));
}

View File

@@ -0,0 +1,15 @@
{
"extends": "@goauthentik/tsconfig",
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"checkJs": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"]
},
"exclude": [
// ---
"**/out/**/*",
"**/dist/**/*",
"storybook-static"
]
}

75
packages/lit-jsx/types/jsx-runtime.d.ts vendored Normal file
View File

@@ -0,0 +1,75 @@
/**
* @file JSX runtime types for Lit.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
// /* eslint-disable @typescript-eslint/no-empty-object-type */
// /// <reference types="./lit-jsx-runtime.d.ts" />
// /**
// * JSX runtime types for Lit.
// */
// export namespace JSX {
// // type ElementType = Lit.JSX.ElementType;
// // interface Element extends Lit.JSX.Element {}
// // interface ElementClass extends Lit.JSX.ElementClass {}
// // interface ElementAttributesProperty extends Lit.JSX.ElementAttributesProperty {}
// // interface ElementChildrenAttribute extends Lit.JSX.ElementChildrenAttribute {}
// // type LibraryManagedAttributes<C, P> = Lit.JSX.LibraryManagedAttributes<C, P>;
// // interface IntrinsicAttributes extends Lit.JSX.IntrinsicAttributes {}
// // interface IntrinsicClassAttributes<T> extends Lit.JSX.IntrinsicClassAttributes<T> {}
// interface IntrinsicElements extends Lit.JSX.IntrinsicElements {}
// }
import { JSX } from "./lit-jsx.js";
import { Attributes, ComponentChild, ComponentChildren, ComponentType, VNode } from "preact";
export { Fragment } from "preact";
export function jsx(
type: string,
props: JSX.HTMLAttributes &
JSX.SVGAttributes &
Record<string, any> & { children?: ComponentChild },
key?: string,
): VNode<any>;
export function jsx<P>(
type: ComponentType<P>,
props: Attributes & P & { children?: ComponentChild },
key?: string,
): VNode<any>;
export function jsxs(
type: string,
props: JSX.HTMLAttributes &
JSX.SVGAttributes &
Record<string, any> & { children?: ComponentChild[] },
key?: string,
): VNode<any>;
export function jsxs<P>(
type: ComponentType<P>,
props: Attributes & P & { children?: ComponentChild[] },
key?: string,
): VNode<any>;
export function jsxDEV(
type: string,
props: JSX.HTMLAttributes &
JSX.SVGAttributes &
Record<string, any> & { children?: ComponentChildren },
key?: string,
): VNode<any>;
export function jsxDEV<P>(
type: ComponentType<P>,
props: Attributes & P & { children?: ComponentChildren },
key?: string,
): VNode<any>;
// These are not expected to be used manually, but by a JSX transform
export function jsxTemplate(template: string[], ...expressions: any[]): VNode<any>;
export function jsxAttr(name: string, value: any): string | null;
export function jsxEscape<T>(value: T): string | null | VNode<any> | Array<string | null | VNode>;
export { JSX };

318
packages/lit-jsx/types/lit-jsx.d.ts vendored Normal file
View File

@@ -0,0 +1,318 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* @file JSX runtime types for Lit.
*/
import { nothing, TemplateResult } from "lit";
export = Lit;
export as namespace Lit;
declare namespace Lit {
namespace JSX {
interface Element {
type: string;
}
interface IntrinsicElements {
div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
}
}
/**
* A constructable element which returns a valid HTMLElement.
*/
type CustomElementConstructor<P = unknown, T extends HTMLElement = HTMLElement> = new (
...args: unknown[]
) => T;
/**
* A function which given a props object, returns a Lit element.
*/
type ElementFactory<P = unknown> = (props: P) => ElementType;
/**
* Either a constructor, or a function which returns an HTML element.
*/
type ElementFactoryLike<P = unknown> = CustomElementConstructor<P> | ElementFactory<P>;
type ElementType<
P = unknown,
Tag extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements,
> =
| { [K in Tag]: P extends JSX.IntrinsicElements[K] ? K : never }[Tag]
| CustomElementConstructor<P>;
interface Fragment {
children: Lit.ElementType | Lit.ElementType[];
}
type LitNode = JSX.Element | ElementType | string | TemplateResult | typeof nothing;
type FC<P = unknown> = (props: P) => LitNode | LitNode[];
// Keep in sync with JSX namespace in ./jsx-runtime.d.ts and ./jsx-dev-runtime.d.ts
}
// Users who only use Preact for SSR might not specify "dom" in their lib in tsconfig.json
// import * as preact from "preact";
// type Defaultize<Props, Defaults> =
// // Distribute over unions
// Props extends any // Make any properties included in Default optional
// ? Partial<Pick<Props, Extract<keyof Props, keyof Defaults>>> & // Include the remaining properties from Props
// Pick<Props, Exclude<keyof Props, keyof Defaults>>
// : never;
// declare namespace JSX {
// export type LibraryManagedAttributes<Component, Props> = Component extends {
// defaultProps: infer Defaults;
// }
// ? Defaultize<Props, Defaults>
// : Props;
// export interface IntrinsicAttributes {
// key?: any;
// }
// export type ElementType<P = any> =
// | {
// [K in keyof IntrinsicElements]: P extends IntrinsicElements[K] ? K : never;
// }[keyof IntrinsicElements]
// | preact.ComponentType<P>;
// export interface Element extends preact.VNode<any> {}
// export type ElementClass = preact.Component<any, any> | preact.FunctionComponent<any>;
// export interface ElementAttributesProperty {
// props: any;
// }
// export interface ElementChildrenAttribute {
// children: any;
// }
// export interface IntrinsicSVGElements {
// svg: preact.SVGAttributes<SVGSVGElement>;
// animate: preact.SVGAttributes<SVGAnimateElement>;
// circle: preact.SVGAttributes<SVGCircleElement>;
// animateMotion: preact.SVGAttributes<SVGAnimateMotionElement>;
// animateTransform: preact.SVGAttributes<SVGAnimateTransformElement>;
// clipPath: preact.SVGAttributes<SVGClipPathElement>;
// defs: preact.SVGAttributes<SVGDefsElement>;
// desc: preact.SVGAttributes<SVGDescElement>;
// ellipse: preact.SVGAttributes<SVGEllipseElement>;
// feBlend: preact.SVGAttributes<SVGFEBlendElement>;
// feColorMatrix: preact.SVGAttributes<SVGFEColorMatrixElement>;
// feComponentTransfer: preact.SVGAttributes<SVGFEComponentTransferElement>;
// feComposite: preact.SVGAttributes<SVGFECompositeElement>;
// feConvolveMatrix: preact.SVGAttributes<SVGFEConvolveMatrixElement>;
// feDiffuseLighting: preact.SVGAttributes<SVGFEDiffuseLightingElement>;
// feDisplacementMap: preact.SVGAttributes<SVGFEDisplacementMapElement>;
// feDistantLight: preact.SVGAttributes<SVGFEDistantLightElement>;
// feDropShadow: preact.SVGAttributes<SVGFEDropShadowElement>;
// feFlood: preact.SVGAttributes<SVGFEFloodElement>;
// feFuncA: preact.SVGAttributes<SVGFEFuncAElement>;
// feFuncB: preact.SVGAttributes<SVGFEFuncBElement>;
// feFuncG: preact.SVGAttributes<SVGFEFuncGElement>;
// feFuncR: preact.SVGAttributes<SVGFEFuncRElement>;
// feGaussianBlur: preact.SVGAttributes<SVGFEGaussianBlurElement>;
// feImage: preact.SVGAttributes<SVGFEImageElement>;
// feMerge: preact.SVGAttributes<SVGFEMergeElement>;
// feMergeNode: preact.SVGAttributes<SVGFEMergeNodeElement>;
// feMorphology: preact.SVGAttributes<SVGFEMorphologyElement>;
// feOffset: preact.SVGAttributes<SVGFEOffsetElement>;
// fePointLight: preact.SVGAttributes<SVGFEPointLightElement>;
// feSpecularLighting: preact.SVGAttributes<SVGFESpecularLightingElement>;
// feSpotLight: preact.SVGAttributes<SVGFESpotLightElement>;
// feTile: preact.SVGAttributes<SVGFETileElement>;
// feTurbulence: preact.SVGAttributes<SVGFETurbulenceElement>;
// filter: preact.SVGAttributes<SVGFilterElement>;
// foreignObject: preact.SVGAttributes<SVGForeignObjectElement>;
// g: preact.SVGAttributes<SVGGElement>;
// image: preact.SVGAttributes<SVGImageElement>;
// line: preact.SVGAttributes<SVGLineElement>;
// linearGradient: preact.SVGAttributes<SVGLinearGradientElement>;
// marker: preact.SVGAttributes<SVGMarkerElement>;
// mask: preact.SVGAttributes<SVGMaskElement>;
// metadata: preact.SVGAttributes<SVGMetadataElement>;
// mpath: preact.SVGAttributes<SVGMPathElement>;
// path: preact.SVGAttributes<SVGPathElement>;
// pattern: preact.SVGAttributes<SVGPatternElement>;
// polygon: preact.SVGAttributes<SVGPolygonElement>;
// polyline: preact.SVGAttributes<SVGPolylineElement>;
// radialGradient: preact.SVGAttributes<SVGRadialGradientElement>;
// rect: preact.SVGAttributes<SVGRectElement>;
// set: preact.SVGAttributes<SVGSetElement>;
// stop: preact.SVGAttributes<SVGStopElement>;
// switch: preact.SVGAttributes<SVGSwitchElement>;
// symbol: preact.SVGAttributes<SVGSymbolElement>;
// text: preact.SVGAttributes<SVGTextElement>;
// textPath: preact.SVGAttributes<SVGTextPathElement>;
// tspan: preact.SVGAttributes<SVGTSpanElement>;
// use: preact.SVGAttributes<SVGUseElement>;
// view: preact.SVGAttributes<SVGViewElement>;
// }
// export interface IntrinsicMathMLElements {
// "annotation": preact.AnnotationMathMLAttributes<MathMLElement>;
// "annotation-xml": preact.AnnotationXmlMathMLAttributes<MathMLElement>;
// /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/maction */
// "maction": preact.MActionMathMLAttributes<MathMLElement>;
// "math": preact.MathMathMLAttributes<MathMLElement>;
// /** This feature is non-standard. See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/menclose */
// "menclose": preact.MEncloseMathMLAttributes<MathMLElement>;
// "merror": preact.MErrorMathMLAttributes<MathMLElement>;
// /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mfenced */
// "mfenced": preact.MFencedMathMLAttributes<MathMLElement>;
// "mfrac": preact.MFracMathMLAttributes<MathMLElement>;
// "mi": preact.MiMathMLAttributes<MathMLElement>;
// "mmultiscripts": preact.MmultiScriptsMathMLAttributes<MathMLElement>;
// "mn": preact.MNMathMLAttributes<MathMLElement>;
// "mo": preact.MOMathMLAttributes<MathMLElement>;
// "mover": preact.MOverMathMLAttributes<MathMLElement>;
// "mpadded": preact.MPaddedMathMLAttributes<MathMLElement>;
// "mphantom": preact.MPhantomMathMLAttributes<MathMLElement>;
// "mprescripts": preact.MPrescriptsMathMLAttributes<MathMLElement>;
// "mroot": preact.MRootMathMLAttributes<MathMLElement>;
// "mrow": preact.MRowMathMLAttributes<MathMLElement>;
// "ms": preact.MSMathMLAttributes<MathMLElement>;
// "mspace": preact.MSpaceMathMLAttributes<MathMLElement>;
// "msqrt": preact.MSqrtMathMLAttributes<MathMLElement>;
// "mstyle": preact.MStyleMathMLAttributes<MathMLElement>;
// "msub": preact.MSubMathMLAttributes<MathMLElement>;
// "msubsup": preact.MSubsupMathMLAttributes<MathMLElement>;
// "msup": preact.MSupMathMLAttributes<MathMLElement>;
// "mtable": preact.MTableMathMLAttributes<MathMLElement>;
// "mtd": preact.MTdMathMLAttributes<MathMLElement>;
// "mtext": preact.MTextMathMLAttributes<MathMLElement>;
// "mtr": preact.MTrMathMLAttributes<MathMLElement>;
// "munder": preact.MUnderMathMLAttributes<MathMLElement>;
// "munderover": preact.MUnderMathMLAttributes<MathMLElement>;
// "semantics": preact.SemanticsMathMLAttributes<MathMLElement>;
// }
// export interface IntrinsicHTMLElements {
// a: preact.AccessibleAnchorHTMLAttributes<HTMLAnchorElement>;
// abbr: preact.HTMLAttributes<HTMLElement>;
// address: preact.HTMLAttributes<HTMLElement>;
// area: preact.AccessibleAreaHTMLAttributes<HTMLAreaElement>;
// article: preact.ArticleHTMLAttributes<HTMLElement>;
// aside: preact.AsideHTMLAttributes<HTMLElement>;
// audio: preact.AudioHTMLAttributes<HTMLAudioElement>;
// b: preact.HTMLAttributes<HTMLElement>;
// base: preact.BaseHTMLAttributes<HTMLBaseElement>;
// bdi: preact.HTMLAttributes<HTMLElement>;
// bdo: preact.HTMLAttributes<HTMLElement>;
// big: preact.HTMLAttributes<HTMLElement>;
// blockquote: preact.BlockquoteHTMLAttributes<HTMLQuoteElement>;
// body: preact.HTMLAttributes<HTMLBodyElement>;
// br: preact.BrHTMLAttributes<HTMLBRElement>;
// button: preact.ButtonHTMLAttributes<HTMLButtonElement>;
// canvas: preact.CanvasHTMLAttributes<HTMLCanvasElement>;
// caption: preact.CaptionHTMLAttributes<HTMLTableCaptionElement>;
// cite: preact.HTMLAttributes<HTMLElement>;
// code: preact.HTMLAttributes<HTMLElement>;
// col: preact.ColHTMLAttributes<HTMLTableColElement>;
// colgroup: preact.ColgroupHTMLAttributes<HTMLTableColElement>;
// data: preact.DataHTMLAttributes<HTMLDataElement>;
// datalist: preact.DataListHTMLAttributes<HTMLDataListElement>;
// dd: preact.DdHTMLAttributes<HTMLElement>;
// del: preact.DelHTMLAttributes<HTMLModElement>;
// details: preact.DetailsHTMLAttributes<HTMLDetailsElement>;
// dfn: preact.HTMLAttributes<HTMLElement>;
// dialog: preact.DialogHTMLAttributes<HTMLDialogElement>;
// div: preact.HTMLAttributes<HTMLDivElement>;
// dl: preact.DlHTMLAttributes<HTMLDListElement>;
// dt: preact.DtHTMLAttributes<HTMLElement>;
// em: preact.HTMLAttributes<HTMLElement>;
// embed: preact.EmbedHTMLAttributes<HTMLEmbedElement>;
// fieldset: preact.FieldsetHTMLAttributes<HTMLFieldSetElement>;
// figcaption: preact.FigcaptionHTMLAttributes<HTMLElement>;
// figure: preact.HTMLAttributes<HTMLElement>;
// footer: preact.FooterHTMLAttributes<HTMLElement>;
// form: preact.FormHTMLAttributes<HTMLFormElement>;
// h1: preact.HeadingHTMLAttributes<HTMLHeadingElement>;
// h2: preact.HeadingHTMLAttributes<HTMLHeadingElement>;
// h3: preact.HeadingHTMLAttributes<HTMLHeadingElement>;
// h4: preact.HeadingHTMLAttributes<HTMLHeadingElement>;
// h5: preact.HeadingHTMLAttributes<HTMLHeadingElement>;
// h6: preact.HeadingHTMLAttributes<HTMLHeadingElement>;
// head: preact.HeadHTMLAttributes<HTMLHeadElement>;
// header: preact.HeaderHTMLAttributes<HTMLElement>;
// hgroup: preact.HTMLAttributes<HTMLElement>;
// hr: preact.HrHTMLAttributes<HTMLHRElement>;
// html: preact.HtmlHTMLAttributes<HTMLHtmlElement>;
// i: preact.HTMLAttributes<HTMLElement>;
// iframe: preact.IframeHTMLAttributes<HTMLIFrameElement>;
// img: preact.AccessibleImgHTMLAttributes<HTMLImageElement>;
// input: preact.AccessibleInputHTMLAttributes<HTMLInputElement>;
// ins: preact.InsHTMLAttributes<HTMLModElement>;
// kbd: preact.HTMLAttributes<HTMLElement>;
// keygen: preact.KeygenHTMLAttributes<HTMLUnknownElement>;
// label: preact.LabelHTMLAttributes<HTMLLabelElement>;
// legend: preact.LegendHTMLAttributes<HTMLLegendElement>;
// li: preact.LiHTMLAttributes<HTMLLIElement>;
// link: preact.LinkHTMLAttributes<HTMLLinkElement>;
// main: preact.MainHTMLAttributes<HTMLElement>;
// map: preact.MapHTMLAttributes<HTMLMapElement>;
// mark: preact.HTMLAttributes<HTMLElement>;
// marquee: preact.MarqueeHTMLAttributes<HTMLMarqueeElement>;
// menu: preact.MenuHTMLAttributes<HTMLMenuElement>;
// menuitem: preact.HTMLAttributes<HTMLUnknownElement>;
// meta: preact.MetaHTMLAttributes<HTMLMetaElement>;
// meter: preact.MeterHTMLAttributes<HTMLMeterElement>;
// nav: preact.NavHTMLAttributes<HTMLElement>;
// noscript: preact.NoScriptHTMLAttributes<HTMLElement>;
// object: preact.ObjectHTMLAttributes<HTMLObjectElement>;
// ol: preact.OlHTMLAttributes<HTMLOListElement>;
// optgroup: preact.OptgroupHTMLAttributes<HTMLOptGroupElement>;
// option: preact.OptionHTMLAttributes<HTMLOptionElement>;
// output: preact.OutputHTMLAttributes<HTMLOutputElement>;
// p: preact.HTMLAttributes<HTMLParagraphElement>;
// param: preact.ParamHTMLAttributes<HTMLParamElement>;
// picture: preact.PictureHTMLAttributes<HTMLPictureElement>;
// pre: preact.HTMLAttributes<HTMLPreElement>;
// progress: preact.ProgressHTMLAttributes<HTMLProgressElement>;
// q: preact.QuoteHTMLAttributes<HTMLQuoteElement>;
// rp: preact.HTMLAttributes<HTMLElement>;
// rt: preact.HTMLAttributes<HTMLElement>;
// ruby: preact.HTMLAttributes<HTMLElement>;
// s: preact.HTMLAttributes<HTMLElement>;
// samp: preact.HTMLAttributes<HTMLElement>;
// script: preact.ScriptHTMLAttributes<HTMLScriptElement>;
// search: preact.SearchHTMLAttributes<HTMLElement>;
// section: preact.HTMLAttributes<HTMLElement>;
// select: preact.AccessibleSelectHTMLAttributes<HTMLSelectElement>;
// slot: preact.SlotHTMLAttributes<HTMLSlotElement>;
// small: preact.HTMLAttributes<HTMLElement>;
// source: preact.SourceHTMLAttributes<HTMLSourceElement>;
// span: preact.HTMLAttributes<HTMLSpanElement>;
// strong: preact.HTMLAttributes<HTMLElement>;
// style: preact.StyleHTMLAttributes<HTMLStyleElement>;
// sub: preact.HTMLAttributes<HTMLElement>;
// summary: preact.HTMLAttributes<HTMLElement>;
// sup: preact.HTMLAttributes<HTMLElement>;
// table: preact.TableHTMLAttributes<HTMLTableElement>;
// tbody: preact.HTMLAttributes<HTMLTableSectionElement>;
// td: preact.TdHTMLAttributes<HTMLTableCellElement>;
// template: preact.TemplateHTMLAttributes<HTMLTemplateElement>;
// textarea: preact.TextareaHTMLAttributes<HTMLTextAreaElement>;
// tfoot: preact.HTMLAttributes<HTMLTableSectionElement>;
// th: preact.ThHTMLAttributes<HTMLTableCellElement>;
// thead: preact.HTMLAttributes<HTMLTableSectionElement>;
// time: preact.TimeHTMLAttributes<HTMLTimeElement>;
// title: preact.TitleHTMLAttributes<HTMLTitleElement>;
// tr: preact.HTMLAttributes<HTMLTableRowElement>;
// track: preact.TrackHTMLAttributes<HTMLTrackElement>;
// u: preact.UlHTMLAttributes<HTMLElement>;
// ul: preact.HTMLAttributes<HTMLUListElement>;
// var: preact.HTMLAttributes<HTMLElement>;
// video: preact.VideoHTMLAttributes<HTMLVideoElement>;
// wbr: preact.WbrHTMLAttributes<HTMLElement>;
// }
// export interface IntrinsicElements
// extends IntrinsicSVGElements,
// IntrinsicMathMLElements,
// IntrinsicHTMLElements {}
// }

View File

@@ -0,0 +1,12 @@
/// <reference types="vitest/config" />
import { defineConfig } from "vite";
export default defineConfig({
test: {
name: "unit",
environment: "node",
dir: "./test",
include: ["./**/*.test.{ts,tsx}"],
},
});

893
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -98,6 +98,7 @@
"@goauthentik/core": "^1.0.0",
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
"@goauthentik/eslint-config": "^1.0.5",
"@goauthentik/lit-jsx": "^1.0.0",
"@goauthentik/prettier-config": "^3.1.0",
"@goauthentik/tsconfig": "^1.0.4",
"@hcaptcha/types": "^1.0.4",

View File

@@ -21,6 +21,7 @@ import diffGrammar from "highlight.js/lib/languages/diff";
import confGrammar from "highlight.js/lib/languages/ini";
import nginxGrammar from "highlight.js/lib/languages/nginx";
import { common } from "lowlight";
import React from "react";
import { createRoot, Root } from "react-dom/client";
import * as runtime from "react/jsx-runtime";
import rehypeHighlight, { Options as HighlightOptions } from "rehype-highlight";
@@ -225,15 +226,17 @@ export class AKMDX extends AKElement {
const { frontmatter = {} } = mdxExports;
this.#reactRoot.render(
<MDXModuleContext.Provider value={mdxModule}>
<Content
frontmatter={frontmatter}
components={{
React.createElement(
MDXModuleContext.Provider,
{ value: mdxModule },
React.createElement(Content, {
frontmatter,
components: {
wrapper: MDXWrapper,
a: MDXAnchor,
}}
/>
</MDXModuleContext.Provider>,
},
}),
),
);
}
}

View File

@@ -49,15 +49,15 @@ export const MDXAnchor = ({
});
};
return (
<a
href={href}
onClick={interceptHeadingLinks}
rel="noopener noreferrer"
target="_blank"
{...props}
>
{children}
</a>
return React.createElement(
"a",
{
href,
onClick: interceptHeadingLinks,
rel: "noopener noreferrer",
target: "_blank",
...props,
},
children,
);
};

View File

@@ -13,8 +13,8 @@ export const MDXWrapper = ({ children, frontmatter }: MDXWrapperProps) => {
const nextChildren = React.Children.toArray(children);
if (title) {
nextChildren.unshift(<h1 key="header-title">{title}</h1>);
nextChildren.unshift(React.createElement("h1", { key: "header-title" }, title));
}
return <div className="pf-c-content">{nextChildren}</div>;
return React.createElement("div", { className: "pf-c-content" }, nextChildren);
};

View File

@@ -54,19 +54,6 @@ export type LitPropertyRecord<T extends object> = {
*/
export type LitPropertyKey<K> = K extends string ? `.${K}` | `?${K}` | K : K;
/**
* A React-like functional component. Used to render a component in a template.
*
* @template P The type of the props object.
* @param props The props object.
* @param children The children to render.
* @returns The rendered template.
*/
export type LitFC<P> = (
props: P,
children?: SlottedTemplateResult,
) => SlottedTemplateResult | SlottedTemplateResult[];
//#endregion
//#region Host/Controller

View File

@@ -14,6 +14,8 @@
"module": "esnext",
"moduleResolution": "bundler",
"baseUrl": ".",
"jsx": "react-jsx",
"jsxImportSource": "@goauthentik/lit-jsx",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
// TODO: We should enable this when when we're ready to enforce it.
"noUncheckedIndexedAccess": false,