web: Build entrypoints with a single ESBuild context. Clean up entrypoints.

This commit is contained in:
Teffen Ellis
2025-04-22 00:57:32 +02:00
committed by Teffen Ellis
parent af72a23d7c
commit be2cb10f40
15 changed files with 183 additions and 105 deletions

View File

@ -35,6 +35,11 @@
"watch": "run-s build-locales esbuild:watch" "watch": "run-s build-locales esbuild:watch"
}, },
"type": "module", "type": "module",
"exports": {
"./package.json": "./package.json",
"./paths": "./paths.js",
"./scripts/*": "./scripts/*.mjs"
},
"dependencies": { "dependencies": {
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.9",

78
web/paths.js Normal file
View File

@ -0,0 +1,78 @@
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const relativeDirname = dirname(fileURLToPath(import.meta.url));
//#region Base paths
/**
* @typedef {'@goauthentik/web'} WebPackageIdentifier
*/
/**
* The root of the web package.
*/
export const PackageRoot = /** @type {WebPackageIdentifier} */ (resolve(relativeDirname));
/**
* The name of the distribution directory.
*/
export const DistDirectoryName = "dist";
/**
* Path to the web package's distribution directory.
*
* This is where the built files are located after running the build process.
*/
export const DistDirectory = /** @type {`${WebPackageIdentifier}/${DistDirectoryName}`} */ (
resolve(relativeDirname, DistDirectoryName)
);
//#endregion
//#region Entry points
/**
* @typedef {{ in: string, out: string }} EntryPointTarget
*
* ESBuild entrypoint target.
* Matches the type defined in the ESBuild context.
*/
/**
* Entry points available for building.
*
* @satisfies {Record<string, EntryPointTarget>}
*/
export const EntryPoint = /** @type {const} */ ({
Admin: {
in: resolve(PackageRoot, "src", "admin", "AdminInterface", "index.entrypoint.ts"),
out: resolve(DistDirectory, "admin", "AdminInterface"),
},
User: {
in: resolve(PackageRoot, "src", "user", "index.entrypoint.ts"),
out: resolve(DistDirectory, "user", "UserInterface"),
},
Flow: {
in: resolve(PackageRoot, "src", "flow", "index.entrypoint.ts"),
out: resolve(DistDirectory, "flow", "FlowInterface"),
},
Standalone: {
in: resolve(PackageRoot, "src", "standalone", "api-browser/index.entrypoint.ts"),
out: resolve(DistDirectory, "standalone", "api-browser", "index"),
},
StandaloneLoading: {
in: resolve(PackageRoot, "src", "standalone", "loading/index.entrypoint.ts"),
out: resolve(DistDirectory, "standalone", "loading", "index"),
},
RAC: {
in: resolve(PackageRoot, "src", "rac", "index.entrypoint.ts"),
out: resolve(DistDirectory, "rac", "index"),
},
Polyfill: {
in: resolve(PackageRoot, "src", "polyfill", "index.entrypoint.ts"),
out: resolve(DistDirectory, "poly"),
},
});
//#endregion

View File

@ -4,20 +4,21 @@
* @import { BuildOptions } from "esbuild"; * @import { BuildOptions } from "esbuild";
*/ */
import { liveReloadPlugin } from "@goauthentik/esbuild-plugin-live-reload/plugin"; import { liveReloadPlugin } from "@goauthentik/esbuild-plugin-live-reload/plugin";
import { execFileSync } from "child_process"; import { DistDirectory, DistDirectoryName, EntryPoint, PackageRoot } from "@goauthentik/web/paths";
import { deepmerge } from "deepmerge-ts"; import { deepmerge } from "deepmerge-ts";
import esbuild from "esbuild"; import esbuild from "esbuild";
import { polyfillNode } from "esbuild-plugin-polyfill-node"; import { polyfillNode } from "esbuild-plugin-polyfill-node";
import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs";
import { globSync } from "glob"; import { globSync } from "glob";
import * as path from "path"; import { execFileSync } from "node:child_process";
import { cwd } from "process"; import { copyFileSync, mkdirSync, readFileSync, statSync } from "node:fs";
import process from "process"; import * as fs from "node:fs/promises";
import { fileURLToPath } from "url"; import * as path from "node:path";
import process, { cwd } from "node:process";
import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs"; import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs";
const __dirname = fileURLToPath(new URL(".", import.meta.url)); const logPrefix = "[Build]";
let authentikProjectRoot = path.join(__dirname, "..", ".."); let authentikProjectRoot = path.join(__dirname, "..", "..");
try { try {
@ -46,7 +47,6 @@ const definitions = Object.fromEntries(
return [`process.env.${key}`, JSON.stringify(value)]; return [`process.env.${key}`, JSON.stringify(value)];
}), }),
); );
/** /**
* All is magic is just to make sure the assets are copied into the right places. This is a very * All is magic is just to make sure the assets are copied into the right places. This is a very
* stripped down version of what the rollup-copy-plugin does, without any of the features we don't * stripped down version of what the rollup-copy-plugin does, without any of the features we don't
@ -93,38 +93,23 @@ for (const [source, rawdest, strip] of assetsFileMappings) {
} }
/** /**
* @typedef {[source: string, destination: string]} EntryPoint * @type {Readonly<BuildOptions>}
*/
/**
* This starts the definitions used for esbuild: Our targets, our arguments, the function for
* running a build, and three options for building: watching, building, and building the proxy.
* Ordered by largest to smallest interface to build even faster
*
* @type {EntryPoint[]}
*/
const entryPoints = [
["admin/AdminInterface/AdminInterface.ts", "admin"],
["user/UserInterface.ts", "user"],
["flow/FlowInterface.ts", "flow"],
["standalone/api-browser/index.ts", "standalone/api-browser"],
["rac/index.ts", "rac"],
["standalone/loading/index.ts", "standalone/loading"],
["polyfill/poly.ts", "."],
];
/**
* @type {import("esbuild").BuildOptions}
*/ */
const BASE_ESBUILD_OPTIONS = { const BASE_ESBUILD_OPTIONS = {
entryNames: `[dir]/[name]-${composeVersionID()}`,
chunkNames: "[dir]/chunks/[name]-[hash]",
assetNames: "assets/[dir]/[name]-[hash]",
publicPath: path.join("/static", DistDirectoryName),
outdir: DistDirectory,
bundle: true, bundle: true,
write: true, write: true,
sourcemap: true, sourcemap: true,
minify: NODE_ENV === "production", minify: NODE_ENV === "production",
legalComments: "external",
splitting: true, splitting: true,
treeShaking: true, treeShaking: true,
external: ["*.woff", "*.woff2"], external: ["*.woff", "*.woff2"],
tsconfig: path.resolve(__dirname, "..", "tsconfig.build.json"), tsconfig: path.resolve(PackageRoot, "tsconfig.build.json"),
loader: { loader: {
".css": "text", ".css": "text",
}, },
@ -166,54 +151,43 @@ function composeVersionID() {
return version; return version;
} }
/** async function cleanDistDirectory() {
* Build a single entry point. const timerLabel = `${logPrefix} ♻️ Cleaning previous builds...`;
*
* @param {EntryPoint} buildTarget
* @param {Partial<esbuild.BuildOptions>} [overrides]
* @throws {Error} on build failure
*/
function createEntryPointOptions([source, dest], overrides = {}) {
const outdir = path.join(__dirname, "..", "dist", dest);
/** console.time(timerLabel);
* @type {esbuild.BuildOptions}
*/
const entryPointConfig = { await fs.rm(DistDirectory, {
entryPoints: [`./src/${source}`], recursive: true,
entryNames: `[dir]/[name]-${composeVersionID()}`, force: true,
publicPath: path.join("/static", "dist", dest), });
outdir,
};
/** await fs.mkdir(DistDirectory, {
* @type {esbuild.BuildOptions} recursive: true,
*/ });
const mergedConfig = deepmerge(BASE_ESBUILD_OPTIONS, entryPointConfig, overrides);
return mergedConfig; console.timeEnd(timerLabel);
} }
/** /**
* Build all entry points in parallel. * Creates an ESBuild options, extending the base options with the given overrides.
* *
* @param {EntryPoint[]} entryPoints * @param {BuildOptions} overrides
* @returns {Promise<esbuild.BuildResult[]>} * @returns {BuildOptions}
*/ */
async function buildParallel(entryPoints) { export function createESBuildOptions(overrides) {
return Promise.all( /**
entryPoints.map((entryPoint) => { * @type {BuildOptions}
return esbuild.build(createEntryPointOptions(entryPoint)); */
}), const mergedOptions = deepmerge(BASE_ESBUILD_OPTIONS, overrides);
);
return mergedOptions;
} }
function doHelp() { function doHelp() {
console.log(`Build the authentik UI console.log(`Build the authentik UI
options: options:
-w, --watch: Build all ${entryPoints.length} interfaces -w, --watch: Build all interfaces
-p, --proxy: Build only the polyfills and the loading application -p, --proxy: Build only the polyfills and the loading application
-h, --help: This help message -h, --help: This help message
`); `);
@ -222,27 +196,29 @@ function doHelp() {
} }
async function doWatch() { async function doWatch() {
console.log("Watching all entry points..."); console.group(`${logPrefix} 🤖 Watching entry points`);
const buildContexts = await Promise.all( const entryPoints = Object.entries(EntryPoint).map(([entrypointID, target]) => {
entryPoints.map((entryPoint) => { console.log(entrypointID);
return esbuild.context(
createEntryPointOptions(entryPoint, {
define: definitions,
plugins: [
liveReloadPlugin({
logPrefix: `Build Observer (${entryPoint[1]})`,
relativeRoot: path.join(__dirname, ".."),
}),
],
}),
);
}),
);
await Promise.all(buildContexts.map((context) => context.rebuild())); return target;
});
await Promise.allSettled(buildContexts.map((context) => context.watch())); console.groupEnd();
const buildOptions = createESBuildOptions({
entryPoints,
plugins: [
liveReloadPlugin({
relativeRoot: PackageRoot,
}),
],
});
const buildContext = await esbuild.context(buildOptions);
await buildContext.rebuild();
await buildContext.watch();
return /** @type {Promise<void>} */ ( return /** @type {Promise<void>} */ (
new Promise((resolve) => { new Promise((resolve) => {
@ -254,15 +230,34 @@ async function doWatch() {
} }
async function doBuild() { async function doBuild() {
console.log("Building all entry points"); console.group(`${logPrefix} 🚀 Building entry points:`);
return buildParallel(entryPoints); const entryPoints = Object.entries(EntryPoint).map(([entrypointID, target]) => {
console.log(entrypointID);
return target;
});
console.groupEnd();
const buildOptions = createESBuildOptions({
entryPoints,
});
await esbuild.build(buildOptions);
console.log("Build complete");
} }
async function doProxy() { async function doProxy() {
return buildParallel( const entryPoints = [EntryPoint.StandaloneLoading];
entryPoints.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)),
); const buildOptions = createESBuildOptions({
entryPoints,
});
await esbuild.build(buildOptions);
console.log("Proxy build complete");
} }
async function delegateCommand() { async function delegateCommand() {
@ -284,12 +279,16 @@ async function delegateCommand() {
} }
} }
await delegateCommand() await cleanDistDirectory()
.then(() => { // ---
console.log("Build complete"); .then(() =>
process.exit(0); delegateCommand()
}) .then(() => {
.catch((error) => { console.log("Build complete");
console.error(error); process.exit(0);
process.exit(1); })
}); .catch((error) => {
console.error(error);
process.exit(1);
}),
);

View File

@ -1,5 +0,0 @@
import { AdminInterface } from "./AdminInterface";
import "./AdminInterface";
export { AdminInterface };
export default AdminInterface;

View File

@ -1,4 +1,4 @@
import { AdminInterface } from "@goauthentik/admin/AdminInterface"; import type { AdminInterface } from "@goauthentik/admin/AdminInterface/index.entrypoint.js";
import "@goauthentik/admin/users/ServiceAccountForm"; import "@goauthentik/admin/users/ServiceAccountForm";
import "@goauthentik/admin/users/UserActiveForm"; import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserForm"; import "@goauthentik/admin/users/UserForm";

View File

@ -1,4 +1,4 @@
import type { AdminInterface } from "@goauthentik/admin/AdminInterface/AdminInterface"; import type { AdminInterface } from "@goauthentik/admin/AdminInterface/index.entrypoint.js";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider"; import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";

View File

@ -1,3 +1,4 @@
// sort-imports-ignore
import "construct-style-sheets-polyfill"; import "construct-style-sheets-polyfill";
import "@webcomponents/webcomponentsjs"; import "@webcomponents/webcomponentsjs";
import "lit/polyfill-support.js"; import "lit/polyfill-support.js";

View File

@ -6,7 +6,7 @@ import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Expand"; import "@goauthentik/elements/Expand";
import "@goauthentik/user/LibraryApplication/RACLaunchEndpointModal"; import "@goauthentik/user/LibraryApplication/RACLaunchEndpointModal";
import type { RACLaunchEndpointModal } from "@goauthentik/user/LibraryApplication/RACLaunchEndpointModal"; import type { RACLaunchEndpointModal } from "@goauthentik/user/LibraryApplication/RACLaunchEndpointModal";
import { UserInterface } from "@goauthentik/user/UserInterface"; import type { UserInterface } from "@goauthentik/user/index.entrypoint.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit"; import { CSSResult, TemplateResult, css, html, nothing } from "lit";

View File

@ -5,7 +5,7 @@ import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/user/SessionList"; import "@goauthentik/elements/user/SessionList";
import "@goauthentik/elements/user/UserConsentList"; import "@goauthentik/elements/user/UserConsentList";
import "@goauthentik/elements/user/sources/SourceSettings"; import "@goauthentik/elements/user/sources/SourceSettings";
import { UserInterface } from "@goauthentik/user/UserInterface"; import type { UserInterface } from "@goauthentik/user/index.entrypoint.js";
import "@goauthentik/user/user-settings/details/UserPassword"; import "@goauthentik/user/user-settings/details/UserPassword";
import "@goauthentik/user/user-settings/details/UserSettingsFlowExecutor"; import "@goauthentik/user/user-settings/details/UserSettingsFlowExecutor";
import "@goauthentik/user/user-settings/mfa/MFADevicesPage"; import "@goauthentik/user/user-settings/mfa/MFADevicesPage";