From f96834299af5db40ab703d04c287367a3f44944d Mon Sep 17 00:00:00 2001 From: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:32:58 +0100 Subject: [PATCH] web: replace JSDoc types with TypeScript sources for build tooling Node runs TypeScript directly now, so the build scripts, bundler plugins, and `@goauthentik/core` no longer need to be JavaScript annotated with JSDoc. Every module reachable from `scripts/build-web.ts` is now real TypeScript, run under Node's type stripping. The package.json `imports`/`exports` maps point at the `.ts` sources instead of pairing a `.js` runtime entry with a generated `.d.ts` under `out/`, so types resolve on a clean checkout without a declaration build first. Type stripping does not rewrite `.js` specifiers back to `.ts`, so relative imports name the real file on disk and `web/tsconfig.json` enables `allowImportingTsExtensions`. Two suppressions in the locale scripts turned out to be a real soundness gap rather than the `@lit/localize-tools` bug the tsconfig comment claimed. `Config["output"]` is a union, and `outputDir` is only required on its runtime variant, so the unnarrowed access was legitimately `string | undefined`. `build-locales` only works in runtime mode, so it now validates `output.mode` up front the way it already validated `interchange.format`; the narrowing that follows removes both the cast and the `@ts-expect-error`, and a misconfigured `lit-localize.json` fails with a clear message instead of a TypeError inside the library. Regenerating all 18 locale modules produces byte-identical output. Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> --- packages/theme/package.json | 2 +- web/.storybook/main.js | 2 +- web/AGENTS.md | 2 +- .../mdx-plugin/{compile.js => compile.ts} | 44 ++++++------ web/bundler/mdx-plugin/{node.js => node.ts} | 52 ++++++-------- .../mdx-plugin/{rehype.js => rehype.ts} | 22 +++--- .../mdx-plugin/{remark.js => remark.ts} | 42 +++++------ .../style-loader-plugin/{node.js => node.ts} | 49 ++++++------- web/bundler/utils/{node.js => node.ts} | 11 ++- .../vite-plugin-lit-css/{node.js => node.ts} | 10 ++- web/logger/{browser.js => browser.ts} | 71 +++++++------------ web/logger/{node.js => node.ts} | 56 ++++++--------- web/logger/{transport.js => transport.ts} | 9 +-- web/package.json | 40 +++-------- .../environment/load/{node.js => node.ts} | 0 .../core/environment/{node.js => node.ts} | 38 +++++----- web/packages/core/id/{index.js => index.ts} | 10 ++- web/packages/core/{index.js => index.ts} | 0 web/packages/core/package.json | 39 +++------- web/packages/core/paths/{node.js => node.ts} | 19 ++--- web/packages/core/promises/index.js | 27 ------- web/packages/core/promises/index.ts | 24 +++++++ .../core/scripting/{node.js => node.ts} | 6 +- web/packages/core/tsconfig.json | 20 ++++-- .../core/version/{node.js => node.ts} | 22 +++--- web/paths/{index.js => index.ts} | 0 web/paths/{node.js => node.ts} | 26 +++---- .../{build-assets.mjs => build-assets.ts} | 14 +--- .../{build-locales.mjs => build-locales.ts} | 68 +++++++----------- web/scripts/{build-web.mjs => build-web.ts} | 45 ++++-------- .../{pseudolocalize.mjs => pseudolocalize.ts} | 34 +++------ ...tities.mjs => unescape-locale-entities.ts} | 43 +++++------ web/test/unit/tsconfig.json | 7 +- .../unit/unescape-locale-entities.test.ts | 2 +- web/tsconfig.json | 9 ++- 35 files changed, 348 insertions(+), 517 deletions(-) rename web/bundler/mdx-plugin/{compile.js => compile.ts} (76%) rename web/bundler/mdx-plugin/{node.js => node.ts} (75%) rename web/bundler/mdx-plugin/{rehype.js => rehype.ts} (90%) rename web/bundler/mdx-plugin/{remark.js => remark.ts} (79%) rename web/bundler/style-loader-plugin/{node.js => node.ts} (88%) rename web/bundler/utils/{node.js => node.ts} (82%) rename web/bundler/vite-plugin-lit-css/{node.js => node.ts} (79%) rename web/logger/{browser.js => browser.ts} (61%) rename web/logger/{node.js => node.ts} (55%) rename web/logger/{transport.js => transport.ts} (57%) rename web/packages/core/environment/load/{node.js => node.ts} (100%) rename web/packages/core/environment/{node.js => node.ts} (59%) rename web/packages/core/id/{index.js => index.ts} (79%) rename web/packages/core/{index.js => index.ts} (100%) rename web/packages/core/paths/{node.js => node.ts} (67%) delete mode 100644 web/packages/core/promises/index.js create mode 100644 web/packages/core/promises/index.ts rename web/packages/core/scripting/{node.js => node.ts} (86%) rename web/packages/core/version/{node.js => node.ts} (84%) rename web/paths/{index.js => index.ts} (100%) rename web/paths/{node.js => node.ts} (83%) rename web/scripts/{build-assets.mjs => build-assets.ts} (94%) rename web/scripts/{build-locales.mjs => build-locales.ts} (84%) rename web/scripts/{build-web.mjs => build-web.ts} (90%) rename web/scripts/{pseudolocalize.mjs => pseudolocalize.ts} (72%) rename web/scripts/{unescape-locale-entities.mjs => unescape-locale-entities.ts} (88%) diff --git a/packages/theme/package.json b/packages/theme/package.json index 2a65cb7b49e5..454fedee8600 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -178,7 +178,7 @@ "#exports-comment": [ "The TypeScript entry points resolve to src/ for tooling that can read", "TypeScript directly — tsc via `types`, node via jiti/tsgo, and bundlers", - "that opt into the `bundler` condition (see web/scripts/build-web.mjs).", + "that opt into the `bundler` condition (see web/scripts/build-web.ts).", "`default` keeps the built dist/ output for plain Node and any consumer", "that does not set a condition; publishConfig.exports points every entry", "at dist/ for the published tarball.", diff --git a/web/.storybook/main.js b/web/.storybook/main.js index 1421011be06c..6c78e10446bc 100644 --- a/web/.storybook/main.js +++ b/web/.storybook/main.js @@ -3,7 +3,7 @@ * @import { StorybookConfig } from "@storybook/web-components-vite"; */ -import { copyAssets } from "../scripts/build-assets.mjs"; +import { copyAssets } from "../scripts/build-assets.ts"; /** * @param {TemplateStringsArray} strings diff --git a/web/AGENTS.md b/web/AGENTS.md index bbf51a261977..1f48de689eab 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -91,7 +91,7 @@ scripts/ # Build scripts (esbuild config, localization) - `src/elements/Interface.ts` — Base interface class with context management - `src/common/global.ts` — Global authentik config and state - `src/flow/FlowExecutor.ts` — Flow execution engine -- `scripts/build-web.mjs` — Main ESBuild configuration +- `scripts/build-web.ts` — Main ESBuild configuration ### Conventions diff --git a/web/bundler/mdx-plugin/compile.js b/web/bundler/mdx-plugin/compile.ts similarity index 76% rename from web/bundler/mdx-plugin/compile.js rename to web/bundler/mdx-plugin/compile.ts index c6eb18c469d1..93373644906a 100644 --- a/web/bundler/mdx-plugin/compile.js +++ b/web/bundler/mdx-plugin/compile.ts @@ -6,15 +6,16 @@ * styles and expose CSS parts (`title`, `content`) to host pages. */ -import { rehypeAnchors, rehypeMermaid } from "./rehype.js"; +import { rehypeAnchors, rehypeMermaid } from "./rehype.ts"; import { normalizeAdmonitionLabels, remarkAdmonition, remarkHeadings, remarkLists, -} from "./remark.js"; +} from "./remark.ts"; import GithubSlugger from "github-slugger"; +import type { Element, Root } from "hast"; import { toHtml } from "hast-util-to-html"; import apacheGrammar from "highlight.js/lib/languages/apache"; import diffGrammar from "highlight.js/lib/languages/diff"; @@ -33,11 +34,11 @@ import { parse as parseYAML } from "yaml"; /** * Pull a YAML frontmatter block off the top of `source` and return both * pieces. Returns an empty object if there is no frontmatter. - * - * @param {string} source - * @returns {{ body: string, frontmatter: Record }} */ -function splitFrontmatter(source) { +function splitFrontmatter(source: string): { + body: string; + frontmatter: Record; +} { const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); if (!match) return { body: source, frontmatter: {} }; const frontmatter = parseYAML(match[1]) || {}; @@ -51,13 +52,11 @@ function splitFrontmatter(source) { * escaping rules — no hand-rolled `&`/`<`/`>`/`"` replacement that has * to be remembered and audited separately. * - * @param {import('hast').Element[]} bodyChildren Hast nodes from the markdown pipeline. - * @param {string | null} title Frontmatter title, or `null` to omit the `

`. - * @returns {string} + * @param bodyChildren Hast nodes from the markdown pipeline. + * @param title Frontmatter title, or `null` to omit the `

`. */ -function renderEnvelope(bodyChildren, title) { - /** @type {import('hast').Element[]} */ - const children = []; +function renderEnvelope(bodyChildren: Element[], title: string | null): string { + const children: Element["children"] = []; if (title) { children.push({ @@ -70,8 +69,7 @@ function renderEnvelope(bodyChildren, title) { children.push(...bodyChildren); - /** @type {import('hast').Root} */ - const root = { + const root: Root = { type: "root", children: [ { @@ -91,12 +89,14 @@ function renderEnvelope(bodyChildren, title) { * frontmatter. Used by the build-time plugin; the runtime side mirrors * this pipeline in the browser for admin-supplied prose. * - * @param {string} source - * @param {string} publicDirectory Path of the file's directory inside the - * docs site, used to resolve relative `` hrefs at build time. - * @returns {Promise<{ html: string, frontmatter: Record }>} + * @param source The markdown source. + * @param publicDirectory Path of the file's directory inside the docs site, + * used to resolve relative `` hrefs at build time. */ -export async function compileMarkdown(source, publicDirectory) { +export async function compileMarkdown( + source: string, + publicDirectory: string, +): Promise<{ html: string; frontmatter: Record }> { const { body: rawBody, frontmatter } = splitFrontmatter(source); const body = normalizeAdmonitionLabels(rawBody); const slugger = new GithubSlugger(); @@ -125,12 +125,10 @@ export async function compileMarkdown(source, publicDirectory) { }) .use(rehypeMermaid); - const tree = /** @type {import('hast').Root} */ ( - await processor.run(processor.parse(body), body) - ); + const tree = (await processor.run(processor.parse(body), body)) as Root; const title = typeof frontmatter.title === "string" ? frontmatter.title : null; - const html = renderEnvelope(/** @type {import('hast').Element[]} */ (tree.children), title); + const html = renderEnvelope(tree.children as Element[], title); return { html, frontmatter }; } diff --git a/web/bundler/mdx-plugin/node.js b/web/bundler/mdx-plugin/node.ts similarity index 75% rename from web/bundler/mdx-plugin/node.js rename to web/bundler/mdx-plugin/node.ts index 808f7f395150..a7a225c2db96 100644 --- a/web/bundler/mdx-plugin/node.js +++ b/web/bundler/mdx-plugin/node.ts @@ -12,51 +12,43 @@ * over the existing fetch-then-set-innerHTML path used by ``. The * shape is `{ content, frontmatter, publicPath, publicDirectory }` where * `content` is now pre-rendered HTML rather than raw markdown source. - * - * @import { - * OnLoadArgs, - * OnLoadResult, - * OnResolveArgs, - * OnResolveResult, - * Plugin, - * PluginBuild - * } from "esbuild" */ import * as fs from "node:fs/promises"; import * as path from "node:path"; -import { compileMarkdown } from "./compile.js"; +import { compileMarkdown } from "./compile.ts"; import { MonoRepoRoot } from "@goauthentik/core/paths/node"; +import type { + OnLoadArgs, + OnLoadResult, + OnResolveArgs, + OnResolveResult, + Plugin, + PluginBuild, +} from "esbuild"; + const pluginName = "mdx-plugin"; -/** - * @typedef MDXPluginOptions - * @property {string} root Repository root. - */ +export interface MDXPluginOptions { + /** + * Repository root. + */ + root: string; +} /** * Bundle markdown and MDX source into JSON modules. - * - * @param {MDXPluginOptions} options - * @returns {Plugin} */ -export function mdxPlugin({ root }) { +export function mdxPlugin({ root }: MDXPluginOptions): Plugin { const prefix = "~docs"; // TODO: Replace with `resolvePackage` after NPM Workspaces support is added. const docsPackageRoot = path.resolve(MonoRepoRoot, "website"); - /** - * @param {PluginBuild} build - */ - function setup(build) { - /** - * @param {OnResolveArgs} args - * @returns {Promise} - */ - async function resolveListener(args) { + function setup(build: PluginBuild) { + async function resolveListener(args: OnResolveArgs): Promise { if (!args.path.startsWith("~")) return args; return { @@ -65,11 +57,7 @@ export function mdxPlugin({ root }) { }; } - /** - * @param {OnLoadArgs} args - * @returns {Promise} - */ - async function loadListener(args) { + async function loadListener(args: OnLoadArgs): Promise { const source = String(await fs.readFile(args.path)); const publicPath = path.resolve( diff --git a/web/bundler/mdx-plugin/rehype.js b/web/bundler/mdx-plugin/rehype.ts similarity index 90% rename from web/bundler/mdx-plugin/rehype.js rename to web/bundler/mdx-plugin/rehype.ts index b180c8675e8d..f7bb9a50960f 100644 --- a/web/bundler/mdx-plugin/rehype.js +++ b/web/bundler/mdx-plugin/rehype.ts @@ -4,6 +4,7 @@ import { CurrentReleaseDocsURL } from "@goauthentik/core/version/node"; +import type { Element, Root } from "hast"; import { SKIP, visit } from "unist-util-visit"; /** @@ -11,12 +12,8 @@ import { SKIP, visit } from "unist-util-visit"; * runtime `MDXAnchor` used: take a `./...` href relative to the file's * `publicDirectory`, drop trailing `index`/`.md`/`.mdx`, and absolutize * against {@linkcode CurrentReleaseDocsURL}. - * - * @param {string} href - * @param {string} publicDirectory - * @returns {string} */ -function resolveDocsHref(href, publicDirectory) { +function resolveDocsHref(href: string, publicDirectory: string): string { // `new URL(...)` against `file:///` lets us reuse the browser-style // path resolver while preserving the hash and any query string. const joined = `${publicDirectory}/${href}`.replace(/\/{2,}/g, "/"); @@ -28,6 +25,10 @@ function resolveDocsHref(href, publicDirectory) { return next.toString(); } +export interface RehypeAnchorsOptions { + publicDirectory: string; +} + /** * Rehype plugin: resolve relative anchors at build time and wrap every * `` in an `` light-DOM custom element. The wrapper attaches @@ -40,11 +41,9 @@ function resolveDocsHref(href, publicDirectory) { * ``'s shadow tree where the existing PatternFly link CSS in * `styles.css` applies. The wrapper itself uses `display: contents` so * it does not perturb inline-flow layout. - * - * @param {{ publicDirectory: string }} options */ -export function rehypeAnchors({ publicDirectory }) { - return (/** @type {import('hast').Root} */ tree) => { +export function rehypeAnchors({ publicDirectory }: RehypeAnchorsOptions) { + return (tree: Root) => { visit(tree, "element", (node) => { if (node.tagName !== "a") return; @@ -69,8 +68,7 @@ export function rehypeAnchors({ publicDirectory }) { // the visitor from descending into the freshly-stamped // child anchor (which would re-match this filter and // recurse forever). - /** @type {import('hast').Element} */ - const original = { + const original: Element = { type: "element", tagName: "a", properties: { ...props }, @@ -93,7 +91,7 @@ export function rehypeAnchors({ publicDirectory }) { * wrapper element is needed. */ export function rehypeMermaid() { - return (/** @type {import('hast').Root} */ tree) => { + return (tree: Root) => { visit(tree, "element", (node) => { if (node.tagName !== "pre") return; const child = node.children?.[0]; diff --git a/web/bundler/mdx-plugin/remark.js b/web/bundler/mdx-plugin/remark.ts similarity index 79% rename from web/bundler/mdx-plugin/remark.js rename to web/bundler/mdx-plugin/remark.ts index b4e38b45cf66..7b8f1f546673 100644 --- a/web/bundler/mdx-plugin/remark.js +++ b/web/bundler/mdx-plugin/remark.ts @@ -6,6 +6,8 @@ * either pipeline grows a new transform. */ +import type GithubSlugger from "github-slugger"; +import type { Root } from "mdast"; import { visit } from "unist-util-visit"; const ADMONITIONS = new Set(["info", "warning", "danger", "note", "caution", "tip"]); @@ -14,7 +16,7 @@ const ADMONITIONS = new Set(["info", "warning", "danger", "note", "caution", "ti * `caution` and `tip` aren't first-class PatternFly alert levels — map * them onto the closest equivalent so PFAlert styles render correctly. */ -const ADMONITION_LEVEL = { +const ADMONITION_LEVEL: Record = { info: "pf-m-info", warning: "pf-m-warning", danger: "pf-m-danger", @@ -38,11 +40,7 @@ const ADMONITION_BARE_LABEL_RE = new RegExp( "gm", ); -/** - * @param {string} source - * @returns {string} - */ -export function normalizeAdmonitionLabels(source) { +export function normalizeAdmonitionLabels(source: string): string { return source.replace(ADMONITION_BARE_LABEL_RE, "$1[$2]"); } @@ -54,7 +52,7 @@ export function normalizeAdmonitionLabels(source) { * element inside the slot. */ export function remarkAdmonition() { - return (/** @type {import('mdast').Root} */ tree) => { + return (tree: Root) => { visit(tree, (node) => { if ( node.type !== "containerDirective" && @@ -71,12 +69,11 @@ export function remarkAdmonition() { data.hProperties = { ...(data.hProperties || {}), ...(node.attributes || {}), - level: - /** @type {Record} */ (ADMONITION_LEVEL)[node.name] ?? - `pf-m-${node.name}`, + level: ADMONITION_LEVEL[node.name] ?? `pf-m-${node.name}`, }; - const children = /** @type {any[]} */ (node.children || []); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const children = (node.children || []) as any[]; const labelIndex = children.findIndex( (c) => c.type === "paragraph" && c.data?.directiveLabel, ); @@ -91,31 +88,24 @@ export function remarkAdmonition() { }; } -/** - * @typedef {object} RemarkHeadingsOptions - * @property {import("github-slugger").default} slugger - */ +export interface RemarkHeadingsOptions { + slugger: GithubSlugger; +} /** * Remark plugin: heading slugs into `id` attributes. * * Uses `github-slugger` to match the anchor IDs Docusaurus generates for the * same content. - * - * @param {RemarkHeadingsOptions} options */ -export function remarkHeadings({ slugger }) { - /** - * @param {{ value?: string, children?: any[] }} n - * @returns {string} - */ - const flatten = (n) => { +export function remarkHeadings({ slugger }: RemarkHeadingsOptions) { + const flatten = (n: { value?: string; children?: unknown[] }): string => { if (n.value) return n.value; - if (n.children) return n.children.map(flatten).join(""); + if (n.children) return n.children.map((child) => flatten(child as typeof n)).join(""); return ""; }; - return (/** @type {import('mdast').Root} */ tree) => { + return (tree: Root) => { visit(tree, "heading", (node) => { const id = slugger.slug(flatten(node)); const data = node.data || (node.data = {}); @@ -128,7 +118,7 @@ export function remarkHeadings({ slugger }) { * Remark plugin: tag lists with PatternFly's content class. */ export function remarkLists() { - return (/** @type {import('mdast').Root} */ tree) => { + return (tree: Root) => { visit(tree, "list", (node) => { const data = node.data || (node.data = {}); data.hProperties = { diff --git a/web/bundler/style-loader-plugin/node.js b/web/bundler/style-loader-plugin/node.ts similarity index 88% rename from web/bundler/style-loader-plugin/node.js rename to web/bundler/style-loader-plugin/node.ts index b3420bffa136..7f1171d2f04d 100644 --- a/web/bundler/style-loader-plugin/node.js +++ b/web/bundler/style-loader-plugin/node.ts @@ -1,8 +1,5 @@ /** * @file MDX plugin for ESBuild. - * - * @import { Plugin, PluginBuild, BuildContext, BuildOptions } from "esbuild" - * @import { BaseLogger } from "pino" */ import { readFile } from "node:fs/promises"; @@ -13,29 +10,30 @@ import { ConsoleLogger } from "#logger/node"; import { resolvePackage } from "@goauthentik/core/paths/node"; -const CSSNamespace = /** @type {const} */ ({ +import type { BuildContext, BuildOptions, Plugin, PluginBuild } from "esbuild"; +import type { BaseLogger } from "pino"; + +const CSSNamespace = { Global: "css-global", Process: "css-process", Bundled: "css-bundled", -}); +} as const; -/** - * @typedef StyleLoaderPluginOptions - * - * @property {boolean} [watch] Whether to watch for file changes. - * @property {BaseLogger} [logger] - */ +export interface StyleLoaderPluginOptions { + /** + * Whether to watch for file changes. + */ + watch?: boolean; + logger?: BaseLogger; +} /** * Selectively apply the ESBuild `css` loader. - * - * @param {StyleLoaderPluginOptions} [options] - * @returns {Plugin} */ export function styleLoaderPlugin({ watch = false, logger = ConsoleLogger.child({ name: "style-loader-plugin" }), -} = {}) { +}: StyleLoaderPluginOptions = {}): Plugin { const patternflyPath = resolvePackage("@patternfly/patternfly", import.meta); const require = createRequire(import.meta.url); @@ -44,9 +42,8 @@ export function styleLoaderPlugin({ * * This is necessary because Patternfly's CSS references fonts via relative paths * that ESBuild cannot resolve automatically. - * @type {Parameters} */ - const fontResolverArgs = [ + const fontResolverArgs: Parameters = [ { filter: /\.woff2?$/ }, async (args) => { if (!args.resolveDir.startsWith(patternflyPath)) { @@ -64,10 +61,7 @@ export function styleLoaderPlugin({ setup(build) { const { absWorkingDir = process.cwd() } = build.initialOptions; - /** - * @type {Map} - */ - const disposables = new Map(); + const disposables = new Map(); build.onDispose(async () => { for (const [filePath, ctx] of disposables) { @@ -98,6 +92,8 @@ export function styleLoaderPlugin({ namespace: CSSNamespace.Bundled, }; } + + return undefined; }); /** @@ -108,7 +104,7 @@ export function styleLoaderPlugin({ build.onLoad({ filter: /.*/, namespace: CSSNamespace.Process }, async (args) => { return { contents: await readFile(args.path, "utf8"), - loader: "css", + loader: "css" as const, resolveDir: dirname(args.path), }; }); @@ -122,10 +118,7 @@ export function styleLoaderPlugin({ const cssContent = await readFile(args.path, "utf8"); let context = disposables.get(args.path); - /** - * @type {BuildOptions} - */ - const buildOptions = { + const buildOptions: BuildOptions = { stdin: { contents: cssContent, resolveDir: dirname(args.path), @@ -168,7 +161,7 @@ export function styleLoaderPlugin({ return { contents: bundledCSS, - loader: "text", + loader: "text" as const, }; } @@ -192,7 +185,7 @@ export function styleLoaderPlugin({ return { contents: bundledCSS, - loader: "text", + loader: "text" as const, watchFiles, }; }); diff --git a/web/bundler/utils/node.js b/web/bundler/utils/node.ts similarity index 82% rename from web/bundler/utils/node.js rename to web/bundler/utils/node.ts index 6be336eeec60..2b56b5befefb 100644 --- a/web/bundler/utils/node.js +++ b/web/bundler/utils/node.ts @@ -14,13 +14,10 @@ import { * Creates a mapping of environment variables to their respective runtime constants. */ export function createBundleDefinitions() { - const SerializedNodeEnvironment = /** @type {`"development"` | `"production"`} */ ( - JSON.stringify(NodeEnvironment) - ); + const SerializedNodeEnvironment = JSON.stringify(NodeEnvironment) as + | `"development"` + | `"production"`; - /** - * @satisfies {Record} - */ const envRecord = { AK_VERSION: AuthentikVersion, AK_DOCS_URL: CurrentReleaseDocsURL.href, @@ -28,7 +25,7 @@ export function createBundleDefinitions() { AK_DOCS_PRE_RELEASE_URL: PreReleaseDocsURL.href, AK_API_BASE_PATH: process.env.AK_API_BASE_PATH ?? "", AK_BUNDLER: JSON.stringify(process.env.AK_BUNDLER ?? "authentik"), - }; + } satisfies Record; return { ...serializeEnvironmentVars(envRecord), diff --git a/web/bundler/vite-plugin-lit-css/node.js b/web/bundler/vite-plugin-lit-css/node.ts similarity index 79% rename from web/bundler/vite-plugin-lit-css/node.js rename to web/bundler/vite-plugin-lit-css/node.ts index f655e2e05010..925fe97d3e86 100644 --- a/web/bundler/vite-plugin-lit-css/node.js +++ b/web/bundler/vite-plugin-lit-css/node.ts @@ -1,18 +1,16 @@ /** * @file Vite plugin to inline CSS imports - * @import { Plugin as VitePlugin } from "vite"; */ +import type { Plugin as VitePlugin } from "vite"; + const CSSImportPattern = /import [\w$]+ from .+\.(css)/g; const JavaScriptFilePattern = /\.m?(js|ts|tsx)$/; export function inlineCSSPlugin() { - /** - * @satisfies {VitePlugin} - */ const inlineCSSPlugin = { name: "inline-css-plugin", - transform: (source, id) => { + transform: (source: string, id: string) => { if (!JavaScriptFilePattern.test(id)) return; const code = source.replace(CSSImportPattern, (match) => { @@ -23,7 +21,7 @@ export function inlineCSSPlugin() { code, }; }, - }; + } satisfies VitePlugin; return inlineCSSPlugin; } diff --git a/web/logger/browser.js b/web/logger/browser.ts similarity index 61% rename from web/logger/browser.js rename to web/logger/browser.ts index e11fbe80598c..83ef17e20b30 100644 --- a/web/logger/browser.js +++ b/web/logger/browser.ts @@ -14,34 +14,31 @@ //#region Functions -/** - * @typedef {object} Logger - * @property {typeof console.info} info; - * @property {typeof console.warn} warn; - * @property {typeof console.error} error; - * @property {typeof console.debug} debug; - * @property {typeof console.trace} trace; - */ +export interface Logger { + info: typeof console.info; + warn: typeof console.warn; + error: typeof console.error; + debug: typeof console.debug; + trace: typeof console.trace; +} /** * Labels log levels in the browser console. */ -const LogLevelLabel = /** @type {const} */ ({ +const LogLevelLabel = { info: "[INFO]", warn: "[WARN]", error: "[ERROR]", debug: "[DEBUG]", trace: "[TRACE]", -}); +} as const; -/** - * @typedef {keyof typeof LogLevelLabel} LogLevel - */ +export type LogLevel = keyof typeof LogLevelLabel; /** * Predefined log levels. */ -const LogLevels = /** @type {LogLevel[]} */ (Object.keys(LogLevelLabel)); +const LogLevels = Object.keys(LogLevelLabel) as LogLevel[]; /** * Colors for log levels in the browser console. @@ -51,29 +48,21 @@ const LogLevels = /** @type {LogLevel[]} */ (Object.keys(LogLevelLabel)); * The colors are derived from Carbon Design System's palette to ensure * sufficient contrast and accessibility across light and dark themes. */ -const LogLevelColors = /** @type {const} */ ({ +const LogLevelColors = { info: `light-dark(#0043CE, #4589FF)`, warn: `light-dark(#F1C21B, #F1C21B)`, error: `light-dark(#DA1E28, #FA4D56)`, debug: `light-dark(#8A3FFC, #A56EFF)`, trace: `light-dark(#8A3FFC, #A56EFF)`, -}); +} as const; /** * Creates a logger with the given prefix. - * - * @param {string} [prefix] - * @param {...string[]} args - * @returns {Logger} - * */ -export function createLogger(prefix, ...args) { +export function createLogger(prefix?: string, ...args: string[][]): Logger { const suffix = prefix ? `(${prefix}):` : ":"; - /** - * @type {Partial} - */ - const logger = {}; + const logger: Partial = {}; for (const level of LogLevels) { const label = LogLevelLabel[level]; @@ -89,47 +78,39 @@ export function createLogger(prefix, ...args) { ); } - return /** @type {Logger} */ (logger); + return logger as Logger; } //#endregion //#region Console Logger -/** - * @typedef {Logger & {prefix: (logPrefix: string) => Logger}} IConsoleLogger - */ +export type IConsoleLogger = Logger & { prefix: (logPrefix: string) => Logger }; /** * A singleton logger instance for the browser. * - * ```js + * ```ts * import { ConsoleLogger } from "#logger/browser"; * * ConsoleLogger.info("Hello, world!"); * ``` * - * @implements {IConsoleLogger} * @runtime browser */ -// @ts-expect-error Logging properties are dynamically assigned. export class ConsoleLogger { - /** @type {typeof console.info} */ - static info; - /** @type {typeof console.warn} */ - static warn; - /** @type {typeof console.error} */ - static error; - /** @type {typeof console.debug} */ - static debug; - /** @type {typeof console.trace} */ - static trace; + // Assigned below via `Object.assign` so the browser reports the caller's + // call site rather than a wrapper's. + declare static info: typeof console.info; + declare static warn: typeof console.warn; + declare static error: typeof console.error; + declare static debug: typeof console.debug; + declare static trace: typeof console.trace; /** * Creates a logger with the given prefix. - * @param {string} logPrefix */ - static prefix(logPrefix) { + static prefix(logPrefix: string): Logger { return createLogger(logPrefix); } } diff --git a/web/logger/node.js b/web/logger/node.ts similarity index 55% rename from web/logger/node.js rename to web/logger/node.ts index 663913954900..625205ff2c2d 100644 --- a/web/logger/node.js +++ b/web/logger/node.ts @@ -1,11 +1,9 @@ /** * Application logger. - * - * @import { LoggerOptions, Logger, Level, ChildLoggerOptions } from "pino" - * @import { PrettyOptions } from "pino-pretty" */ -import { pino } from "pino"; +import { type ChildLoggerOptions, type Level, type Logger, type LoggerOptions, pino } from "pino"; +import type { PrettyOptions } from "pino-pretty"; //#region Constants @@ -13,18 +11,17 @@ import { pino } from "pino"; * Default options for creating a Pino logger. * * @category Logger - * @satisfies {LoggerOptions} */ export const DEFAULT_PINO_LOGGER_OPTIONS = { enabled: true, level: "info", transport: { - target: "./transport.js", - options: /** @satisfies {PrettyOptions} */ ({ + target: "./transport.ts", + options: { colorize: true, - }), + } satisfies PrettyOptions, }, -}; +} satisfies LoggerOptions; //#endregion @@ -32,24 +29,19 @@ export const DEFAULT_PINO_LOGGER_OPTIONS = { /** * Read the log level from the environment. - * @return {Level} */ -export function readLogLevel() { +export function readLogLevel(): Level { return process.env.AK_LOG_LEVEL || DEFAULT_PINO_LOGGER_OPTIONS.level; } -/** - * @typedef {Logger} FixtureLogger - */ +export type FixtureLogger = Logger; -/** - * @this {Logger} - * @param {string} fixtureName - * @param {string} [testName] - * @param {ChildLoggerOptions} [options] - * @returns {FixtureLogger} - */ -function createFixtureLogger(fixtureName, testName, options) { +function createFixtureLogger( + this: Logger, + fixtureName: string, + testName?: string, + options?: ChildLoggerOptions, +): FixtureLogger { return this.child( { name: fixtureName }, { @@ -59,28 +51,24 @@ function createFixtureLogger(fixtureName, testName, options) { ); } -/** - * @typedef {object} CustomLoggerMethods - * @property {typeof createFixtureLogger} fixture - */ +export interface CustomLoggerMethods { + fixture: typeof createFixtureLogger; +} -/** - * @typedef {Logger & CustomLoggerMethods} ConsoleLogger - */ +export type ConsoleLogger = Logger & CustomLoggerMethods; /** * A singleton logger instance for Node.js. * - * ```js + * ```ts * import { ConsoleLogger } from "#logger/node"; * * ConsoleLogger.info("Hello, world!"); * ``` * * @runtime node - * @type {ConsoleLogger} */ -export const ConsoleLogger = Object.assign( +export const ConsoleLogger: ConsoleLogger = Object.assign( pino({ ...DEFAULT_PINO_LOGGER_OPTIONS, level: readLogLevel(), @@ -88,9 +76,7 @@ export const ConsoleLogger = Object.assign( { fixture: createFixtureLogger }, ); -/** - * @typedef {ReturnType} ChildConsoleLogger - */ +export type ChildConsoleLogger = ReturnType; //#region Aliases diff --git a/web/logger/transport.js b/web/logger/transport.ts similarity index 57% rename from web/logger/transport.js rename to web/logger/transport.ts index f2d5f0cd263d..6a50455d1d38 100644 --- a/web/logger/transport.js +++ b/web/logger/transport.ts @@ -1,15 +1,10 @@ /** * @file Pretty transport for Pino - * - * @import { PrettyOptions } from "pino-pretty" */ -import PinoPretty from "pino-pretty"; +import PinoPretty, { type PrettyOptions } from "pino-pretty"; -/** - * @param {PrettyOptions} options - */ -function prettyTransporter(options) { +function prettyTransporter(options: PrettyOptions) { const pretty = PinoPretty({ ...options, ignore: "pid,hostname", diff --git a/web/package.json b/web/package.json index e1838a70748c..799d90ab4eb2 100644 --- a/web/package.json +++ b/web/package.json @@ -6,9 +6,9 @@ "scripts": { "build": "wireit", "build:sfe": "pnpm --filter @goauthentik/web-sfe run build", - "build-locales": "node scripts/build-locales.mjs", + "build-locales": "node scripts/build-locales.ts", "build-proxy": "wireit", - "bundler:watch": "node scripts/build-web.mjs --watch", + "bundler:watch": "node scripts/build-web.ts --watch", "extract-locales": "lit-localize extract", "format": "wireit", "lint": "eslint --fix .", @@ -19,7 +19,7 @@ "precommit": "wireit", "prettier": "prettier --cache --write -u .", "prettier-check": "prettier --cache --check -u .", - "pseudolocalize": "node ./scripts/pseudolocalize.mjs", + "pseudolocalize": "node ./scripts/pseudolocalize.ts", "storybook": "storybook dev -p 6006", "storybook:build": "wireit", "test": "vitest", @@ -38,18 +38,9 @@ "./locales/*": "./src/locales/*", "./user/*": "./src/user/*", "./admin/*": "./src/admin/*", - "./*/browser": { - "types": "./out/*/browser.d.ts", - "import": "./*/browser.js" - }, - "./*/node": { - "types": "./out/*/node.d.ts", - "import": "./*/node.js" - }, - "./*": { - "types": "./out/*/index.d.ts", - "import": "./*/index.js" - } + "./*/browser": "./*/browser.ts", + "./*/node": "./*/node.ts", + "./*": "./*/index.ts" }, "imports": { "#styles/*.css": "./src/styles/*.css", @@ -72,18 +63,9 @@ "#e2e": "./e2e/index.ts", "#e2e/*": "./e2e/*.ts", "#types/*": "./types/*/index.d.ts", - "#*/browser": { - "types": "./out/*/browser.d.ts", - "import": "./*/browser.js" - }, - "#*/node": { - "types": "./out/*/node.d.ts", - "import": "./*/node.js" - }, - "#*": { - "types": "./out/*/index.d.ts", - "import": "./*/index.js" - } + "#*/browser": "./*/browser.ts", + "#*/node": "./*/node.ts", + "#*": "./*/index.ts" }, "dependencies": { "@codemirror/lang-css": "^6.3.1", @@ -211,7 +193,7 @@ }, "wireit": { "build": { - "command": "${NODE_RUNNER} scripts/build-web.mjs", + "command": "${NODE_RUNNER} scripts/build-web.ts", "dependencies": [ "build-locales" ], @@ -249,7 +231,7 @@ } }, "build-proxy": { - "command": "node scripts/build-web.mjs --styles-only", + "command": "node scripts/build-web.ts --styles-only", "dependencies": [ "build-locales" ] diff --git a/web/packages/core/environment/load/node.js b/web/packages/core/environment/load/node.ts similarity index 100% rename from web/packages/core/environment/load/node.js rename to web/packages/core/environment/load/node.ts diff --git a/web/packages/core/environment/node.js b/web/packages/core/environment/node.ts similarity index 59% rename from web/packages/core/environment/node.js rename to web/packages/core/environment/node.ts index 3732a9e7b52c..1b9550fb3ae8 100644 --- a/web/packages/core/environment/node.js +++ b/web/packages/core/environment/node.ts @@ -20,16 +20,28 @@ export const NodeEnvironment = process.env.NODE_ENV || "development"; /** * A source environment variable, which can be a string, number, boolean, null, or undefined. - * @typedef {string | number | boolean | null | undefined} EnvironmentVariable */ +export type EnvironmentVariable = string | number | boolean | null | undefined; /** * A type helper for serializing environment variables. * * @category Environment - * @template {EnvironmentVariable} T - * @typedef {T extends string ? `"${T}"` : T} JSONify */ +export type JSONify = T extends string ? `"${T}"` : T; + +/** + * A mapping of environment variable names to their source values. + */ +export type EnvRecord = Record; + +/** + * The result of serializing an {@linkcode EnvRecord}, i.e. each key prefixed and + * each value replaced with its JSON representation. + */ +export type SerializedEnvRecord = { + [K in keyof R & string as `${Prefix}${K}`]: JSONify; +}; //#endregion @@ -43,26 +55,16 @@ export const NodeEnvironment = process.env.NODE_ENV || "development"; * * @category Environment * @runtime node - * - * @typeParam {string} [Prefix='import.meta.env.'] - * - * @typedef {Record} EnvRecord - * @typedef {string} Prefix - * - * @param {EnvRecord} input - * @param {Prefix} [prefix='import.meta.env.'] - * - * @returns {{[K in keyof EnvRecord as `${Prefix}${K}`]: JSONify}} */ -export function serializeEnvironmentVars( - input, - prefix = /** @type {Prefix} */ ("import.meta.env."), -) { +export function serializeEnvironmentVars< + R extends EnvRecord, + Prefix extends string = "import.meta.env.", +>(input: R, prefix: Prefix = "import.meta.env." as Prefix): SerializedEnvRecord { const env = Object.fromEntries( Object.entries(input).map(([key, value]) => [prefix + key, JSON.stringify(value ?? "")]), ); - return /** @type {any} */ (env); + return env as SerializedEnvRecord; } //#endregion diff --git a/web/packages/core/id/index.js b/web/packages/core/id/index.ts similarity index 79% rename from web/packages/core/id/index.js rename to web/packages/core/id/index.ts index d8475bb37705..166e0c090b21 100644 --- a/web/packages/core/id/index.js +++ b/web/packages/core/id/index.ts @@ -20,9 +20,9 @@ export class IDGenerator { * This ID will be unique for the lifetime of the page and will not be * exposed on the `window` object. * - * @param {string | number} [name] An optional name to use for the element. + * @param name An optional name to use for the element. */ - static elementID(name) { + static elementID(name?: string | number): string { name = name || ++this.#elementIndex; return "«ak-" + name + "»"; @@ -31,7 +31,7 @@ export class IDGenerator { /** * Create a new ID. */ - static next() { + static next(): number { this.#sequenceIndex += 1; return this.#sequenceIndex; @@ -39,10 +39,8 @@ export class IDGenerator { /** * Generate a random ID in hexadecimal format. - * - * @param {number} [characterLength] */ - static randomID(characterLength = 6) { + static randomID(characterLength = 6): string { const bytes = crypto.getRandomValues(new Uint8Array(characterLength / 2)); return Array.from(bytes, (a) => a.toString(16)).join(""); diff --git a/web/packages/core/index.js b/web/packages/core/index.ts similarity index 100% rename from web/packages/core/index.js rename to web/packages/core/index.ts diff --git a/web/packages/core/package.json b/web/packages/core/package.json index f2d84755af67..080fdb4c5420 100644 --- a/web/packages/core/package.json +++ b/web/packages/core/package.json @@ -7,41 +7,20 @@ "scripts": { "build": "tsc -p ." }, - "main": "index.js", + "main": "./index.ts", "type": "module", - "types": "./out/index.d.ts", + "types": "./index.ts", "exports": { "./package.json": "./package.json", - "./*/browser": { - "types": "./out/*/browser.d.ts", - "import": "./*/browser.js" - }, - "./*/node": { - "types": "./out/*/node.d.ts", - "import": "./*/node.js" - }, - "./*": { - "types": "./out/*/index.d.ts", - "import": "./*/index.js" - }, - ".": { - "types": "./out/index.d.ts", - "import": "./index.js" - } + "./*/browser": "./*/browser.ts", + "./*/node": "./*/node.ts", + "./*": "./*/index.ts", + ".": "./index.ts" }, "imports": { - "#*/browser": { - "types": "./out/*/browser.d.ts", - "import": "./*/browser.js" - }, - "#*/node": { - "types": "./out/*/node.d.ts", - "import": "./*/node.js" - }, - "#*": { - "types": "./out/*/index.d.ts", - "import": "./*/index.js" - } + "#*/browser": "./*/browser.ts", + "#*/node": "./*/node.ts", + "#*": "./*/index.ts" }, "dependencies": { "@goauthentik/tsconfig": "^2.0.0", diff --git a/web/packages/core/paths/node.js b/web/packages/core/paths/node.ts similarity index 67% rename from web/packages/core/paths/node.js rename to web/packages/core/paths/node.ts index 77ec9b1ca3d3..57c5a5cbd582 100644 --- a/web/packages/core/paths/node.js +++ b/web/packages/core/paths/node.ts @@ -4,36 +4,31 @@ import { fileURLToPath } from "node:url"; const relativeDirname = dirname(fileURLToPath(import.meta.url)); -/** - * @typedef {'~authentik'} MonoRepoRoot - */ +export type MonoRepoRoot = "~authentik"; /** * The root of the authentik monorepo. * * @runtime node */ -export const MonoRepoRoot = /** @type {MonoRepoRoot} */ ( - resolve(relativeDirname, "..", "..", "..", "..") -); +export const MonoRepoRoot = resolve(relativeDirname, "..", "..", "..", "..") as MonoRepoRoot; /** * Resolve a package name to its location in the monorepo to the single node_modules directory. * - * @param {string} packageName - * @param {ImportMeta} [meta] The `import.meta` object of the module. + * @param packageName The name of the package to resolve. + * @param meta The `import.meta` object of the module. * * @runtime node - * @returns {string} The resolved path to the package. + * @returns The resolved path to the package. * @throws {Error} If the package cannot be resolved. */ -export function resolvePackage(packageName, meta) { +export function resolvePackage(packageName: string, meta?: ImportMeta): string { const require = createRequire(meta ? meta.url : import.meta.url); const relativePackageJSONPath = join(packageName, "package.json"); - /** @type {string} */ - let absolutePackageJSONPath; + let absolutePackageJSONPath: string; try { absolutePackageJSONPath = require.resolve(relativePackageJSONPath); diff --git a/web/packages/core/promises/index.js b/web/packages/core/promises/index.js deleted file mode 100644 index 3bc2308e241b..000000000000 --- a/web/packages/core/promises/index.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @file Helpers for running tests. - */ - -/** - * A function that returns a promise. - * @template {never[]} [A=never[]] - * @typedef {(...args: A) => Promise} Thenable - */ - -/** - * A tuple of a function and its arguments. - * @template {Thenable} [T=Thenable] - * @typedef {[T, Parameters]} SerializedThenable - */ - -/** - * Executes a sequence of promise-returning functions in series - * @template {Thenable[]} T - * @param {{ [K in keyof T]: [T[K], ...Parameters] }} sequence - * @returns {Promise} - */ -export async function series(...sequence) { - for (const [thenable, ...args] of sequence) { - await thenable(...args); - } -} diff --git a/web/packages/core/promises/index.ts b/web/packages/core/promises/index.ts new file mode 100644 index 000000000000..cecc654aa039 --- /dev/null +++ b/web/packages/core/promises/index.ts @@ -0,0 +1,24 @@ +/** + * @file Helpers for running tests. + */ + +/** + * A function that returns a promise. + */ +export type Thenable = (...args: A) => Promise; + +/** + * A tuple of a function and its arguments. + */ +export type SerializedThenable = [T, Parameters]; + +/** + * Executes a sequence of promise-returning functions in series + */ +export async function series( + ...sequence: { [K in keyof T]: [T[K], ...Parameters] } +): Promise { + for (const [thenable, ...args] of sequence) { + await thenable(...args); + } +} diff --git a/web/packages/core/scripting/node.js b/web/packages/core/scripting/node.ts similarity index 86% rename from web/packages/core/scripting/node.js rename to web/packages/core/scripting/node.ts index 16513d3d0679..3202bba569f7 100644 --- a/web/packages/core/scripting/node.js +++ b/web/packages/core/scripting/node.ts @@ -6,12 +6,12 @@ import { fileURLToPath } from "node:url"; /** * Predicate to determine if a module was run directly, i.e. not imported. * - * @param {ImportMeta} meta The `import.meta` object of the module. + * @param meta The `import.meta` object of the module. * - * @return {boolean} Whether the module was run directly. + * @return Whether the module was run directly. * @runtime node */ -export function isMain(meta) { +export function isMain(meta: ImportMeta): boolean { // Are we not in a module context? if (!meta) return false; diff --git a/web/packages/core/tsconfig.json b/web/packages/core/tsconfig.json index 545506fe907f..e0cc5840b1e7 100644 --- a/web/packages/core/tsconfig.json +++ b/web/packages/core/tsconfig.json @@ -3,8 +3,20 @@ "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ESNext"], "resolveJsonModule": true, - "checkJs": true, - "allowJs": true, - "emitDeclarationOnly": true - } + // Consumers import these modules as TypeScript sources — Node strips the + // types at runtime, and every bundler in the monorepo reads `.ts` + // directly. Nothing reads a declaration file, so there is nothing to emit. + "noEmit": true, + "emitDeclarationOnly": false, + "declaration": false, + "declarationMap": false, + "composite": false, + "incremental": false, + "allowImportingTsExtensions": true, + // Node's type stripping cannot evaluate non-erasable syntax such as + // enums or constructor parameter properties. + "erasableSyntaxOnly": true + }, + "include": ["./**/*.ts"], + "exclude": ["./out/**", "./node_modules/**"] } diff --git a/web/packages/core/version/node.js b/web/packages/core/version/node.ts similarity index 84% rename from web/packages/core/version/node.js rename to web/packages/core/version/node.ts index 3aa7b256f937..0c2b5954299f 100644 --- a/web/packages/core/version/node.js +++ b/web/packages/core/version/node.ts @@ -11,16 +11,14 @@ import PackageJSON from "../../../../package.json" with { type: "json" }; //#region Semantic Versioning -/** - * @typedef {`${number}.${number}.${number}${string}`} SemVerSource - */ +export type SemVerSource = `${number}.${number}.${number}${string}`; /** * The current version of authentik in SemVer format. * * @runtime node */ -export const AuthentikVersion = /** @type {SemVerSource} */ (PackageJSON.version); +export const AuthentikVersion = PackageJSON.version as SemVerSource; /** * A parsed SemVer object of the current version of authentik. @@ -61,9 +59,9 @@ const prerelease = NodeEnvironment === "development" || !GIT_BUILD_HASH || GIT_B * @runtime node * @see {@link "authentik\_\_init\_\_.py"} */ -export const BuildIdentifier = /** @type {SemVerSource | `${SemVerSource}+${string}`} */ ( +export const BuildIdentifier = ( GIT_BUILD_HASH ? [AuthentikVersion, GIT_BUILD_HASH].join("+") : AuthentikVersion -); +) as SemVerSource | `${SemVerSource}+${string}`; //#endregion @@ -72,14 +70,14 @@ export const BuildIdentifier = /** @type {SemVerSource | `${SemVerSource}+${stri /** * The published subdomain for the current version of authentik. */ -export const VersionSubdomain = /** @type {`version-${number}-${number}`} */ ( - ["version", major, minor].join("-") -); +export const VersionSubdomain = ["version", major, minor].join( + "-", +) as `version-${number}-${number}`; /** * The published path to the current version of authentik. */ -export const VersionPath = /** @type {`${number}.${number}`} */ ([major, minor].join(".")); +export const VersionPath = [major, minor].join(".") as `${number}.${number}`; /** * A URL to the latest pre-release documentation. @@ -95,11 +93,9 @@ export const CurrentReleaseDocsURL = prerelease /** * A URL to the latest release notes, if any are available. * - * @type {URL} - * * @runtime node */ -let ReleaseNotesURL; +let ReleaseNotesURL: URL; if (prerelease) { ReleaseNotesURL = new URL("/releases", PreReleaseDocsURL); diff --git a/web/paths/index.js b/web/paths/index.ts similarity index 100% rename from web/paths/index.js rename to web/paths/index.ts diff --git a/web/paths/node.js b/web/paths/node.ts similarity index 83% rename from web/paths/node.js rename to web/paths/node.ts index ccd6fb79afd8..f0b000a3130a 100644 --- a/web/paths/node.js +++ b/web/paths/node.ts @@ -13,16 +13,14 @@ const relativeDirname = dirname(fileURLToPath(import.meta.url)); //#region Base paths -/** - * @typedef {'@goauthentik/web'} WebPackageIdentifier - */ +export type WebPackageIdentifier = "@goauthentik/web"; /** * The root of the web package. * * @runtime node */ -export const PackageRoot = /** @type {WebPackageIdentifier} */ (resolve(relativeDirname, "..")); +export const PackageRoot = resolve(relativeDirname, "..") as WebPackageIdentifier; /** * Path to the web package's distribution directory. @@ -31,29 +29,31 @@ export const PackageRoot = /** @type {WebPackageIdentifier} */ (resolve(relative * * @runtime node */ -export const DistDirectory = /** @type {`${WebPackageIdentifier}/${typeof DistDirectoryName}`} */ ( - resolve(PackageRoot, DistDirectoryName) -); +export const DistDirectory = resolve( + PackageRoot, + DistDirectoryName, +) as `${WebPackageIdentifier}/${typeof DistDirectoryName}`; //#endregion //#region Entry points /** - * @typedef {{ in: string, out: string }} EntryPointTarget - * * ESBuild entrypoint target. + * * Matches the type defined in the ESBuild context. */ +export interface EntryPointTarget { + in: string; + out: string; +} /** * Entry points available for building. * - * @satisfies {Record} - * * @runtime node */ -export const EntryPoint = /** @type {const} */ ({ +export const EntryPoint = { Admin: { in: resolve(PackageRoot, "src", "admin", "index.entrypoint.ts"), out: resolve(DistDirectory, "admin", "AdminInterface"), @@ -94,6 +94,6 @@ export const EntryPoint = /** @type {const} */ ({ in: resolve(PackageRoot, "src", "styles", "flows.global.css"), out: resolve(DistDirectory, "styles", "flow"), }, -}); +} as const satisfies Record; //#endregion diff --git a/web/scripts/build-assets.mjs b/web/scripts/build-assets.ts similarity index 94% rename from web/scripts/build-assets.mjs rename to web/scripts/build-assets.ts index 864813d052cb..9dac82d31e90 100644 --- a/web/scripts/build-assets.mjs +++ b/web/scripts/build-assets.ts @@ -13,14 +13,9 @@ import { MonoRepoRoot } from "@goauthentik/core/paths/node"; const require = createRequire(import.meta.url); const logger = ConsoleLogger.child({ name: "Assets" }); -/** - * @typedef {[from: string, to: string]} SourceDestinationPair - */ +type SourceDestinationPair = [from: string, to: string]; -/** - * @type {SourceDestinationPair[]} - */ -const assets = [ +const assets: SourceDestinationPair[] = [ [ path.join(path.dirname(EntryPoint.StandaloneLoading.in), "startup"), path.dirname(EntryPoint.StandaloneLoading.out), @@ -86,10 +81,7 @@ async function copyHexworld() { } export async function copyAssets() { - /** - * @param {SourceDestinationPair} pair - */ - const copy = ([from, to]) => { + const copy = ([from, to]: SourceDestinationPair) => { const resolvedDestination = path.resolve(DistDirectory, to); logger.debug(`📋 Copying assets from ${from} to ${to}`); diff --git a/web/scripts/build-locales.mjs b/web/scripts/build-locales.ts similarity index 84% rename from web/scripts/build-locales.mjs rename to web/scripts/build-locales.ts index dc3786165472..43df4fe1151e 100644 --- a/web/scripts/build-locales.mjs +++ b/web/scripts/build-locales.ts @@ -1,5 +1,3 @@ -/// - /** * @file Lit Localize build script. * @@ -10,15 +8,14 @@ * If they are not, it runs the locale building script, intercepting the * long spew of "this string is not translated" and replacing it with a * summary of how many strings are missing with respect to the source locale. - * - * @import { Stats } from "node:fs"; */ +import type { Stats } from "node:fs"; import * as fs from "node:fs/promises"; import path, { resolve } from "node:path"; -import { generatePseudoLocaleModule } from "./pseudolocalize.mjs"; -import { unescapeOverescapedLitTemplates } from "./unescape-locale-entities.mjs"; +import { generatePseudoLocaleModule } from "./pseudolocalize.ts"; +import { unescapeOverescapedLitTemplates } from "./unescape-locale-entities.ts"; import { ConsoleLogger } from "#logger/node"; import { PackageRoot } from "#paths/node"; @@ -32,25 +29,32 @@ const missingMessagePattern = /([\w_-]+)\smessage\s(?:[\w_.-]+)\sis\smissing/; const outdatedMessagePattern = /([\w_-]+)\smessage\s(?:[\w_.-]+)\sdoes\snot\sexist/; const logger = ConsoleLogger.child({ name: "Locales" }); -const localizeRules = readConfigFileAndWriteSchema(path.join(PackageRoot, "lit-localize.json")); +const configFileRules = readConfigFileAndWriteSchema(path.join(PackageRoot, "lit-localize.json")); + +const { interchange, output } = configFileRules; -if (localizeRules.interchange.format !== "xliff") { +if (interchange.format !== "xliff") { logger.error("Unsupported interchange type, expected 'xliff'"); process.exit(1); } -const { sourceLocale } = localizeRules; +if (output.mode !== "runtime") { + logger.error("Unsupported output mode, expected 'runtime'"); + process.exit(1); +} -localizeRules.targetLocales = localizeRules.targetLocales.filter((locale) => { - return locale !== sourceLocale; -}); +const { sourceLocale } = configFileRules; + +const localizeRules = { + ...configFileRules, + interchange, + output, + targetLocales: configFileRules.targetLocales.filter((locale) => locale !== sourceLocale), +}; -const XLIFFPath = resolve(PackageRoot, localizeRules.interchange.xliffDir); +const XLIFFPath = resolve(PackageRoot, interchange.xliffDir); -const EmittedLocalesDirectory = resolve( - PackageRoot, - /** @type {string} */ (localizeRules.output.outputDir), -); +const EmittedLocalesDirectory = resolve(PackageRoot, output.outputDir); const targetLocales = localizeRules.targetLocales.filter((localeCode) => { return localeCode !== "en-XA"; @@ -84,18 +88,12 @@ async function cleanEmittedLocales() { * generated file doesn't exist, or the XLF file is newer (has a higher date) * than the generated file. The missing XLF file is important enough it * generates a unique error message and halts the build. - * - * @param {string} localeCode - * @returns {Promise} */ -async function checkIfEmittedFileCurrent(localeCode) { +async function checkIfEmittedFileCurrent(localeCode: string): Promise { const xliffPath = path.join(XLIFFPath, `${localeCode}.xlf`); const emittedPath = path.join(EmittedLocalesDirectory, `${localeCode}.ts`); - /** - * @type {Stats} - */ - let xliffStat; + let xliffStat: Stats; try { xliffStat = await fs.stat(xliffPath); @@ -104,10 +102,7 @@ async function checkIfEmittedFileCurrent(localeCode) { process.exit(1); } - /** - * @type {Stats} - */ - let emittedStat; + let emittedStat: Stats; // If the generated file doesn't exist, of course it's not up to date. try { @@ -128,9 +123,8 @@ async function checkIfEmittedFileCurrent(localeCode) { /** * Checks if all the locale source files are up-to-date with their XLIFF sources. - * @returns {Promise} */ -async function checkIfLocalesAreCurrent() { +async function checkIfLocalesAreCurrent(): Promise { logger.info("Reading locale configuration..."); logger.info(`Checking ${targetLocales.length} source files...`); @@ -157,15 +151,8 @@ export async function generateLocaleModules() { logger.info("Generating locale modules..."); - /** - * @type {Map} - */ - const missingTranslationWarnings = new Map(); - - /** - * @type {Map} - */ - const outdatedTranslationWarnings = new Map(); + const missingTranslationWarnings = new Map(); + const outdatedTranslationWarnings = new Map(); const initialConsoleWarn = console.warn; @@ -202,7 +189,6 @@ export async function generateLocaleModules() { initialConsoleWarn(arg0, ...args); }; - // @ts-expect-error: Type is too broad. const localizer = new RuntimeLitLocalizer(localizeRules); await localizer.build(); diff --git a/web/scripts/build-web.mjs b/web/scripts/build-web.ts similarity index 90% rename from web/scripts/build-web.mjs rename to web/scripts/build-web.ts index 9e5860d5a4b8..1025ce7cfc84 100644 --- a/web/scripts/build-web.mjs +++ b/web/scripts/build-web.ts @@ -7,13 +7,8 @@ import "@goauthentik/core/environment/load/node"; import * as fs from "node:fs/promises"; import * as path from "node:path"; -import { copyAssets } from "./build-assets.mjs"; +import { copyAssets } from "./build-assets.ts"; -/** - * @file ESBuild script for building the authentik web UI. - * - * @import { BuildOptions, Plugin } from "esbuild"; - */ import { mdxPlugin } from "#bundler/mdx-plugin/node"; import { styleLoaderPlugin } from "#bundler/style-loader-plugin/node"; import { createBundleDefinitions } from "#bundler/utils/node"; @@ -24,7 +19,7 @@ import { NodeEnvironment } from "@goauthentik/core/environment/node"; import { MonoRepoRoot } from "@goauthentik/core/paths/node"; import { BuildIdentifier } from "@goauthentik/core/version/node"; -import esbuild from "esbuild"; +import esbuild, { type BuildOptions, type PartialMessage, type Plugin } from "esbuild"; /// @@ -41,18 +36,12 @@ const entryPointNames = Object.keys(EntryPoint); const entryPoints = Object.values(EntryPoint); const entryPointsDescription = entryPointNames.join("\n\t"); -/** - * @type {Plugin[]} - */ -const BASE_ESBUILD_PLUGINS = [ +const BASE_ESBUILD_PLUGINS: Plugin[] = [ { name: "copy", setup(build) { build.onEnd(async () => { - /** - * @type {import('esbuild').PartialMessage[]} - */ - const errors = []; + const errors: PartialMessage[] = []; await copyAssets(); @@ -83,10 +72,7 @@ const BASE_ESBUILD_PLUGINS = [ }), ]; -/** - * @type {BuildOptions} - */ -const BASE_ESBUILD_OPTIONS = { +const BASE_ESBUILD_OPTIONS: BuildOptions = { entryNames: `[dir]/[name]-${BuildIdentifier}`, chunkNames: "[dir]/chunks/[hash]", assetNames: "assets/[dir]/[name]-[hash]", @@ -142,12 +128,11 @@ const BASE_ESBUILD_OPTIONS = { /** * Creates an ESBuild options, extending the base options with the given overrides. - * - * @param {BuildOptions["entryPoints"]} entryPoints - * @param {Plugin[]} plugIns - * @returns {BuildOptions} */ -export function createESBuildOptions(entryPoints, plugIns = []) { +export function createESBuildOptions( + entryPoints: BuildOptions["entryPoints"], + plugIns: Plugin[] = [], +): BuildOptions { const plugins = [...BASE_ESBUILD_PLUGINS, ...plugIns]; return { @@ -183,10 +168,9 @@ function doHelp() { } /** - * - * @returns {Promise<() => Promise>} dispose + * @returns dispose */ -async function doWatch() { +async function doWatch(): Promise<() => Promise> { logger.info(`🤖 Watching entry points:\n\t${entryPointsDescription}`); const developmentPlugins = await import("@goauthentik/esbuild-plugin-live-reload/plugin") @@ -251,7 +235,7 @@ async function doProxy() { logger.info("Proxy build complete"); } -async function delegateCommand() { +async function delegateCommand(): Promise<(() => Promise) | void> { const command = process.argv[2]; switch (command) { @@ -279,10 +263,7 @@ await cleanDistDirectory() process.exit(0); } - /** - * @type {Promise} - */ - const signalListener = new Promise((resolve) => { + const signalListener = new Promise((resolve) => { // We prevent multiple attempts to dispose the context // because ESBuild will repeatedly restart its internal clean-up logic. // However, sending a second SIGINT will still exit the process immediately. diff --git a/web/scripts/pseudolocalize.mjs b/web/scripts/pseudolocalize.ts similarity index 72% rename from web/scripts/pseudolocalize.mjs rename to web/scripts/pseudolocalize.ts index 0afc1579a671..06837a4d3973 100644 --- a/web/scripts/pseudolocalize.mjs +++ b/web/scripts/pseudolocalize.ts @@ -1,12 +1,5 @@ -/// - /** * @file Pseudo-localization script. - * - * @import { ConfigFile } from "@lit/localize-tools/lib/types/config.js" - * @import { Config } from '@lit/localize-tools/lib/types/config.js'; - * @import { ProgramMessage } from "@lit/localize-tools/src/messages.js" - * @import { Locale } from "@lit/localize-tools/src/types/locale.js" */ import { readFileSync } from "node:fs"; @@ -20,27 +13,27 @@ import { isMain } from "@goauthentik/core/scripting/node"; import pseudolocale from "pseudolocale"; import { makeFormatter } from "@lit/localize-tools/lib/formatters/index.js"; +import type { Message, ProgramMessage } from "@lit/localize-tools/lib/messages.js"; import { sortProgramMessages } from "@lit/localize-tools/lib/messages.js"; import { TransformLitLocalizer } from "@lit/localize-tools/lib/modes/transform.js"; +import type { Config, ConfigFile } from "@lit/localize-tools/lib/types/config.js"; +import type { Locale } from "@lit/localize-tools/lib/types/locale.js"; +import type { TransformOutputConfig } from "@lit/localize-tools/lib/types/modes.js"; -const pseudoLocale = /** @type {Locale} */ ("en-XA"); +const pseudoLocale = "en-XA" as Locale; const targetLocales = [pseudoLocale]; const __dirname = fileURLToPath(new URL(".", import.meta.url)); -/** - * @type {ConfigFile} - */ -const baseConfig = JSON.parse(readFileSync(path.join(PackageRoot, "lit-localize.json"), "utf-8")); +const baseConfig = JSON.parse( + readFileSync(path.join(PackageRoot, "lit-localize.json"), "utf-8"), +) as ConfigFile; // Need to make some internal specifications to satisfy the transformer. It doesn't actually matter // which Localizer we use (transformer or runtime), because all of the functionality we care about // is in their common parent class, but I had to pick one. Everything else here is just pure // exploitation of the lit/localize-tools internals. -/** - * @satisfies {Config} - */ -const config = { +const config: Config & { output: TransformOutputConfig } = { ...baseConfig, baseDir: path.join(__dirname, ".."), targetLocales, @@ -48,15 +41,10 @@ const config = { ...baseConfig.output, mode: "transform", }, - resolve: (path) => path, + resolve: (path: string) => path, }; -/** - * - * @param {ProgramMessage} message - * @returns - */ -const pseudoMessagify = (message) => ({ +const pseudoMessagify = (message: ProgramMessage): Message => ({ name: message.name, contents: message.contents.map((content) => typeof content === "string" ? pseudolocale(content, { prepend: "", append: "" }) : content, diff --git a/web/scripts/unescape-locale-entities.mjs b/web/scripts/unescape-locale-entities.ts similarity index 88% rename from web/scripts/unescape-locale-entities.mjs rename to web/scripts/unescape-locale-entities.ts index 21d043f42ba9..b8fa4522f5a7 100644 --- a/web/scripts/unescape-locale-entities.mjs +++ b/web/scripts/unescape-locale-entities.ts @@ -1,5 +1,3 @@ -/// - /** * @file Locale module post-process pass. * @@ -24,8 +22,7 @@ const TEMPLATE_TAGS = new Set(["str", "html"]); const ENTITY_PATTERN = /&(?:lt|gt|quot|apos|amp);/g; const DOUBLE_ENCODED_PATTERN = /&(lt|gt|quot|apos|amp);/g; -/** @type {Record} */ -const ENTITY_TABLE = { +const ENTITY_TABLE: Record = { "<": "<", ">": ">", """: '"', @@ -33,17 +30,23 @@ const ENTITY_TABLE = { "&": "&", }; +/** + * The result of a rewrite pass: the new source, and how many entity + * references it replaced. + */ +export interface EntityRewrite { + output: string; + replacements: number; +} + /** * Decode the XML predefined entity refs. Repeats until stable so * doubly-encoded forms like `&quot;` collapse to `"` in one call. * * Used for non-HTML templates: the result becomes raw text content in the * DOM, so the entity references have to disappear entirely. - * - * @param {string} input - * @returns {{ output: string; replacements: number }} */ -function decodeXmlEntities(input) { +function decodeXmlEntities(input: string): EntityRewrite { let current = input; let replacements = 0; @@ -68,11 +71,8 @@ function decodeXmlEntities(input) { * HTML, so a legitimate `>` must stay as `>`, but a translator's * `&gt;` (which renders to a visible `>`) should collapse back to * `>` (which renders to `>`). - * - * @param {string} input - * @returns {{ output: string; replacements: number }} */ -function undoubleHtmlEntities(input) { +function undoubleHtmlEntities(input: string): EntityRewrite { let replacements = 0; const output = input.replace(DOUBLE_ENCODED_PATTERN, (_match, name) => { replacements++; @@ -87,11 +87,8 @@ function undoubleHtmlEntities(input) { * * Does not parse the file as TypeScript; the emitted shape from * `@lit/localize-tools` is regular enough to scan character-by-character. - * - * @param {string} source - * @returns {{ output: string; replacements: number }} */ -export function sanitizeLocaleModule(source) { +export function sanitizeLocaleModule(source: string): EntityRewrite { let output = ""; let cursor = 0; let replacements = 0; @@ -182,15 +179,21 @@ export function sanitizeLocaleModule(source) { return { output, replacements }; } +/** + * How many locale modules a sanitize pass rewrote, and how many entity + * references it replaced across them. + */ +export interface SanitizeSummary { + touched: number; + replacements: number; +} + /** * Run {@link sanitizeLocaleModule} over every `.ts` and `.js` file in the * emitted locales directory. Rewrites files in place when their contents * change. - * - * @param {string} directory - * @returns {Promise<{ touched: number; replacements: number }>} */ -export async function unescapeOverescapedLitTemplates(directory) { +export async function unescapeOverescapedLitTemplates(directory: string): Promise { const entries = await fs.readdir(directory, { withFileTypes: true }); let touched = 0; diff --git a/web/test/unit/tsconfig.json b/web/test/unit/tsconfig.json index 59b690efbf20..9dc7869f8a16 100644 --- a/web/test/unit/tsconfig.json +++ b/web/test/unit/tsconfig.json @@ -8,6 +8,7 @@ "composite": true, "resolveJsonModule": true, "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": true, "emitDeclarationOnly": true, "target": "esnext", "module": "preserve", @@ -21,11 +22,7 @@ // --- "**/out/**/*", "**/dist/**/*", - "storybook-static", - // TODO: @lit/localize-tools v0.8.0 has a nullish coalescing typing error. - // Remove when we upgrade past that. - "scripts/pseudolocalize.mjs", - "scripts/build-locales.mjs" + "storybook-static" ], "references": [ { diff --git a/web/test/unit/unescape-locale-entities.test.ts b/web/test/unit/unescape-locale-entities.test.ts index 251e3c1a2ba2..f8059ee28662 100644 --- a/web/test/unit/unescape-locale-entities.test.ts +++ b/web/test/unit/unescape-locale-entities.test.ts @@ -1,4 +1,4 @@ -import { sanitizeLocaleModule } from "../../scripts/unescape-locale-entities.mjs"; +import { sanitizeLocaleModule } from "../../scripts/unescape-locale-entities.ts"; import { describe, expect, it } from "vitest"; diff --git a/web/tsconfig.json b/web/tsconfig.json index b60dd34ad242..6bc703199ede 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -7,6 +7,10 @@ "types": ["node"], "checkJs": true, "allowJs": true, + // Build scripts and bundler plugins run straight from source under + // Node's type stripping, which does not rewrite `.js` specifiers back + // to `.ts`. Their relative imports name the real file on disk. + "allowImportingTsExtensions": true, "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "emitDeclarationOnly": true, @@ -74,11 +78,6 @@ // packages/client-ts) into every `tsc -p .` for no benefit. "packages/client-ts", "packages/sfe", - // TODO: @lit/localize-tools v0.8.0 has a nullish coalescing typing error. - // Remove when we upgrade past that. - "scripts/pseudolocalize.mjs", - "scripts/build-locales.mjs", - // TODO: Remove after monorepo cleanup. "src/**/*.comp.ts" ]