From be2cb10f40ede51db80cda1620b4cd9460ba0539 Mon Sep 17 00:00:00 2001 From: Teffen Ellis Date: Tue, 22 Apr 2025 00:57:32 +0200 Subject: [PATCH] web: Build entrypoints with a single ESBuild context. Clean up entrypoints. --- web/package.json | 5 + web/paths.js | 78 +++++++ web/scripts/build-web.mjs | 191 +++++++++--------- ...{AdminInterface.ts => index.entrypoint.ts} | 0 web/src/admin/AdminInterface/index.ts | 5 - web/src/admin/users/UserListPage.ts | 2 +- web/src/elements/sidebar/SidebarVersion.ts | 2 +- .../{FlowInterface.ts => index.entrypoint.ts} | 0 .../polyfill/{poly.ts => index.entrypoint.ts} | 1 + web/src/rac/{index.ts => index.entrypoint.ts} | 0 .../{index.ts => index.entrypoint.ts} | 0 .../loading/{index.ts => index.entrypoint.ts} | 0 web/src/user/LibraryApplication/index.ts | 2 +- .../{UserInterface.ts => index.entrypoint.ts} | 0 .../user/user-settings/UserSettingsPage.ts | 2 +- 15 files changed, 183 insertions(+), 105 deletions(-) create mode 100644 web/paths.js rename web/src/admin/AdminInterface/{AdminInterface.ts => index.entrypoint.ts} (100%) delete mode 100644 web/src/admin/AdminInterface/index.ts rename web/src/flow/{FlowInterface.ts => index.entrypoint.ts} (100%) rename web/src/polyfill/{poly.ts => index.entrypoint.ts} (91%) rename web/src/rac/{index.ts => index.entrypoint.ts} (100%) rename web/src/standalone/api-browser/{index.ts => index.entrypoint.ts} (100%) rename web/src/standalone/loading/{index.ts => index.entrypoint.ts} (100%) rename web/src/user/{UserInterface.ts => index.entrypoint.ts} (100%) diff --git a/web/package.json b/web/package.json index 4533b81290..d5bb8acb29 100644 --- a/web/package.json +++ b/web/package.json @@ -35,6 +35,11 @@ "watch": "run-s build-locales esbuild:watch" }, "type": "module", + "exports": { + "./package.json": "./package.json", + "./paths": "./paths.js", + "./scripts/*": "./scripts/*.mjs" + }, "dependencies": { "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.9", diff --git a/web/paths.js b/web/paths.js new file mode 100644 index 0000000000..5d9dac17de --- /dev/null +++ b/web/paths.js @@ -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} + */ +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 diff --git a/web/scripts/build-web.mjs b/web/scripts/build-web.mjs index 07a14e246a..d985a0c13f 100644 --- a/web/scripts/build-web.mjs +++ b/web/scripts/build-web.mjs @@ -4,20 +4,21 @@ * @import { BuildOptions } from "esbuild"; */ 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 esbuild from "esbuild"; import { polyfillNode } from "esbuild-plugin-polyfill-node"; -import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs"; import { globSync } from "glob"; -import * as path from "path"; -import { cwd } from "process"; -import process from "process"; -import { fileURLToPath } from "url"; +import { execFileSync } from "node:child_process"; +import { copyFileSync, mkdirSync, readFileSync, statSync } from "node:fs"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import process, { cwd } from "node:process"; import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs"; -const __dirname = fileURLToPath(new URL(".", import.meta.url)); +const logPrefix = "[Build]"; + let authentikProjectRoot = path.join(__dirname, "..", ".."); try { @@ -46,7 +47,6 @@ const definitions = Object.fromEntries( 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 * 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 - */ - -/** - * 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} + * @type {Readonly} */ 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, write: true, sourcemap: true, minify: NODE_ENV === "production", + legalComments: "external", splitting: true, treeShaking: true, external: ["*.woff", "*.woff2"], - tsconfig: path.resolve(__dirname, "..", "tsconfig.build.json"), + tsconfig: path.resolve(PackageRoot, "tsconfig.build.json"), loader: { ".css": "text", }, @@ -166,54 +151,43 @@ function composeVersionID() { return version; } -/** - * Build a single entry point. - * - * @param {EntryPoint} buildTarget - * @param {Partial} [overrides] - * @throws {Error} on build failure - */ -function createEntryPointOptions([source, dest], overrides = {}) { - const outdir = path.join(__dirname, "..", "dist", dest); +async function cleanDistDirectory() { + const timerLabel = `${logPrefix} ♻️ Cleaning previous builds...`; - /** - * @type {esbuild.BuildOptions} - */ + console.time(timerLabel); - const entryPointConfig = { - entryPoints: [`./src/${source}`], - entryNames: `[dir]/[name]-${composeVersionID()}`, - publicPath: path.join("/static", "dist", dest), - outdir, - }; + await fs.rm(DistDirectory, { + recursive: true, + force: true, + }); - /** - * @type {esbuild.BuildOptions} - */ - const mergedConfig = deepmerge(BASE_ESBUILD_OPTIONS, entryPointConfig, overrides); + await fs.mkdir(DistDirectory, { + recursive: true, + }); - 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 - * @returns {Promise} + * @param {BuildOptions} overrides + * @returns {BuildOptions} */ -async function buildParallel(entryPoints) { - return Promise.all( - entryPoints.map((entryPoint) => { - return esbuild.build(createEntryPointOptions(entryPoint)); - }), - ); +export function createESBuildOptions(overrides) { + /** + * @type {BuildOptions} + */ + const mergedOptions = deepmerge(BASE_ESBUILD_OPTIONS, overrides); + + return mergedOptions; } function doHelp() { console.log(`Build the authentik UI options: - -w, --watch: Build all ${entryPoints.length} interfaces + -w, --watch: Build all interfaces -p, --proxy: Build only the polyfills and the loading application -h, --help: This help message `); @@ -222,27 +196,29 @@ function doHelp() { } async function doWatch() { - console.log("Watching all entry points..."); + console.group(`${logPrefix} 🤖 Watching entry points`); - const buildContexts = await Promise.all( - entryPoints.map((entryPoint) => { - return esbuild.context( - createEntryPointOptions(entryPoint, { - define: definitions, - plugins: [ - liveReloadPlugin({ - logPrefix: `Build Observer (${entryPoint[1]})`, - relativeRoot: path.join(__dirname, ".."), - }), - ], - }), - ); - }), - ); + const entryPoints = Object.entries(EntryPoint).map(([entrypointID, target]) => { + console.log(entrypointID); - 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} */ ( new Promise((resolve) => { @@ -254,15 +230,34 @@ async function doWatch() { } 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() { - return buildParallel( - entryPoints.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)), - ); + const entryPoints = [EntryPoint.StandaloneLoading]; + + const buildOptions = createESBuildOptions({ + entryPoints, + }); + + await esbuild.build(buildOptions); + console.log("Proxy build complete"); } async function delegateCommand() { @@ -284,12 +279,16 @@ async function delegateCommand() { } } -await delegateCommand() - .then(() => { - console.log("Build complete"); - process.exit(0); - }) - .catch((error) => { - console.error(error); - process.exit(1); - }); +await cleanDistDirectory() + // --- + .then(() => + delegateCommand() + .then(() => { + console.log("Build complete"); + process.exit(0); + }) + .catch((error) => { + console.error(error); + process.exit(1); + }), + ); diff --git a/web/src/admin/AdminInterface/AdminInterface.ts b/web/src/admin/AdminInterface/index.entrypoint.ts similarity index 100% rename from web/src/admin/AdminInterface/AdminInterface.ts rename to web/src/admin/AdminInterface/index.entrypoint.ts diff --git a/web/src/admin/AdminInterface/index.ts b/web/src/admin/AdminInterface/index.ts deleted file mode 100644 index 570b87e3bc..0000000000 --- a/web/src/admin/AdminInterface/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { AdminInterface } from "./AdminInterface"; -import "./AdminInterface"; - -export { AdminInterface }; -export default AdminInterface; diff --git a/web/src/admin/users/UserListPage.ts b/web/src/admin/users/UserListPage.ts index a0c8b7da6f..f54cf58210 100644 --- a/web/src/admin/users/UserListPage.ts +++ b/web/src/admin/users/UserListPage.ts @@ -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/UserActiveForm"; import "@goauthentik/admin/users/UserForm"; diff --git a/web/src/elements/sidebar/SidebarVersion.ts b/web/src/elements/sidebar/SidebarVersion.ts index f5e0fbed05..3bc80b12e6 100644 --- a/web/src/elements/sidebar/SidebarVersion.ts +++ b/web/src/elements/sidebar/SidebarVersion.ts @@ -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 { AKElement, rootInterface } from "@goauthentik/elements/Base"; import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider"; diff --git a/web/src/flow/FlowInterface.ts b/web/src/flow/index.entrypoint.ts similarity index 100% rename from web/src/flow/FlowInterface.ts rename to web/src/flow/index.entrypoint.ts diff --git a/web/src/polyfill/poly.ts b/web/src/polyfill/index.entrypoint.ts similarity index 91% rename from web/src/polyfill/poly.ts rename to web/src/polyfill/index.entrypoint.ts index 08d103117a..2703f69c89 100644 --- a/web/src/polyfill/poly.ts +++ b/web/src/polyfill/index.entrypoint.ts @@ -1,3 +1,4 @@ +// sort-imports-ignore import "construct-style-sheets-polyfill"; import "@webcomponents/webcomponentsjs"; import "lit/polyfill-support.js"; diff --git a/web/src/rac/index.ts b/web/src/rac/index.entrypoint.ts similarity index 100% rename from web/src/rac/index.ts rename to web/src/rac/index.entrypoint.ts diff --git a/web/src/standalone/api-browser/index.ts b/web/src/standalone/api-browser/index.entrypoint.ts similarity index 100% rename from web/src/standalone/api-browser/index.ts rename to web/src/standalone/api-browser/index.entrypoint.ts diff --git a/web/src/standalone/loading/index.ts b/web/src/standalone/loading/index.entrypoint.ts similarity index 100% rename from web/src/standalone/loading/index.ts rename to web/src/standalone/loading/index.entrypoint.ts diff --git a/web/src/user/LibraryApplication/index.ts b/web/src/user/LibraryApplication/index.ts index 2f672882bc..6a25826f56 100644 --- a/web/src/user/LibraryApplication/index.ts +++ b/web/src/user/LibraryApplication/index.ts @@ -6,7 +6,7 @@ import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import "@goauthentik/elements/Expand"; import "@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 { CSSResult, TemplateResult, css, html, nothing } from "lit"; diff --git a/web/src/user/UserInterface.ts b/web/src/user/index.entrypoint.ts similarity index 100% rename from web/src/user/UserInterface.ts rename to web/src/user/index.entrypoint.ts diff --git a/web/src/user/user-settings/UserSettingsPage.ts b/web/src/user/user-settings/UserSettingsPage.ts index cf4b09ff18..038cfcf3b1 100644 --- a/web/src/user/user-settings/UserSettingsPage.ts +++ b/web/src/user/user-settings/UserSettingsPage.ts @@ -5,7 +5,7 @@ import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/user/SessionList"; import "@goauthentik/elements/user/UserConsentList"; 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/UserSettingsFlowExecutor"; import "@goauthentik/user/user-settings/mfa/MFADevicesPage";