diff --git a/src/bin/keycloakify/BuildOptions.ts b/src/bin/keycloakify/BuildOptions.ts index e5f76d8f..2c537482 100644 --- a/src/bin/keycloakify/BuildOptions.ts +++ b/src/bin/keycloakify/BuildOptions.ts @@ -4,9 +4,11 @@ import { join as pathJoin } from "path"; import parseArgv from "minimist"; import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath"; import * as fs from "fs"; +import { getParsedKeycloakifyViteConfig, getKeycloakifyBuildDirPath } from "./parsedKeycloakifyViteConfig"; /** Consolidated build option gathered form CLI arguments and config in package.json */ export type BuildOptions = { + bundler: "vite" | "webpack"; isSilent: boolean; themeVersion: string; themeNames: string[]; @@ -25,113 +27,102 @@ export type BuildOptions = { * In this case the urlPathname will be "/my-app/" */ urlPathname: string | undefined; assetsDirPath: string; - bundler: "vite" | "webpack"; }; export function readBuildOptions(params: { reactAppRootDirPath: string; processArgv: string[] }): BuildOptions { const { reactAppRootDirPath, processArgv } = params; - const { isSilentCliParamProvided } = (() => { - const argv = parseArgv(processArgv); - - return { - "isSilentCliParamProvided": typeof argv["silent"] === "boolean" ? argv["silent"] : false - }; - })(); - const parsedPackageJson = getParsedPackageJson({ reactAppRootDirPath }); - const { name, keycloakify = {}, version, homepage } = parsedPackageJson; - - const { extraThemeProperties, groupId, artifactId, doCreateJar, loginThemeResourcesFromKeycloakVersion } = keycloakify ?? {}; + const { parsedKeycloakifyViteConfig } = + getParsedKeycloakifyViteConfig({ + "parsedPackageJson_keycloakify_keycloakifyBuildDirPath": parsedPackageJson.keycloakify?.keycloakifyBuildDirPath, + reactAppRootDirPath + }) ?? {}; const themeNames = (() => { - if (keycloakify.themeName === undefined) { + if (parsedPackageJson.keycloakify?.themeName === undefined) { return [ - name + parsedPackageJson.name .replace(/^@(.*)/, "$1") .split("/") .join("-") ]; } - if (typeof keycloakify.themeName === "string") { - return [keycloakify.themeName]; + if (typeof parsedPackageJson.keycloakify.themeName === "string") { + return [parsedPackageJson.keycloakify.themeName]; } - return keycloakify.themeName; + return parsedPackageJson.keycloakify.themeName; })(); - return { + const { keycloakifyBuildDirPath } = getKeycloakifyBuildDirPath({ + "parsedPackageJson_keycloakify_keycloakifyBuildDirPath": parsedPackageJson.keycloakify?.keycloakifyBuildDirPath, reactAppRootDirPath, + "bundler": parsedKeycloakifyViteConfig !== undefined ? "vite" : "webpack" + }); + //const keycloakifyBuildDirPath = keycloakifyBuildDirPath_vite ?? pathJoin(reactAppRootDirPath, "build_keycloak"); + + return { + "bundler": parsedKeycloakifyViteConfig !== undefined ? "vite" : "webpack", + "isSilent": (() => { + const argv = parseArgv(processArgv); + + return typeof argv["silent"] === "boolean" ? argv["silent"] : false; + })(), + "themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0", themeNames, - "doCreateJar": doCreateJar ?? true, - "artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeNames[0]}-keycloak-theme`, + "extraThemeProperties": parsedPackageJson.keycloakify?.extraThemeProperties, "groupId": (() => { const fallbackGroupId = `${themeNames[0]}.keycloak`; return ( process.env.KEYCLOAKIFY_GROUP_ID ?? - groupId ?? - (!homepage + parsedPackageJson.keycloakify?.groupId ?? + (parsedPackageJson.homepage === undefined ? fallbackGroupId - : urlParse(homepage) + : urlParse(parsedPackageJson.homepage) .host?.replace(/:[0-9]+$/, "") ?.split(".") .reverse() .join(".") ?? fallbackGroupId) + ".keycloak" ); })(), - "themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0", - extraThemeProperties, - "isSilent": isSilentCliParamProvided, - "loginThemeResourcesFromKeycloakVersion": loginThemeResourcesFromKeycloakVersion ?? "11.0.3", - "publicDirPath": (() => { - let { PUBLIC_DIR_PATH } = process.env; + "artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? parsedPackageJson.keycloakify?.artifactId ?? `${themeNames[0]}-keycloak-theme`, + "doCreateJar": parsedPackageJson.keycloakify?.doCreateJar ?? true, + "loginThemeResourcesFromKeycloakVersion": parsedPackageJson.keycloakify?.loginThemeResourcesFromKeycloakVersion ?? "11.0.3", + reactAppRootDirPath, + "reactAppBuildDirPath": (() => { + if (parsedKeycloakifyViteConfig !== undefined) { + return pathJoin(reactAppRootDirPath, parsedKeycloakifyViteConfig.buildDir); + } - if (PUBLIC_DIR_PATH !== undefined) { + if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) { return getAbsoluteAndInOsFormatPath({ - "pathIsh": PUBLIC_DIR_PATH, + "pathIsh": parsedPackageJson.keycloakify?.reactAppBuildDirPath, + "cwd": reactAppRootDirPath + }); + } + + return pathJoin(reactAppRootDirPath, "build"); + })(), + + "publicDirPath": (() => { + if (parsedKeycloakifyViteConfig !== undefined) { + return parsedKeycloakifyViteConfig.publicDirPath; + } + + if (process.env.PUBLIC_DIR_PATH !== undefined) { + return getAbsoluteAndInOsFormatPath({ + "pathIsh": process.env.PUBLIC_DIR_PATH, "cwd": reactAppRootDirPath }); } return pathJoin(reactAppRootDirPath, "public"); })(), - "reactAppBuildDirPath": (() => { - const { reactAppBuildDirPath } = parsedPackageJson.keycloakify ?? {}; - - if (reactAppBuildDirPath !== undefined) { - return getAbsoluteAndInOsFormatPath({ - "pathIsh": reactAppBuildDirPath, - "cwd": reactAppRootDirPath - }); - } - - for (const name of ["build", "dist"]) { - const out = pathJoin(reactAppRootDirPath, name); - - if (!fs.existsSync(out)) { - continue; - } - - return out; - } - - throw new Error("Please use the reactAppBuildDirPath option to specify the build directory of your react app"); - })(), - "keycloakifyBuildDirPath": (() => { - const { keycloakifyBuildDirPath } = parsedPackageJson.keycloakify ?? {}; - - if (keycloakifyBuildDirPath !== undefined) { - return getAbsoluteAndInOsFormatPath({ - "pathIsh": keycloakifyBuildDirPath, - "cwd": reactAppRootDirPath - }); - } - - return pathJoin(reactAppRootDirPath, "build_keycloak"); - })(), + keycloakifyBuildDirPath, "cacheDirPath": pathJoin( (() => { let { XDG_CACHE_HOME } = process.env; diff --git a/src/bin/keycloakify/parsedKeycloakifyViteConfig.ts b/src/bin/keycloakify/parsedKeycloakifyViteConfig.ts index 504fb92e..1de01b06 100644 --- a/src/bin/keycloakify/parsedKeycloakifyViteConfig.ts +++ b/src/bin/keycloakify/parsedKeycloakifyViteConfig.ts @@ -5,20 +5,19 @@ import { z } from "zod"; import { pathJoin } from "../tools/pathJoin"; import { keycloakifyViteConfigJsonBasename } from "../constants"; import type { OptionalIfCanBeUndefined } from "../tools/OptionalIfCanBeUndefined"; +import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath"; export type ParsedKeycloakifyViteConfig = { - reactAppRootDirPath: string; - publicDirPath: string; - assetsDirPath: string; - reactAppBuildDirPath: string; + buildDir: string; + publicDir: string; + assetsDir: string; urlPathname: string | undefined; }; -export const zParsedKeycloakifyViteConfig = z.object({ - "reactAppRootDirPath": z.string(), - "publicDirPath": z.string(), - "assetsDirPath": z.string(), - "reactAppBuildDirPath": z.string(), +const zParsedKeycloakifyViteConfig = z.object({ + "buildDir": z.string(), + "publicDir": z.string(), + "assetsDir": z.string(), "urlPathname": z.string().optional() }); @@ -29,20 +28,33 @@ export const zParsedKeycloakifyViteConfig = z.object({ assert>(); } -let cache: { parsedKeycloakifyViteConfig: ParsedKeycloakifyViteConfig | undefined } | undefined = undefined; +export function getParsedKeycloakifyViteConfig(params: { + reactAppRootDirPath: string; + parsedPackageJson_keycloakify_keycloakifyBuildDirPath: string | undefined; +}): + | { + parsedKeycloakifyViteConfig: ParsedKeycloakifyViteConfig; + } + | undefined { + const { reactAppRootDirPath, parsedPackageJson_keycloakify_keycloakifyBuildDirPath } = params; -export function getParsedKeycloakifyViteConfig(params: { keycloakifyBuildDirPath: string }): ParsedKeycloakifyViteConfig | undefined { - const { keycloakifyBuildDirPath } = params; + const viteConfigTsFilePath = pathJoin(reactAppRootDirPath, "vite.config.ts"); - if (cache !== undefined) { - return cache.parsedKeycloakifyViteConfig; + if (!fs.existsSync(viteConfigTsFilePath)) { + return undefined; } + const { keycloakifyBuildDirPath } = getKeycloakifyBuildDirPath({ + reactAppRootDirPath, + parsedPackageJson_keycloakify_keycloakifyBuildDirPath, + "bundler": "vite" + }); + const parsedKeycloakifyViteConfig = (() => { const keycloakifyViteConfigJsonFilePath = pathJoin(keycloakifyBuildDirPath, keycloakifyViteConfigJsonBasename); if (!fs.existsSync(keycloakifyViteConfigJsonFilePath)) { - return undefined; + throw new Error("Missing Keycloakify Vite plugin output."); } let out: ParsedKeycloakifyViteConfig; @@ -69,11 +81,36 @@ export function getParsedKeycloakifyViteConfig(params: { keycloakifyBuildDirPath return out; })(); - if (parsedKeycloakifyViteConfig === undefined && fs.existsSync(pathJoin(keycloakifyBuildDirPath, "vite.config.ts"))) { - throw new Error("Make sure you have enabled the Keycloakiy plugin in your vite.config.ts"); - } - - cache = { parsedKeycloakifyViteConfig }; - - return parsedKeycloakifyViteConfig; + return { parsedKeycloakifyViteConfig }; +} + +export function getKeycloakifyBuildDirPath(params: { + reactAppRootDirPath: string; + parsedPackageJson_keycloakify_keycloakifyBuildDirPath: string | undefined; + bundler: "vite" | "webpack"; +}) { + const { reactAppRootDirPath, parsedPackageJson_keycloakify_keycloakifyBuildDirPath, bundler } = params; + + const keycloakifyBuildDirPath = (() => { + if (parsedPackageJson_keycloakify_keycloakifyBuildDirPath !== undefined) { + getAbsoluteAndInOsFormatPath({ + "pathIsh": parsedPackageJson_keycloakify_keycloakifyBuildDirPath, + "cwd": reactAppRootDirPath + }); + } + + return pathJoin( + reactAppRootDirPath, + `${(() => { + switch (bundler) { + case "vite": + return "dist"; + case "webpack": + return "build"; + } + })()}_keycloak` + ); + })(); + + return { keycloakifyBuildDirPath }; } diff --git a/src/bin/keycloakify/parsedPackageJson.ts b/src/bin/keycloakify/parsedPackageJson.ts index d649adc5..52905de1 100644 --- a/src/bin/keycloakify/parsedPackageJson.ts +++ b/src/bin/keycloakify/parsedPackageJson.ts @@ -20,7 +20,7 @@ export type ParsedPackageJson = { }; }; -export const zParsedPackageJson = z.object({ +const zParsedPackageJson = z.object({ "name": z.string(), "version": z.string().optional(), "homepage": z.string().optional(), diff --git a/src/bin/tools/getAbsoluteAndInOsFormatPath.ts b/src/bin/tools/getAbsoluteAndInOsFormatPath.ts index 5b64edeb..dd686204 100644 --- a/src/bin/tools/getAbsoluteAndInOsFormatPath.ts +++ b/src/bin/tools/getAbsoluteAndInOsFormatPath.ts @@ -7,6 +7,8 @@ export function getAbsoluteAndInOsFormatPath(params: { pathIsh: string; cwd: str pathOut = pathOut.replace(/\//g, pathSep); + pathOut = pathOut.endsWith(pathSep) ? pathOut.slice(0, -1) : pathOut; + if (!pathIsAbsolute(pathOut)) { pathOut = pathJoin(cwd, pathOut); } diff --git a/src/vite-plugin/vite-plugin.ts b/src/vite-plugin/vite-plugin.ts index 3ac1c990..22a54b86 100644 --- a/src/vite-plugin/vite-plugin.ts +++ b/src/vite-plugin/vite-plugin.ts @@ -1,87 +1,75 @@ -import { join as pathJoin, sep as pathSep } from "path"; +import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path"; import { getParsedPackageJson } from "../bin/keycloakify/parsedPackageJson"; import type { Plugin } from "vite"; import { assert } from "tsafe/assert"; -import { getAbsoluteAndInOsFormatPath } from "../bin/tools/getAbsoluteAndInOsFormatPath"; import * as fs from "fs"; import { keycloakifyViteConfigJsonBasename, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../bin/constants"; -import type { ParsedKeycloakifyViteConfig } from "../bin/keycloakify/parsedKeycloakifyViteConfig"; +import { type ParsedKeycloakifyViteConfig, getKeycloakifyBuildDirPath } from "../bin/keycloakify/parsedKeycloakifyViteConfig"; import { replaceAll } from "../bin/tools/String.prototype.replaceAll"; +import { id } from "tsafe/id"; export function keycloakify(): Plugin { - let keycloakifyViteConfig: ParsedKeycloakifyViteConfig | undefined = undefined; + let reactAppRootDirPath: string | undefined = undefined; + let urlPathname: string | undefined = undefined; return { "name": "keycloakify", "configResolved": resolvedConfig => { - const reactAppRootDirPath = resolvedConfig.root; - const reactAppBuildDirPath = pathJoin(reactAppRootDirPath, resolvedConfig.build.outDir); + reactAppRootDirPath = resolvedConfig.root; + urlPathname = (() => { + let out = resolvedConfig.env.BASE_URL; - keycloakifyViteConfig = { - reactAppRootDirPath, - "publicDirPath": resolvedConfig.publicDir, - "assetsDirPath": pathJoin(reactAppBuildDirPath, resolvedConfig.build.assetsDir), - reactAppBuildDirPath, - "urlPathname": (() => { - let out = resolvedConfig.env.BASE_URL; - - if (out === undefined) { - return undefined; - } - - if (!out.startsWith("/")) { - out = "/" + out; - } - - if (!out.endsWith("/")) { - out += "/"; - } - - return out; - })() - }; - - const parsedPackageJson = getParsedPackageJson({ reactAppRootDirPath }); - - if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) { - throw new Error( - [ - "Please do not use the keycloakify.reactAppBuildDirPath option in your package.json.", - "In Vite setups it's inferred automatically from the vite config." - ].join(" ") - ); - } - - const keycloakifyBuildDirPath = (() => { - const { keycloakifyBuildDirPath } = parsedPackageJson.keycloakify ?? {}; - - if (keycloakifyBuildDirPath !== undefined) { - return getAbsoluteAndInOsFormatPath({ - "pathIsh": keycloakifyBuildDirPath, - "cwd": reactAppRootDirPath - }); + if (out === undefined) { + return undefined; } - return pathJoin(reactAppRootDirPath, "build_keycloak"); + if (!out.startsWith("/")) { + out = "/" + out; + } + + if (!out.endsWith("/")) { + out += "/"; + } + + return out; })(); + const { keycloakifyBuildDirPath } = getKeycloakifyBuildDirPath({ + "parsedPackageJson_keycloakify_keycloakifyBuildDirPath": getParsedPackageJson({ reactAppRootDirPath }).keycloakify + ?.keycloakifyBuildDirPath, + reactAppRootDirPath, + "bundler": "vite" + }); + if (!fs.existsSync(keycloakifyBuildDirPath)) { fs.mkdirSync(keycloakifyBuildDirPath); } fs.writeFileSync( pathJoin(keycloakifyBuildDirPath, keycloakifyViteConfigJsonBasename), - Buffer.from(JSON.stringify(keycloakifyViteConfig, null, 2), "utf8") + Buffer.from( + JSON.stringify( + id({ + "publicDir": pathRelative(reactAppRootDirPath, resolvedConfig.publicDir), + "assetsDir": resolvedConfig.build.assetsDir, + "buildDir": resolvedConfig.build.outDir, + urlPathname + }), + null, + 2 + ), + "utf8" + ) ); }, "transform": (code, id) => { - assert(keycloakifyViteConfig !== undefined); + assert(reactAppRootDirPath !== undefined); let transformedCode: string | undefined = undefined; replace_import_meta_env_base_url_in_source_code: { { - const isWithinSourceDirectory = id.startsWith(pathJoin(keycloakifyViteConfig.publicDirPath, "src") + pathSep); + const isWithinSourceDirectory = id.startsWith(pathJoin(reactAppRootDirPath, "src") + pathSep); if (!isWithinSourceDirectory) { break replace_import_meta_env_base_url_in_source_code; @@ -110,18 +98,20 @@ export function keycloakify(): Plugin { [ `(`, `(${windowToken}.${nameOfTheGlobal} === undefined || import.meta.env.MODE === "development") ?`, - ` "${keycloakifyViteConfig.urlPathname ?? "/"}" :`, + ` "${urlPathname ?? "/"}" :`, ` \`\${${windowToken}.${nameOfTheGlobal}.url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/\``, `)` ].join("") ); } - if (transformedCode !== undefined) { - return { - "code": transformedCode - }; + if (transformedCode === undefined) { + return; } + + return { + "code": transformedCode + }; } }; }