web: Packagify live reload plugin. (#14134)
* web: Packagify live reload plugin. * web: Use shared formatter. * web: Format. * web: Use project mode typecheck. * web: Fix type errors.
This commit is contained in:
@ -1,23 +0,0 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 100,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "consistent",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"plugins": ["@trivago/prettier-plugin-sort-imports"],
|
||||
"importOrder": ["^(@?)lit(.*)$", "\\.css$", "^@goauthentik/api$", "^[./]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true,
|
||||
"importOrderParserPlugins": ["typescript", "jsx", "classProperties", "decorators-legacy"]
|
||||
}
|
790
web/package-lock.json
generated
790
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -61,6 +61,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.11.1",
|
||||
"@goauthentik/esbuild-plugin-live-reload": "^1.0.4",
|
||||
"@goauthentik/prettier-config": "^1.0.4",
|
||||
"@goauthentik/tsconfig": "^1.0.4",
|
||||
"@hcaptcha/types": "^1.0.4",
|
||||
"@lit/localize-tools": "^0.8.0",
|
||||
"@rollup/plugin-replace": "^6.0.1",
|
||||
@ -72,7 +75,7 @@
|
||||
"@storybook/manager-api": "^8.3.4",
|
||||
"@storybook/web-components": "^8.3.4",
|
||||
"@storybook/web-components-vite": "^8.3.4",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"@types/chart.js": "^2.9.41",
|
||||
"@types/codemirror": "^5.60.15",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
@ -95,7 +98,6 @@
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-plugin-lit": "^1.15.0",
|
||||
"eslint-plugin-wc": "^2.1.1",
|
||||
"find-free-ports": "^3.1.1",
|
||||
"github-slugger": "^2.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"globals": "^15.10.0",
|
||||
@ -136,6 +138,7 @@
|
||||
"axios": "^1.8.4"
|
||||
}
|
||||
},
|
||||
"prettier": "@goauthentik/prettier-config",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "wireit",
|
||||
@ -271,7 +274,7 @@
|
||||
"command": "tsc --noEmit -p ./tests"
|
||||
},
|
||||
"lint:types": {
|
||||
"command": "tsc --noEmit -p .",
|
||||
"command": "NODE_OPTIONS=\"--max-old-space-size=3000\" tsc -b .",
|
||||
"dependencies": [
|
||||
"build-locales",
|
||||
"lint:types:tests"
|
||||
|
@ -1,13 +1,18 @@
|
||||
/// <reference types="./types.js" />
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Client-side observer for ESBuild events.
|
||||
* @file Client-side observer for ESBuild events.
|
||||
*
|
||||
* @import { Message as ESBuildMessage } from "esbuild";
|
||||
*/
|
||||
import type { Message as ESBuildMessage } from "esbuild";
|
||||
|
||||
const logPrefix = "👷 [ESBuild]";
|
||||
const log = console.debug.bind(console, logPrefix);
|
||||
|
||||
type BuildEventListener<Data = unknown> = (event: MessageEvent<Data>) => void;
|
||||
/**
|
||||
* @template {unknown} [Data=unknown]
|
||||
* @typedef {(event: MessageEvent) => void} BuildEventListener
|
||||
*/
|
||||
|
||||
/**
|
||||
* A client-side watcher for ESBuild.
|
||||
@ -16,14 +21,13 @@ type BuildEventListener<Data = unknown> = (event: MessageEvent<Data>) => void;
|
||||
* ESBuild may tree-shake it out of production builds.
|
||||
*
|
||||
* ```ts
|
||||
* if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
||||
* const { ESBuildObserver } = await import("@goauthentik/common/client");
|
||||
*
|
||||
* new ESBuildObserver(process.env.WATCHER_URL);
|
||||
* if (process.env.NODE_ENV === "development") {
|
||||
* await import("@goauthentik/esbuild-plugin-live-reload/client")
|
||||
* .catch(() => console.warn("Failed to import watcher"))
|
||||
* }
|
||||
* ```
|
||||
}
|
||||
|
||||
*
|
||||
* @implements {Disposable}
|
||||
*/
|
||||
export class ESBuildObserver extends EventSource {
|
||||
/**
|
||||
@ -58,15 +62,19 @@ export class ESBuildObserver extends EventSource {
|
||||
|
||||
/**
|
||||
* The interval for the keep-alive check.
|
||||
* @type {ReturnType<typeof setInterval> | undefined}
|
||||
*/
|
||||
#keepAliveInterval: ReturnType<typeof setInterval> | undefined;
|
||||
#keepAliveInterval;
|
||||
|
||||
#trackActivity = () => {
|
||||
this.lastUpdatedAt = Date.now();
|
||||
this.alive = true;
|
||||
};
|
||||
|
||||
#startListener: BuildEventListener = () => {
|
||||
/**
|
||||
* @type {BuildEventListener}
|
||||
*/
|
||||
#startListener = () => {
|
||||
this.#trackActivity();
|
||||
log("⏰ Build started...");
|
||||
};
|
||||
@ -82,13 +90,18 @@ export class ESBuildObserver extends EventSource {
|
||||
}
|
||||
};
|
||||
|
||||
#errorListener: BuildEventListener<string> = (event) => {
|
||||
/**
|
||||
* @type {BuildEventListener<string>}
|
||||
*/
|
||||
#errorListener = (event) => {
|
||||
this.#trackActivity();
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.group(logPrefix, "⛔️⛔️⛔️ Build error...");
|
||||
|
||||
const esbuildErrorMessages: ESBuildMessage[] = JSON.parse(event.data);
|
||||
/**
|
||||
* @type {ESBuildMessage[]}
|
||||
*/
|
||||
const esbuildErrorMessages = JSON.parse(event.data);
|
||||
|
||||
for (const error of esbuildErrorMessages) {
|
||||
console.warn(error.text);
|
||||
@ -101,11 +114,13 @@ export class ESBuildObserver extends EventSource {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.groupEnd();
|
||||
};
|
||||
|
||||
#endListener: BuildEventListener = () => {
|
||||
/**
|
||||
* @type {BuildEventListener}
|
||||
*/
|
||||
#endListener = () => {
|
||||
cancelAnimationFrame(this.#reloadFrameID);
|
||||
|
||||
this.#trackActivity();
|
||||
@ -126,12 +141,36 @@ export class ESBuildObserver extends EventSource {
|
||||
});
|
||||
};
|
||||
|
||||
#keepAliveListener: BuildEventListener = () => {
|
||||
/**
|
||||
* @type {BuildEventListener}
|
||||
*/
|
||||
#keepAliveListener = () => {
|
||||
this.#trackActivity();
|
||||
log("🏓 Keep-alive");
|
||||
};
|
||||
|
||||
constructor(url: string | URL) {
|
||||
/**
|
||||
* Initialize the ESBuild observer.
|
||||
* This should be called once in your application.
|
||||
*
|
||||
* @param {string | URL} [url]
|
||||
* @returns {ESBuildObserver}
|
||||
*/
|
||||
static initialize = (url) => {
|
||||
const esbuildObserver = new ESBuildObserver(url);
|
||||
|
||||
return esbuildObserver;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string | URL} [url]
|
||||
*/
|
||||
constructor(url) {
|
||||
if (!url) {
|
||||
throw new TypeError("ESBuildObserver: Cannot construct without a URL");
|
||||
}
|
||||
|
||||
super(url);
|
||||
|
||||
this.addEventListener("esbuild:start", this.#startListener);
|
||||
@ -167,4 +206,14 @@ export class ESBuildObserver extends EventSource {
|
||||
log("👋 Waiting for build to start...");
|
||||
}, 15_000);
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
return this.close();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
return this[Symbol.dispose]();
|
||||
}
|
||||
}
|
||||
|
||||
export default ESBuildObserver;
|
13
web/packages/esbuild-plugin-live-reload/client/index.js
Normal file
13
web/packages/esbuild-plugin-live-reload/client/index.js
Normal file
@ -0,0 +1,13 @@
|
||||
/// <reference types="./types.js" />
|
||||
/**
|
||||
* @file Entry point for the ESBuild client-side observer.
|
||||
*/
|
||||
import { ESBuildObserver } from "./ESBuildObserver.js";
|
||||
|
||||
if (import.meta.env?.ESBUILD_WATCHER_URL) {
|
||||
const buildObserver = new ESBuildObserver(import.meta.env.ESBUILD_WATCHER_URL);
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
buildObserver.dispose();
|
||||
});
|
||||
}
|
18
web/packages/esbuild-plugin-live-reload/client/types.d.ts
vendored
Normal file
18
web/packages/esbuild-plugin-live-reload/client/types.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @file Import meta environment variables available via ESBuild.
|
||||
*/
|
||||
|
||||
export {};
|
||||
declare global {
|
||||
interface ImportMeta {
|
||||
readonly env: {
|
||||
/**
|
||||
* The injected watcher URL for ESBuild.
|
||||
* This is used for live reloading in development mode.
|
||||
*
|
||||
* @format url
|
||||
*/
|
||||
ESBUILD_WATCHER_URL: string;
|
||||
};
|
||||
}
|
||||
}
|
2
web/packages/esbuild-plugin-live-reload/index.js
Normal file
2
web/packages/esbuild-plugin-live-reload/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./client/index.js";
|
||||
export * from "./plugin/index.js";
|
53
web/packages/esbuild-plugin-live-reload/package.json
Normal file
53
web/packages/esbuild-plugin-live-reload/package.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@goauthentik/esbuild-plugin-live-reload",
|
||||
"description": "ESBuild plugin to watch for file changes and trigger client-side reloads.",
|
||||
"version": "1.0.4",
|
||||
"dependencies": {
|
||||
"find-free-ports": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@goauthentik/prettier-config": "^1.0.4",
|
||||
"@goauthentik/tsconfig": "^1.0.4",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"@types/node": "^22.14.1",
|
||||
"esbuild": "^0.25.0",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.11"
|
||||
},
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"types": "./out/index.d.ts",
|
||||
"import": "./index.js"
|
||||
},
|
||||
"./client": {
|
||||
"types": "./out/client/index.d.ts",
|
||||
"import": "./client/index.js"
|
||||
},
|
||||
"./plugin": {
|
||||
"types": "./out/plugin/index.d.ts",
|
||||
"import": "./plugin/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"./index.js",
|
||||
"client/**/*",
|
||||
"plugin/**/*",
|
||||
"out/**/*"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"peerDependencies": {
|
||||
"esbuild": "^0.25.0"
|
||||
},
|
||||
"prettier": "@goauthentik/prettier-config",
|
||||
"private": true,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "./out/index.d.ts"
|
||||
}
|
243
web/packages/esbuild-plugin-live-reload/plugin/index.js
Normal file
243
web/packages/esbuild-plugin-live-reload/plugin/index.js
Normal file
@ -0,0 +1,243 @@
|
||||
/**
|
||||
* @file Live reload plugin for ESBuild.
|
||||
*
|
||||
* @import { ListenOptions } from "node:net";
|
||||
* @import {Server as HTTPServer} from "node:http";
|
||||
* @import {Server as HTTPSServer} from "node:https";
|
||||
*/
|
||||
import { findFreePorts } from "find-free-ports";
|
||||
import * as http from "node:http";
|
||||
import * as path from "node:path";
|
||||
|
||||
/**
|
||||
* Serializes a custom event to a text stream.
|
||||
* @param {Event} event
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serializeCustomEventToStream(event) {
|
||||
// @ts-expect-error - TS doesn't know about the detail property
|
||||
const data = event.detail ?? {};
|
||||
|
||||
const eventContent = [`event: ${event.type}`, `data: ${JSON.stringify(data)}`];
|
||||
|
||||
return eventContent.join("\n") + "\n\n";
|
||||
}
|
||||
|
||||
const MIN_PORT = 1025;
|
||||
const MAX_PORT = 65535;
|
||||
|
||||
/**
|
||||
* Find a random port that is not in use, sufficiently far from the default port.
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async function findDisparatePort() {
|
||||
const startPort = Math.floor(Math.random() * (MAX_PORT - MIN_PORT + 1)) + MIN_PORT;
|
||||
|
||||
const wathcherPorts = await findFreePorts(1, {
|
||||
startPort,
|
||||
});
|
||||
|
||||
const [port] = wathcherPorts;
|
||||
|
||||
if (!port) {
|
||||
throw new Error("No free ports available");
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event server initialization options.
|
||||
*
|
||||
* @typedef {Object} EventServerInit
|
||||
*
|
||||
* @property {string} pathname
|
||||
* @property {EventTarget} dispatcher
|
||||
* @property {string} [logPrefix]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {(req: http.IncomingMessage, res: http.ServerResponse) => void} RequestHandler
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create an event request handler.
|
||||
* @param {EventServerInit} options
|
||||
* @returns {RequestHandler}
|
||||
* @category ESBuild
|
||||
*/
|
||||
export function createRequestHandler({ pathname, dispatcher, logPrefix = "Build Observer" }) {
|
||||
// eslint-disable-next-line no-console
|
||||
const log = console.log.bind(console, `[${logPrefix}]`);
|
||||
|
||||
/**
|
||||
* @type {RequestHandler}
|
||||
*/
|
||||
const requestHandler = (req, res) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
if (req.url !== pathname) {
|
||||
log(`🚫 Invalid request to ${req.url}`);
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
log("🔌 Client connected");
|
||||
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {Event} event
|
||||
*/
|
||||
const listener = (event) => {
|
||||
const body = serializeCustomEventToStream(event);
|
||||
|
||||
res.write(body);
|
||||
};
|
||||
|
||||
dispatcher.addEventListener("esbuild:start", listener);
|
||||
dispatcher.addEventListener("esbuild:error", listener);
|
||||
dispatcher.addEventListener("esbuild:end", listener);
|
||||
|
||||
req.on("close", () => {
|
||||
log("🔌 Client disconnected");
|
||||
|
||||
clearInterval(keepAliveInterval);
|
||||
|
||||
dispatcher.removeEventListener("esbuild:start", listener);
|
||||
dispatcher.removeEventListener("esbuild:error", listener);
|
||||
dispatcher.removeEventListener("esbuild:end", listener);
|
||||
});
|
||||
|
||||
const keepAliveInterval = setInterval(() => {
|
||||
console.timeStamp("🏓 Keep-alive");
|
||||
|
||||
res.write("event: keep-alive\n\n");
|
||||
res.write(serializeCustomEventToStream(new CustomEvent("esbuild:keep-alive")));
|
||||
}, 15_000);
|
||||
};
|
||||
|
||||
return requestHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the build observer plugin.
|
||||
*
|
||||
* @typedef {object} BuildObserverOptions
|
||||
*
|
||||
* @property {HTTPServer | HTTPSServer} [server]
|
||||
* @property {ListenOptions} [listenOptions]
|
||||
* @property {string | URL} [publicURL]
|
||||
* @property {string} [logPrefix]
|
||||
* @property {string} [relativeRoot]
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a plugin that listens for build events and sends them to a server-sent event stream.
|
||||
*
|
||||
* @param {BuildObserverOptions} [options]
|
||||
* @returns {import('esbuild').Plugin}
|
||||
*/
|
||||
export function liveReloadPlugin(options = {}) {
|
||||
return {
|
||||
name: "build-watcher",
|
||||
setup: async (build) => {
|
||||
const logPrefix = options.logPrefix || "Build Observer";
|
||||
|
||||
const timerLabel = `[${logPrefix}] 🏁`;
|
||||
const relativeRoot = options.relativeRoot || process.cwd();
|
||||
|
||||
const dispatcher = new EventTarget();
|
||||
|
||||
/**
|
||||
* @type {URL}
|
||||
*/
|
||||
let publicURL;
|
||||
|
||||
if (!options.publicURL) {
|
||||
const port = await findDisparatePort();
|
||||
|
||||
publicURL = new URL(`http://localhost:${port}/events`);
|
||||
} else {
|
||||
publicURL =
|
||||
typeof options.publicURL === "string"
|
||||
? new URL(options.publicURL)
|
||||
: options.publicURL;
|
||||
}
|
||||
|
||||
build.initialOptions.define = {
|
||||
...build.initialOptions.define,
|
||||
"import.meta.env.ESBUILD_WATCHER_URL": JSON.stringify(publicURL.href),
|
||||
};
|
||||
|
||||
const requestHandler = createRequestHandler({
|
||||
pathname: publicURL.pathname,
|
||||
dispatcher,
|
||||
logPrefix,
|
||||
});
|
||||
|
||||
const server = options.server || http.createServer(requestHandler);
|
||||
|
||||
const listenOptions = options.listenOptions || {
|
||||
port: parseInt(publicURL.port, 10),
|
||||
host: publicURL.hostname,
|
||||
};
|
||||
|
||||
server.listen(listenOptions, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[${logPrefix}] Listening`);
|
||||
});
|
||||
|
||||
build.onDispose(() => {
|
||||
server?.close();
|
||||
});
|
||||
|
||||
build.onStart(() => {
|
||||
console.time(timerLabel);
|
||||
|
||||
dispatcher.dispatchEvent(
|
||||
new CustomEvent("esbuild:start", {
|
||||
detail: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
build.onEnd((buildResult) => {
|
||||
console.timeEnd(timerLabel);
|
||||
|
||||
if (!buildResult.errors.length) {
|
||||
dispatcher.dispatchEvent(
|
||||
new CustomEvent("esbuild:end", {
|
||||
detail: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(`Build ended with ${buildResult.errors.length} errors`);
|
||||
|
||||
dispatcher.dispatchEvent(
|
||||
new CustomEvent("esbuild:error", {
|
||||
detail: buildResult.errors.map((error) => ({
|
||||
...error,
|
||||
location: error.location
|
||||
? {
|
||||
...error.location,
|
||||
file: path.resolve(relativeRoot, error.location.file),
|
||||
}
|
||||
: null,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
10
web/packages/esbuild-plugin-live-reload/tsconfig.json
Normal file
10
web/packages/esbuild-plugin-live-reload/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@goauthentik/tsconfig",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"checkJs": true,
|
||||
"emitDeclarationOnly": true
|
||||
}
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
/**
|
||||
* @file ESBuild script for building the authentik web UI.
|
||||
*
|
||||
* @import { BuildOptions } from "esbuild";
|
||||
*/
|
||||
import { liveReloadPlugin } from "@goauthentik/esbuild-plugin-live-reload/plugin";
|
||||
import { execFileSync } from "child_process";
|
||||
import { deepmerge } from "deepmerge-ts";
|
||||
import esbuild from "esbuild";
|
||||
import { polyfillNode } from "esbuild-plugin-polyfill-node";
|
||||
import findFreePorts from "find-free-ports";
|
||||
import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs";
|
||||
import { globSync } from "glob";
|
||||
import * as path from "path";
|
||||
@ -11,7 +16,6 @@ import process from "process";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs";
|
||||
import { buildObserverPlugin } from "./esbuild/build-observer-plugin.mjs";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
let authentikProjectRoot = path.join(__dirname, "..", "..");
|
||||
@ -120,7 +124,7 @@ const BASE_ESBUILD_OPTIONS = {
|
||||
splitting: true,
|
||||
treeShaking: true,
|
||||
external: ["*.woff", "*.woff2"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
tsconfig: path.resolve(__dirname, "..", "tsconfig.build.json"),
|
||||
loader: {
|
||||
".css": "text",
|
||||
},
|
||||
@ -220,26 +224,17 @@ function doHelp() {
|
||||
async function doWatch() {
|
||||
console.log("Watching all entry points...");
|
||||
|
||||
const wathcherPorts = await findFreePorts(entryPoints.length);
|
||||
|
||||
const buildContexts = await Promise.all(
|
||||
entryPoints.map((entryPoint, i) => {
|
||||
const port = wathcherPorts[i];
|
||||
const serverURL = new URL(`http://localhost:${port}/events`);
|
||||
|
||||
entryPoints.map((entryPoint) => {
|
||||
return esbuild.context(
|
||||
createEntryPointOptions(entryPoint, {
|
||||
define: definitions,
|
||||
plugins: [
|
||||
buildObserverPlugin({
|
||||
serverURL,
|
||||
logPrefix: entryPoint[1],
|
||||
liveReloadPlugin({
|
||||
logPrefix: `Build Observer (${entryPoint[1]})`,
|
||||
relativeRoot: path.join(__dirname, ".."),
|
||||
}),
|
||||
],
|
||||
define: {
|
||||
...definitions,
|
||||
"process.env.WATCHER_URL": JSON.stringify(serverURL.toString()),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}),
|
||||
|
@ -1,141 +0,0 @@
|
||||
import * as http from "http";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* Serializes a custom event to a text stream.
|
||||
* a
|
||||
* @param {Event} event
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serializeCustomEventToStream(event) {
|
||||
// @ts-expect-error - TS doesn't know about the detail property
|
||||
const data = event.detail ?? {};
|
||||
|
||||
const eventContent = [`event: ${event.type}`, `data: ${JSON.stringify(data)}`];
|
||||
|
||||
return eventContent.join("\n") + "\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the build observer plugin.
|
||||
*
|
||||
* @typedef {Object} BuildObserverOptions
|
||||
*
|
||||
* @property {URL} serverURL
|
||||
* @property {string} logPrefix
|
||||
* @property {string} relativeRoot
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a plugin that listens for build events and sends them to a server-sent event stream.
|
||||
*
|
||||
* @param {BuildObserverOptions} options
|
||||
* @returns {import('esbuild').Plugin}
|
||||
*/
|
||||
export function buildObserverPlugin({ serverURL, logPrefix, relativeRoot }) {
|
||||
const timerLabel = `[${logPrefix}] Build`;
|
||||
const endpoint = serverURL.pathname;
|
||||
const dispatcher = new EventTarget();
|
||||
|
||||
const eventServer = http.createServer((req, res) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
if (req.url !== endpoint) {
|
||||
console.log(`🚫 Invalid request to ${req.url}`);
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🔌 Client connected");
|
||||
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {Event} event
|
||||
*/
|
||||
const listener = (event) => {
|
||||
const body = serializeCustomEventToStream(event);
|
||||
|
||||
res.write(body);
|
||||
};
|
||||
|
||||
dispatcher.addEventListener("esbuild:start", listener);
|
||||
dispatcher.addEventListener("esbuild:error", listener);
|
||||
dispatcher.addEventListener("esbuild:end", listener);
|
||||
|
||||
req.on("close", () => {
|
||||
console.log("🔌 Client disconnected");
|
||||
|
||||
clearInterval(keepAliveInterval);
|
||||
|
||||
dispatcher.removeEventListener("esbuild:start", listener);
|
||||
dispatcher.removeEventListener("esbuild:error", listener);
|
||||
dispatcher.removeEventListener("esbuild:end", listener);
|
||||
});
|
||||
|
||||
const keepAliveInterval = setInterval(() => {
|
||||
console.timeStamp("🏓 Keep-alive");
|
||||
|
||||
res.write("event: keep-alive\n\n");
|
||||
res.write(serializeCustomEventToStream(new CustomEvent("esbuild:keep-alive")));
|
||||
}, 15_000);
|
||||
});
|
||||
|
||||
return {
|
||||
name: "build-watcher",
|
||||
setup: (build) => {
|
||||
eventServer.listen(parseInt(serverURL.port, 10), serverURL.hostname);
|
||||
|
||||
build.onDispose(() => {
|
||||
eventServer.close();
|
||||
});
|
||||
|
||||
build.onStart(() => {
|
||||
console.time(timerLabel);
|
||||
|
||||
dispatcher.dispatchEvent(
|
||||
new CustomEvent("esbuild:start", {
|
||||
detail: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
build.onEnd((buildResult) => {
|
||||
console.timeEnd(timerLabel);
|
||||
|
||||
if (!buildResult.errors.length) {
|
||||
dispatcher.dispatchEvent(
|
||||
new CustomEvent("esbuild:end", {
|
||||
detail: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(`Build ended with ${buildResult.errors.length} errors`);
|
||||
|
||||
dispatcher.dispatchEvent(
|
||||
new CustomEvent("esbuild:error", {
|
||||
detail: buildResult.errors.map((error) => ({
|
||||
...error,
|
||||
location: error.location
|
||||
? {
|
||||
...error.location,
|
||||
file: path.resolve(relativeRoot, error.location.file),
|
||||
}
|
||||
: null,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
@ -34,6 +34,10 @@ import { SessionUser, UiThemeEnum } from "@goauthentik/api";
|
||||
|
||||
import "./AdminSidebar";
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
await import("@goauthentik/esbuild-plugin-live-reload/client");
|
||||
}
|
||||
|
||||
@customElement("ak-interface-admin")
|
||||
export class AdminInterface extends AuthenticatedInterface {
|
||||
@property({ type: Boolean })
|
||||
@ -119,16 +123,6 @@ export class AdminInterface extends AuthenticatedInterface {
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback(): Promise<void> {
|
||||
super.connectedCallback();
|
||||
|
||||
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
||||
const { ESBuildObserver } = await import("@goauthentik/common/client");
|
||||
|
||||
new ESBuildObserver(process.env.WATCHER_URL);
|
||||
}
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const sidebarClasses = {
|
||||
"pf-m-light": this.activeTheme === UiThemeEnum.Light,
|
||||
|
@ -52,7 +52,6 @@ function renderRadiusOverview(rawProvider: OneOfProvider) {
|
||||
}
|
||||
|
||||
function renderRACOverview(rawProvider: OneOfProvider) {
|
||||
// @ts-expect-error TS6133
|
||||
const _provider = rawProvider as RACProvider;
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { Config as DOMPurifyConfig } from "dompurify";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
import { render } from "@lit-labs/ssr";
|
||||
@ -6,9 +7,9 @@ import { TemplateResult, html } from "lit";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
|
||||
export const DOM_PURIFY_STRICT: DOMPurify.Config = {
|
||||
export const DOM_PURIFY_STRICT = {
|
||||
ALLOWED_TAGS: ["#text"],
|
||||
};
|
||||
} as const satisfies DOMPurifyConfig;
|
||||
|
||||
export async function renderStatic(input: TemplateResult): Promise<string> {
|
||||
return await collectResult(render(input));
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { authentikVersionContext } from "@goauthentik/elements/AuthentikContexts";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import { Constructor } from "@lit/reactive-element/decorators/base";
|
||||
import { Constructor } from "@lit/reactive-element/decorators/base.js";
|
||||
import type { LitElement } from "lit";
|
||||
|
||||
import type { Version } from "@goauthentik/api";
|
||||
|
@ -2,7 +2,7 @@ import { AkControlElement } from "@goauthentik/elements/AkControlElement";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { PropertyValues } from "@lit/reactive-element/reactive-element";
|
||||
import { PropertyValues } from "@lit/reactive-element";
|
||||
import { TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators.js";
|
||||
import { map } from "lit/directives/map.js";
|
||||
|
@ -14,8 +14,6 @@ import "@goauthentik/flow/stages/password/PasswordStage";
|
||||
|
||||
// end of stage import
|
||||
|
||||
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
||||
const { ESBuildObserver } = await import("@goauthentik/common/client");
|
||||
|
||||
new ESBuildObserver(process.env.WATCHER_URL);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
await import("@goauthentik/esbuild-plugin-live-reload/client");
|
||||
}
|
||||
|
@ -43,6 +43,10 @@ import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
|
||||
|
||||
import { CurrentBrand, EventsApi, SessionUser } from "@goauthentik/api";
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
await import("@goauthentik/esbuild-plugin-live-reload/client");
|
||||
}
|
||||
|
||||
const customStyles = css`
|
||||
.pf-c-page__main,
|
||||
.pf-c-drawer__content,
|
||||
@ -291,12 +295,6 @@ export class UserInterface extends AuthenticatedInterface {
|
||||
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer);
|
||||
window.addEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer);
|
||||
window.addEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails);
|
||||
|
||||
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
||||
const { ESBuildObserver } = await import("@goauthentik/common/client");
|
||||
|
||||
new ESBuildObserver(process.env.WATCHER_URL);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
|
@ -1,71 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"esModuleInterop": true,
|
||||
"paths": {
|
||||
"@goauthentik/docs/*": ["../website/docs/*"]
|
||||
},
|
||||
"types": [
|
||||
"node",
|
||||
"@wdio/mocha-framework",
|
||||
"@wdio/types",
|
||||
"expect-webdriverio",
|
||||
"grecaptcha"
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"lib": [
|
||||
"ES5",
|
||||
"ES2015",
|
||||
"ES2016",
|
||||
"ES2017",
|
||||
"ES2018",
|
||||
"ES2019",
|
||||
"ES2020",
|
||||
"ESNext",
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"WebWorker"
|
||||
],
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false,
|
||||
"useDefineForClassFields": false,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "ts-lit-plugin",
|
||||
"strict": true,
|
||||
"rules": {
|
||||
"no-unknown-tag-name": "off",
|
||||
"no-missing-import": "off",
|
||||
"no-incompatible-type-binding": "off",
|
||||
"no-unknown-property": "off",
|
||||
"no-unknown-attribute": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@genesiscommunitysuccess/custom-elements-lsp",
|
||||
"designSystemPrefix": "ak-",
|
||||
"parser": {
|
||||
"timeout": 2000
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"exclude": ["src/**/*.test.ts", "./tests"]
|
||||
}
|
6
web/tsconfig.build.json
Normal file
6
web/tsconfig.build.json
Normal file
@ -0,0 +1,6 @@
|
||||
// @file TSConfig used by the web package during build.
|
||||
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["src/**/*.test.ts", "./tests"]
|
||||
}
|
@ -1,7 +1,19 @@
|
||||
// @file TSConfig used by the web package during development.
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"extends": "@goauthentik/tsconfig",
|
||||
"compilerOptions": {
|
||||
"types": ["@wdio/types", "grecaptcha", "node", "@wdio/mocha-framework", "expect-webdriverio"],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"experimentalDecorators": true,
|
||||
// See https://lit.dev/docs/components/properties/
|
||||
"useDefineForClassFields": false,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"baseUrl": ".",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
// TODO: We should enable this when when we're ready to enforce it.
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"paths": {
|
||||
"@goauthentik/admin/*": ["./src/admin/*"],
|
||||
"@goauthentik/common/*": ["./src/common/*"],
|
||||
@ -13,6 +25,41 @@
|
||||
"@goauthentik/polyfill/*": ["./src/polyfill/*"],
|
||||
"@goauthentik/standalone/*": ["./src/standalone/*"],
|
||||
"@goauthentik/user/*": ["./src/user/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "ts-lit-plugin",
|
||||
"strict": true,
|
||||
"rules": {
|
||||
"no-unknown-tag-name": "off",
|
||||
"no-missing-import": "off",
|
||||
"no-incompatible-type-binding": "off",
|
||||
"no-unknown-property": "off",
|
||||
"no-unknown-attribute": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@genesiscommunitysuccess/custom-elements-lsp",
|
||||
"designSystemPrefix": "ak-",
|
||||
"parser": {
|
||||
"timeout": 2000
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
// ---
|
||||
"./out/**/*",
|
||||
"./dist/**/*",
|
||||
"src/**/*.test.ts",
|
||||
"./tests",
|
||||
|
||||
// TODO: Remove after monorepo cleanup.
|
||||
"src/**/*.comp.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./packages/esbuild-plugin-live-reload"
|
||||
}
|
||||
],
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @file TSConfig used during tests.
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
|
26
web/types/global.d.ts
vendored
Normal file
26
web/types/global.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @file Environment variables available via ESBuild.
|
||||
*/
|
||||
|
||||
declare module "process" {
|
||||
global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV: "production" | "development";
|
||||
/**
|
||||
*
|
||||
* @todo Determine where this is used and if it is needed,
|
||||
* give it a better name.
|
||||
* @deprecated
|
||||
*/
|
||||
CWD: string;
|
||||
/**
|
||||
* @todo Determine where this is used and if it is needed,
|
||||
* give it a better name.
|
||||
* @deprecated
|
||||
*/
|
||||
AK_API_BASE_PATH: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -336,6 +336,7 @@ export const config: Options.Testrunner = {
|
||||
{ error: _error, result: _result, duration: _duration, passed: _passed, retries: _retries },
|
||||
) {
|
||||
if (lemmeSee) {
|
||||
// @ts-expect-error TODO
|
||||
await browser.pause(500);
|
||||
}
|
||||
},
|
||||
|
Reference in New Issue
Block a user