302 lines
8.6 KiB
JavaScript
302 lines
8.6 KiB
JavaScript
import { DistDirectory, PackageRoot } from "@goauthentik/web/paths";
|
|
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";
|
|
import { cwd } from "process";
|
|
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, "..", "..");
|
|
|
|
try {
|
|
// Use the package.json file in the root folder, as it has the current version information.
|
|
authentikProjectRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
encoding: "utf8",
|
|
}).replace("\n", "");
|
|
} catch (_error) {
|
|
// We probably don't have a .git folder, which could happen in container builds.
|
|
}
|
|
|
|
const packageJSONPath = path.join(authentikProjectRoot, "./package.json");
|
|
const rootPackage = JSON.parse(readFileSync(packageJSONPath, "utf8"));
|
|
|
|
const NODE_ENV = process.env.NODE_ENV || "development";
|
|
const AK_API_BASE_PATH = process.env.AK_API_BASE_PATH || "";
|
|
|
|
const environmentVars = new Map([
|
|
["NODE_ENV", NODE_ENV],
|
|
["CWD", cwd()],
|
|
["AK_API_BASE_PATH", AK_API_BASE_PATH],
|
|
]);
|
|
|
|
const definitions = Object.fromEntries(
|
|
Array.from(environmentVars).map(([key, 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
|
|
* stripped down version of what the rollup-copy-plugin does, without any of the features we don't
|
|
* use, and using globSync instead of globby since we already had globSync lying around thanks to
|
|
* Typescript. If there's a third argument in an array entry, it's used to replace the internal path
|
|
* before concatenating it all together as the destination target.
|
|
* @type {Array<[string, string, string?]>}
|
|
*/
|
|
const assetsFileMappings = [
|
|
["node_modules/@patternfly/patternfly/patternfly.min.css", "."],
|
|
["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"],
|
|
["src/common/styles/**", "."],
|
|
["src/assets/images/**", "./assets/images"],
|
|
["./icons/*", "./assets/icons"],
|
|
];
|
|
|
|
/**
|
|
* @param {string} filePath
|
|
*/
|
|
const isFile = (filePath) => statSync(filePath).isFile();
|
|
|
|
/**
|
|
* @param {string} src Source file
|
|
* @param {string} dest Destination folder
|
|
* @param {string} [strip] Path to strip from the source file
|
|
*/
|
|
function nameCopyTarget(src, dest, strip) {
|
|
const target = path.join(dest, strip ? src.replace(strip, "") : path.parse(src).base);
|
|
return [src, target];
|
|
}
|
|
|
|
for (const [source, rawdest, strip] of assetsFileMappings) {
|
|
const matchedPaths = globSync(source);
|
|
const dest = path.join("dist", rawdest);
|
|
|
|
const copyTargets = matchedPaths.map((path) => nameCopyTarget(path, dest, strip));
|
|
|
|
for (const [src, dest] of copyTargets) {
|
|
if (isFile(src)) {
|
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
copyFileSync(src, dest);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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}
|
|
*/
|
|
const BASE_ESBUILD_OPTIONS = {
|
|
bundle: true,
|
|
write: true,
|
|
sourcemap: true,
|
|
minify: NODE_ENV === "production",
|
|
splitting: true,
|
|
treeShaking: true,
|
|
external: ["*.woff", "*.woff2"],
|
|
tsconfig: "./tsconfig.json",
|
|
loader: {
|
|
".css": "text",
|
|
},
|
|
plugins: [
|
|
polyfillNode({
|
|
polyfills: {
|
|
path: true,
|
|
},
|
|
}),
|
|
mdxPlugin({
|
|
root: authentikProjectRoot,
|
|
}),
|
|
],
|
|
define: definitions,
|
|
format: "esm",
|
|
logOverride: {
|
|
/**
|
|
* HACK: Silences issue originating in ESBuild.
|
|
*
|
|
* @see {@link https://github.com/evanw/esbuild/blob/b914dd30294346aa15fcc04278f4b4b51b8b43b5/internal/logger/msg_ids.go#L211 ESBuild source}
|
|
* @expires 2025-08-11
|
|
*/
|
|
"invalid-source-url": "silent",
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Creates a version ID for the build.
|
|
* @returns {string}
|
|
*/
|
|
function composeVersionID() {
|
|
const { version } = rootPackage;
|
|
const buildHash = process.env.GIT_BUILD_HASH;
|
|
|
|
if (buildHash) {
|
|
return `${version}+${buildHash}`;
|
|
}
|
|
|
|
return version;
|
|
}
|
|
|
|
/**
|
|
* Build a single entry point.
|
|
*
|
|
* @param {EntryPoint} buildTarget
|
|
* @param {Partial<esbuild.BuildOptions>} [overrides]
|
|
* @throws {Error} on build failure
|
|
*/
|
|
function createEntryPointOptions([source, dest], overrides = {}) {
|
|
const outdir = path.join(DistDirectory, dest);
|
|
|
|
/**
|
|
* @type {esbuild.BuildOptions}
|
|
*/
|
|
|
|
const entryPointConfig = {
|
|
entryPoints: [`./src/${source}`],
|
|
entryNames: `[dir]/[name]-${composeVersionID()}`,
|
|
publicPath: path.join("/static", "dist", dest),
|
|
outdir,
|
|
};
|
|
|
|
/**
|
|
* @type {esbuild.BuildOptions}
|
|
*/
|
|
const mergedConfig = deepmerge(BASE_ESBUILD_OPTIONS, entryPointConfig, overrides);
|
|
|
|
return mergedConfig;
|
|
}
|
|
|
|
/**
|
|
* Build all entry points in parallel.
|
|
*
|
|
* @param {EntryPoint[]} entryPoints
|
|
* @returns {Promise<esbuild.BuildResult[]>}
|
|
*/
|
|
async function buildParallel(entryPoints) {
|
|
return Promise.all(
|
|
entryPoints.map((entryPoint) => {
|
|
return esbuild.build(createEntryPointOptions(entryPoint));
|
|
}),
|
|
);
|
|
}
|
|
|
|
function doHelp() {
|
|
console.log(`Build the authentik UI
|
|
|
|
options:
|
|
-w, --watch: Build all ${entryPoints.length} interfaces
|
|
-p, --proxy: Build only the polyfills and the loading application
|
|
-h, --help: This help message
|
|
`);
|
|
|
|
process.exit(0);
|
|
}
|
|
|
|
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`);
|
|
|
|
return esbuild.context(
|
|
createEntryPointOptions(entryPoint, {
|
|
plugins: [
|
|
buildObserverPlugin({
|
|
serverURL,
|
|
logPrefix: entryPoint[1],
|
|
relativeRoot: PackageRoot,
|
|
}),
|
|
],
|
|
define: {
|
|
...definitions,
|
|
"process.env.WATCHER_URL": JSON.stringify(serverURL.toString()),
|
|
},
|
|
}),
|
|
);
|
|
}),
|
|
);
|
|
|
|
await Promise.all(buildContexts.map((context) => context.rebuild()));
|
|
|
|
await Promise.allSettled(buildContexts.map((context) => context.watch()));
|
|
|
|
return /** @type {Promise<void>} */ (
|
|
new Promise((resolve) => {
|
|
process.on("SIGINT", () => {
|
|
resolve();
|
|
});
|
|
})
|
|
);
|
|
}
|
|
|
|
async function doBuild() {
|
|
console.log("Building all entry points");
|
|
|
|
return buildParallel(entryPoints);
|
|
}
|
|
|
|
async function doProxy() {
|
|
return buildParallel(
|
|
entryPoints.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)),
|
|
);
|
|
}
|
|
|
|
async function delegateCommand() {
|
|
const command = process.argv[2];
|
|
|
|
switch (command) {
|
|
case "-h":
|
|
case "--help":
|
|
return doHelp();
|
|
case "-w":
|
|
case "--watch":
|
|
return doWatch();
|
|
// There's no watch-for-proxy, sorry.
|
|
case "-p":
|
|
case "--proxy":
|
|
return doProxy();
|
|
default:
|
|
return doBuild();
|
|
}
|
|
}
|
|
|
|
await delegateCommand()
|
|
.then(() => {
|
|
console.log("Build complete");
|
|
process.exit(0);
|
|
})
|
|
.catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|