From 82d7e1371e5af597ed84737e6da9f15d2f298b36 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sat, 8 Jun 2024 14:02:07 +0200 Subject: [PATCH] Add code gen for environement variables an theme name --- .../generateMessageProperties.ts | 68 +------------------ .../generateSrcMainResourcesForMainTheme.ts | 8 ++- src/bin/main.ts | 15 ++++ src/bin/shared/buildOptions.ts | 7 +- src/bin/shared/generateKcGenTs.ts | 61 +++++++++++++++++ .../tools/escapeStringForPropertiesFile.ts | 64 +++++++++++++++++ src/bin/update-kc-gen.ts | 13 ++++ src/vite-plugin/vite-plugin.ts | 20 ++++-- 8 files changed, 181 insertions(+), 75 deletions(-) create mode 100644 src/bin/shared/generateKcGenTs.ts create mode 100644 src/bin/tools/escapeStringForPropertiesFile.ts create mode 100644 src/bin/update-kc-gen.ts diff --git a/src/bin/keycloakify/generateSrcMainResources/generateMessageProperties.ts b/src/bin/keycloakify/generateSrcMainResources/generateMessageProperties.ts index 473731c1..fc0b859c 100644 --- a/src/bin/keycloakify/generateSrcMainResources/generateMessageProperties.ts +++ b/src/bin/keycloakify/generateSrcMainResources/generateMessageProperties.ts @@ -8,6 +8,7 @@ import * as recast from "recast"; import * as babelParser from "@babel/parser"; import babelGenerate from "@babel/generator"; import * as babelTypes from "@babel/types"; +import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile"; export function generateMessageProperties(params: { themeSrcDirPath: string; @@ -146,7 +147,7 @@ export function generateMessageProperties(params: { for (const [languageTag, keyValueMap] of Object.entries(keyValueMapByLanguageTag)) { const propertiesFileSource = Object.entries(keyValueMap) - .map(([key, value]) => `${key}=${escapeString(value)}`) + .map(([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`) .join("\n"); out.push({ @@ -164,68 +165,3 @@ export function generateMessageProperties(params: { return out; } - -// Convert a JavaScript string to UTF-16 encoding -function toUTF16(codePoint: number): string { - if (codePoint <= 0xffff) { - // BMP character - return "\\u" + codePoint.toString(16).padStart(4, "0"); - } else { - // Non-BMP character - codePoint -= 0x10000; - let highSurrogate = (codePoint >> 10) + 0xd800; - let lowSurrogate = (codePoint % 0x400) + 0xdc00; - return ( - "\\u" + - highSurrogate.toString(16).padStart(4, "0") + - "\\u" + - lowSurrogate.toString(16).padStart(4, "0") - ); - } -} - -// Escapes special characters for use in a .properties file -function escapeString(str: string): string { - let escapedStr = ""; - for (const char of [...str]) { - const codePoint = char.codePointAt(0); - if (!codePoint) continue; - - switch (char) { - case "\n": - escapedStr += "\\n"; - break; - case "\r": - escapedStr += "\\r"; - break; - case "\t": - escapedStr += "\\t"; - break; - case "\\": - escapedStr += "\\\\"; - break; - case ":": - escapedStr += "\\:"; - break; - case "=": - escapedStr += "\\="; - break; - case "#": - escapedStr += "\\#"; - break; - case "!": - escapedStr += "\\!"; - break; - case "'": - escapedStr += "''"; - break; - default: - if (codePoint > 0x7f) { - escapedStr += toUTF16(codePoint); // Non-ASCII characters - } else { - escapedStr += char; // ASCII character needs no escape - } - } - } - return escapedStr; -} diff --git a/src/bin/keycloakify/generateSrcMainResources/generateSrcMainResourcesForMainTheme.ts b/src/bin/keycloakify/generateSrcMainResources/generateSrcMainResourcesForMainTheme.ts index 46ed88b6..a0ea2ec3 100644 --- a/src/bin/keycloakify/generateSrcMainResources/generateSrcMainResourcesForMainTheme.ts +++ b/src/bin/keycloakify/generateSrcMainResources/generateSrcMainResourcesForMainTheme.ts @@ -38,6 +38,7 @@ import { type MetaInfKeycloakTheme } from "../../shared/metaInfKeycloakThemes"; import { objectEntries } from "tsafe/objectEntries"; +import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile"; export type BuildOptionsLike = BuildOptionsLike_kcContextExclusionsFtlCode & BuildOptionsLike_downloadKeycloakStaticResources & @@ -50,6 +51,7 @@ export type BuildOptionsLike = BuildOptionsLike_kcContextExclusionsFtlCode & urlPathname: string | undefined; reactAppRootDirPath: string; keycloakifyBuildDirPath: string; + environmentVariables: { name: string; default: string }[]; }; assert(); @@ -261,7 +263,11 @@ export async function generateSrcMainResourcesForMainTheme(params: { } assert>(false); })()}`, - ...(buildOptions.extraThemeProperties ?? []) + ...(buildOptions.extraThemeProperties ?? []), + buildOptions.environmentVariables.map( + ({ name, default: defaultValue }) => + `${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}` + ) ].join("\n\n"), "utf8" ) diff --git a/src/bin/main.ts b/src/bin/main.ts index 9fcb71d7..d7d95a45 100644 --- a/src/bin/main.ts +++ b/src/bin/main.ts @@ -205,6 +205,21 @@ program } }); +program + .command({ + name: "update-kc-gen", + description: + "(Webpack/Create-React-App only) Create/update the kc.gen.ts file in your project." + }) + .task({ + skip, + handler: async cliCommandOptions => { + const { command } = await import("./update-kc-gen"); + + await command({ cliCommandOptions }); + } + }); + // Fallback to build command if no command is provided { const [, , ...rest] = process.argv; diff --git a/src/bin/shared/buildOptions.ts b/src/bin/shared/buildOptions.ts index 7ee5e32c..6843ebbf 100644 --- a/src/bin/shared/buildOptions.ts +++ b/src/bin/shared/buildOptions.ts @@ -31,15 +31,17 @@ export type BuildOptions = { assetsDirPath: string; npmWorkspaceRootDirPath: string; kcContextExclusionsFtlCode: string | undefined; + environmentVariables: { name: string; default: string }[]; }; export type UserProvidedBuildOptions = { + themeName?: string | string[]; + environmentVariables?: { name: string; default: string }[]; extraThemeProperties?: string[]; artifactId?: string; groupId?: string; loginThemeResourcesFromKeycloakVersion?: string; keycloakifyBuildDirPath?: string; - themeName?: string | string[]; kcContextExclusionsFtlCode?: string; }; @@ -305,6 +307,7 @@ export function readBuildOptions(params: { return pathJoin(reactAppBuildDirPath, resolvedViteConfig.assetsDir); })(), npmWorkspaceRootDirPath, - kcContextExclusionsFtlCode: userProvidedBuildOptions.kcContextExclusionsFtlCode + kcContextExclusionsFtlCode: userProvidedBuildOptions.kcContextExclusionsFtlCode, + environmentVariables: userProvidedBuildOptions.environmentVariables ?? [] }; } diff --git a/src/bin/shared/generateKcGenTs.ts b/src/bin/shared/generateKcGenTs.ts new file mode 100644 index 00000000..9edf9e30 --- /dev/null +++ b/src/bin/shared/generateKcGenTs.ts @@ -0,0 +1,61 @@ +import { assert } from "tsafe/assert"; +import type { BuildOptions } from "./buildOptions"; +import { getThemeSrcDirPath } from "./getThemeSrcDirPath"; +import * as fs from "fs/promises"; +import { join as pathJoin } from "path"; + +export type BuildOptionsLike = { + reactAppRootDirPath: string; + themeNames: string[]; + environmentVariables: { name: string; default: string }[]; +}; + +assert(); + +export async function generateKcGenTs(params: { + buildOptions: BuildOptionsLike; +}): Promise { + const { buildOptions } = params; + + const { themeSrcDirPath } = getThemeSrcDirPath({ + reactAppRootDirPath: buildOptions.reactAppRootDirPath + }); + + await fs.writeFile( + pathJoin(themeSrcDirPath, "kc.gen.ts"), + Buffer.from( + [ + `/* prettier-ignore-start */`, + ``, + `/* eslint-disable */`, + ``, + `// @ts-nocheck`, + ``, + `// noinspection JSUnusedGlobalSymbols`, + ``, + `// This file is auto-generated by Keycloakify`, + ``, + `export type ThemeName = ${buildOptions.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`, + ``, + `export const themeNames: ThemeName[] = [${buildOptions.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`, + ``, + `export type KcEnvName = ${buildOptions.environmentVariables.length === 0 ? "never" : buildOptions.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`, + ``, + `export const KcEnvNames: KcEnvName[] = [${buildOptions.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`, + ``, + `export const kcEnvDefaults: Record = ${JSON.stringify( + Object.fromEntries( + buildOptions.environmentVariables.map( + ({ name, default: defaultValue }) => [name, defaultValue] + ) + ), + null, + 2 + )}`, + ``, + `/* prettier-ignore-end */` + ].join("\n"), + "utf8" + ) + ); +} diff --git a/src/bin/tools/escapeStringForPropertiesFile.ts b/src/bin/tools/escapeStringForPropertiesFile.ts new file mode 100644 index 00000000..e9e04105 --- /dev/null +++ b/src/bin/tools/escapeStringForPropertiesFile.ts @@ -0,0 +1,64 @@ +// Convert a JavaScript string to UTF-16 encoding +function toUTF16(codePoint: number): string { + if (codePoint <= 0xffff) { + // BMP character + return "\\u" + codePoint.toString(16).padStart(4, "0"); + } else { + // Non-BMP character + codePoint -= 0x10000; + let highSurrogate = (codePoint >> 10) + 0xd800; + let lowSurrogate = (codePoint % 0x400) + 0xdc00; + return ( + "\\u" + + highSurrogate.toString(16).padStart(4, "0") + + "\\u" + + lowSurrogate.toString(16).padStart(4, "0") + ); + } +} + +// Escapes special characters for use in a .properties file +export function escapeStringForPropertiesFile(str: string): string { + let escapedStr = ""; + for (const char of [...str]) { + const codePoint = char.codePointAt(0); + if (!codePoint) continue; + + switch (char) { + case "\n": + escapedStr += "\\n"; + break; + case "\r": + escapedStr += "\\r"; + break; + case "\t": + escapedStr += "\\t"; + break; + case "\\": + escapedStr += "\\\\"; + break; + case ":": + escapedStr += "\\:"; + break; + case "=": + escapedStr += "\\="; + break; + case "#": + escapedStr += "\\#"; + break; + case "!": + escapedStr += "\\!"; + break; + case "'": + escapedStr += "''"; + break; + default: + if (codePoint > 0x7f) { + escapedStr += toUTF16(codePoint); // Non-ASCII characters + } else { + escapedStr += char; // ASCII character needs no escape + } + } + } + return escapedStr; +} diff --git a/src/bin/update-kc-gen.ts b/src/bin/update-kc-gen.ts new file mode 100644 index 00000000..362b1d50 --- /dev/null +++ b/src/bin/update-kc-gen.ts @@ -0,0 +1,13 @@ +import type { CliCommandOptions } from "./main"; +import { readBuildOptions } from "./shared/buildOptions"; +import { generateKcGenTs } from "./shared/generateKcGenTs"; + +export async function command(params: { cliCommandOptions: CliCommandOptions }) { + const { cliCommandOptions } = params; + + const buildOptions = readBuildOptions({ + cliCommandOptions + }); + + await generateKcGenTs({ buildOptions }); +} diff --git a/src/vite-plugin/vite-plugin.ts b/src/vite-plugin/vite-plugin.ts index 5513d921..d5d3e9bb 100644 --- a/src/vite-plugin/vite-plugin.ts +++ b/src/vite-plugin/vite-plugin.ts @@ -17,6 +17,7 @@ import { type ResolvedViteConfig } from "../bin/shared/buildOptions"; import MagicString from "magic-string"; +import { generateKcGenTs } from "../bin/shared/generateKcGenTs"; export type Params = UserProvidedBuildOptions & { postBuild?: (buildOptions: Omit) => Promise; @@ -115,13 +116,20 @@ export function keycloakify(params?: Params) { process.exit(0); } - await copyKeycloakResourcesToPublic({ - buildOptions: readBuildOptions({ - cliCommandOptions: { - reactAppRootDirPath - } - }) + const buildOptions = readBuildOptions({ + cliCommandOptions: { + reactAppRootDirPath + } }); + + await Promise.all([ + copyKeycloakResourcesToPublic({ + buildOptions + }), + generateKcGenTs({ + buildOptions + }) + ]); }, transform: (code, id) => { assert(command !== undefined);