This commit is contained in:
parent
0eb4ab85b3
commit
8bee5d788e
@ -83,6 +83,7 @@
|
|||||||
"react-markdown": "^5.0.3",
|
"react-markdown": "^5.0.3",
|
||||||
"scripting-tools": "^0.19.13",
|
"scripting-tools": "^0.19.13",
|
||||||
"tsafe": "^0.10.1",
|
"tsafe": "^0.10.1",
|
||||||
"tss-react": "^3.7.1"
|
"tss-react": "^3.7.1",
|
||||||
|
"zod": "^3.17.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
177
src/bin/build-keycloak-theme/BuildOptions.ts
Normal file
177
src/bin/build-keycloak-theme/BuildOptions.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { Equals } from "tsafe";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { parse as urlParse } from "url";
|
||||||
|
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
homepage?: string;
|
||||||
|
keycloakify?: {
|
||||||
|
extraPages?: string[];
|
||||||
|
extraThemeProperties?: string[];
|
||||||
|
isAppAndKeycloakServerSharingSameDomain?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = z.object({
|
||||||
|
"name": z.string(),
|
||||||
|
"version": z.string(),
|
||||||
|
"homepage": z.string().optional(),
|
||||||
|
"keycloakify": z
|
||||||
|
.object({
|
||||||
|
"extraPages": z.array(z.string()).optional(),
|
||||||
|
"extraThemeProperties": z.array(z.string()).optional(),
|
||||||
|
"isAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<ReturnType<typeof zParsedPackageJson["parse"]>, ParsedPackageJson>>();
|
||||||
|
|
||||||
|
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
||||||
|
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptions {
|
||||||
|
export type Common = {
|
||||||
|
version: string;
|
||||||
|
themeName: string;
|
||||||
|
extraPages?: string[];
|
||||||
|
extraThemeProperties?: string[];
|
||||||
|
//NOTE: Only for the pom.xml file, questionable utility...
|
||||||
|
groupId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||||
|
|
||||||
|
export namespace ExternalAssets {
|
||||||
|
export type CommonExternalAssets = Common & {
|
||||||
|
isStandalone: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SameDomain = CommonExternalAssets & {
|
||||||
|
isAppAndKeycloakServerSharingSameDomain: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DifferentDomains = CommonExternalAssets & {
|
||||||
|
isAppAndKeycloakServerSharingSameDomain: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readBuildOptions(params: {
|
||||||
|
packageJson: string;
|
||||||
|
CNAME: string | undefined;
|
||||||
|
isExternalAssetsCliParamProvided: boolean;
|
||||||
|
}): BuildOptions {
|
||||||
|
const { packageJson, CNAME, isExternalAssetsCliParamProvided } = params;
|
||||||
|
|
||||||
|
const parsedPackageJson = zParsedPackageJson.parse(JSON.parse(packageJson));
|
||||||
|
|
||||||
|
const url = (() => {
|
||||||
|
const { homepage } = parsedPackageJson;
|
||||||
|
|
||||||
|
let url: URL | undefined = undefined;
|
||||||
|
|
||||||
|
if (homepage !== undefined) {
|
||||||
|
url = new URL(homepage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CNAME !== undefined) {
|
||||||
|
url = new URL(`https://${CNAME.replace(/\s+$/, "")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"origin": url.origin,
|
||||||
|
"pathname": (() => {
|
||||||
|
const out = url.pathname.replace(/([^/])$/, "$1/");
|
||||||
|
|
||||||
|
return out === "/" ? undefined : out;
|
||||||
|
})(),
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const common: BuildOptions.Common = (() => {
|
||||||
|
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
|
||||||
|
|
||||||
|
const { extraPages, extraThemeProperties } = keycloakify ?? {};
|
||||||
|
|
||||||
|
const themeName = name
|
||||||
|
.replace(/^@(.*)/, "$1")
|
||||||
|
.split("/")
|
||||||
|
.join("-");
|
||||||
|
|
||||||
|
return {
|
||||||
|
themeName,
|
||||||
|
"groupId": (() => {
|
||||||
|
const fallbackGroupId = `${themeName}.keycloak`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
(!homepage
|
||||||
|
? fallbackGroupId
|
||||||
|
: urlParse(homepage)
|
||||||
|
.host?.replace(/:[0-9]+$/, "")
|
||||||
|
?.split(".")
|
||||||
|
.reverse()
|
||||||
|
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
"version": version,
|
||||||
|
extraPages,
|
||||||
|
extraThemeProperties,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (isExternalAssetsCliParamProvided) {
|
||||||
|
const commonExternalAssets = id<BuildOptions.ExternalAssets.CommonExternalAssets>({
|
||||||
|
...common,
|
||||||
|
"isStandalone": false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parsedPackageJson.keycloakify?.isAppAndKeycloakServerSharingSameDomain) {
|
||||||
|
return id<BuildOptions.ExternalAssets.SameDomain>({
|
||||||
|
...commonExternalAssets,
|
||||||
|
"isAppAndKeycloakServerSharingSameDomain": true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
assert(
|
||||||
|
url !== undefined,
|
||||||
|
[
|
||||||
|
"Can't compile in external assets mode if we don't know where",
|
||||||
|
"the app will be hosted.",
|
||||||
|
"You should provide a homepage field in the package.json (or create a",
|
||||||
|
"public/CNAME file.",
|
||||||
|
"Alternatively, if your app and the Keycloak server are on the same domain, ",
|
||||||
|
"eg https://example.com is your app and https://example.com/auth is the keycloak",
|
||||||
|
'admin UI, you can set "keycloakify": { "isAppAndKeycloakServerSharingSameDomain": true }',
|
||||||
|
"in your package.json",
|
||||||
|
].join(" "),
|
||||||
|
);
|
||||||
|
|
||||||
|
return id<BuildOptions.ExternalAssets.DifferentDomains>({
|
||||||
|
...commonExternalAssets,
|
||||||
|
"isAppAndKeycloakServerSharingSameDomain": false,
|
||||||
|
"urlOrigin": url.origin,
|
||||||
|
"urlPathname": url.pathname,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id<BuildOptions.Standalone>({
|
||||||
|
...common,
|
||||||
|
"isStandalone": true,
|
||||||
|
"urlPathname": url?.pathname,
|
||||||
|
});
|
||||||
|
}
|
@ -3,76 +3,28 @@ import { generateJavaStackFiles } from "./generateJavaStackFiles";
|
|||||||
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
|
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
|
||||||
import { URL } from "url";
|
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
import { readBuildOptions } from "./BuildOptions";
|
||||||
type ParsedPackageJson = {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
homepage?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const reactProjectDirPath = process.cwd();
|
const reactProjectDirPath = process.cwd();
|
||||||
|
|
||||||
const doUseExternalAssets = process.argv[2]?.toLowerCase() === "--external-assets";
|
|
||||||
|
|
||||||
const parsedPackageJson: ParsedPackageJson = require(pathJoin(reactProjectDirPath, "package.json"));
|
|
||||||
|
|
||||||
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
|
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
|
||||||
export const keycloakThemeEmailDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "keycloak_email");
|
export const keycloakThemeEmailDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "keycloak_email");
|
||||||
|
|
||||||
function sanitizeThemeName(name: string) {
|
|
||||||
return name
|
|
||||||
.replace(/^@(.*)/, "$1")
|
|
||||||
.split("/")
|
|
||||||
.join("-");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
console.log("🔏 Building the keycloak theme...⌚");
|
console.log("🔏 Building the keycloak theme...⌚");
|
||||||
|
|
||||||
const extraPagesId: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraPages"] ?? [];
|
const buildOptions = readBuildOptions({
|
||||||
const extraThemeProperties: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraThemeProperties"] ?? [];
|
"packageJson": fs.readFileSync(pathJoin(reactProjectDirPath, "")).toString("utf8"),
|
||||||
const themeName = sanitizeThemeName(parsedPackageJson.name);
|
"CNAME": fs.readFileSync(pathJoin(reactProjectDirPath, "public", "CNAME")).toString("utf8"),
|
||||||
|
"isExternalAssetsCliParamProvided": process.argv[2]?.toLowerCase() === "--external-assets",
|
||||||
|
});
|
||||||
|
|
||||||
const { doBundleEmailTemplate } = generateKeycloakThemeResources({
|
const { doBundlesEmailTemplate } = generateKeycloakThemeResources({
|
||||||
keycloakThemeBuildingDirPath,
|
keycloakThemeBuildingDirPath,
|
||||||
keycloakThemeEmailDirPath,
|
keycloakThemeEmailDirPath,
|
||||||
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
||||||
themeName,
|
buildOptions,
|
||||||
...(() => {
|
|
||||||
const url = (() => {
|
|
||||||
const { homepage } = parsedPackageJson;
|
|
||||||
|
|
||||||
if (homepage !== undefined) {
|
|
||||||
return new URL(homepage);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cnameFilePath = pathJoin(reactProjectDirPath, "public", "CNAME");
|
|
||||||
|
|
||||||
if (fs.existsSync(cnameFilePath)) {
|
|
||||||
return new URL(`https://${fs.readFileSync(cnameFilePath).toString("utf8").replace(/\s+$/, "")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
})();
|
|
||||||
|
|
||||||
return {
|
|
||||||
"urlPathname": url === undefined ? "/" : url.pathname.replace(/([^/])$/, "$1/"),
|
|
||||||
"urlOrigin": !doUseExternalAssets
|
|
||||||
? undefined
|
|
||||||
: (() => {
|
|
||||||
if (url === undefined) {
|
|
||||||
console.error("ERROR: You must specify 'homepage' in your package.json");
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return url.origin;
|
|
||||||
})(),
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
extraPagesId,
|
|
||||||
extraThemeProperties,
|
|
||||||
//We have to leave it at that otherwise we break our default theme.
|
//We have to leave it at that otherwise we break our default theme.
|
||||||
//Problem is that we can't guarantee that the the old resources
|
//Problem is that we can't guarantee that the the old resources
|
||||||
//will still be available on the newer keycloak version.
|
//will still be available on the newer keycloak version.
|
||||||
@ -80,11 +32,10 @@ export function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { jarFilePath } = generateJavaStackFiles({
|
const { jarFilePath } = generateJavaStackFiles({
|
||||||
"version": parsedPackageJson.version,
|
"version": buildOptions.version,
|
||||||
themeName,
|
|
||||||
"homepage": parsedPackageJson.homepage,
|
|
||||||
keycloakThemeBuildingDirPath,
|
keycloakThemeBuildingDirPath,
|
||||||
doBundleEmailTemplate,
|
doBundlesEmailTemplate,
|
||||||
|
buildOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
child_process.execSync("mvn package", {
|
child_process.execSync("mvn package", {
|
||||||
@ -96,8 +47,8 @@ export function main() {
|
|||||||
|
|
||||||
generateStartKeycloakTestingContainer({
|
generateStartKeycloakTestingContainer({
|
||||||
keycloakThemeBuildingDirPath,
|
keycloakThemeBuildingDirPath,
|
||||||
themeName,
|
|
||||||
"keycloakVersion": containerKeycloakVersion,
|
"keycloakVersion": containerKeycloakVersion,
|
||||||
|
buildOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@ -145,7 +96,7 @@ export function main() {
|
|||||||
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
|
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
|
||||||
'- Create a realm named "myrealm"',
|
'- Create a realm named "myrealm"',
|
||||||
'- Create a client with ID: "myclient", "Root URL": "https://www.keycloak.org/app/" and "Valid redirect URIs": "https://www.keycloak.org/app/*"',
|
'- Create a client with ID: "myclient", "Root URL": "https://www.keycloak.org/app/" and "Valid redirect URIs": "https://www.keycloak.org/app/*"',
|
||||||
`- Select Login Theme: ${themeName} (don't forget to save at the bottom of the page)`,
|
`- Select Login Theme: ${buildOptions.themeName} (don't forget to save at the bottom of the page)`,
|
||||||
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
|
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
|
||||||
"",
|
"",
|
||||||
"Video demoing this process: https://youtu.be/N3wlBoH4hKg",
|
"Video demoing this process: https://youtu.be/N3wlBoH4hKg",
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import cheerio from "cheerio";
|
import cheerio from "cheerio";
|
||||||
import { replaceImportsFromStaticInJsCode, replaceImportsInInlineCssCode, generateCssCodeToDefineGlobals } from "../replaceImportFromStatic";
|
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
|
||||||
|
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode";
|
||||||
|
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { objectKeys } from "tsafe/objectKeys";
|
import { objectKeys } from "tsafe/objectKeys";
|
||||||
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
// https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
|
// https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
|
||||||
export const pageIds = [
|
export const pageIds = [
|
||||||
@ -25,58 +30,111 @@ export const pageIds = [
|
|||||||
"logout-confirm.ftl",
|
"logout-confirm.ftl",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptionsLike {
|
||||||
|
export type Standalone = {
|
||||||
|
isStandalone: true;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||||
|
|
||||||
|
export namespace ExternalAssets {
|
||||||
|
export type CommonExternalAssets = {
|
||||||
|
isStandalone: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SameDomain = CommonExternalAssets & {
|
||||||
|
isAppAndKeycloakServerSharingSameDomain: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DifferentDomains = CommonExternalAssets & {
|
||||||
|
isAppAndKeycloakServerSharingSameDomain: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
export type PageId = typeof pageIds[number];
|
export type PageId = typeof pageIds[number];
|
||||||
|
|
||||||
export function generateFtlFilesCodeFactory(params: {
|
export function generateFtlFilesCodeFactory(params: {
|
||||||
cssGlobalsToDefine: Record<string, string>;
|
|
||||||
indexHtmlCode: string;
|
indexHtmlCode: string;
|
||||||
urlPathname: string;
|
//NOTE: Expected to be an empty object if external assets mode is enabled.
|
||||||
urlOrigin: undefined | string;
|
cssGlobalsToDefine: Record<string, string>;
|
||||||
|
buildOptions: BuildOptionsLike;
|
||||||
}) {
|
}) {
|
||||||
const { cssGlobalsToDefine, indexHtmlCode, urlPathname, urlOrigin } = params;
|
const { cssGlobalsToDefine, indexHtmlCode, buildOptions } = params;
|
||||||
|
|
||||||
const $ = cheerio.load(indexHtmlCode);
|
const $ = cheerio.load(indexHtmlCode);
|
||||||
|
|
||||||
$("script:not([src])").each((...[, element]) => {
|
fix_imports_statements: {
|
||||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
if (!buildOptions.isStandalone && buildOptions.isAppAndKeycloakServerSharingSameDomain) {
|
||||||
"jsCode": $(element).html()!,
|
break fix_imports_statements;
|
||||||
urlOrigin,
|
}
|
||||||
|
|
||||||
|
$("script:not([src])").each((...[, element]) => {
|
||||||
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
|
"jsCode": $(element).html()!,
|
||||||
|
buildOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
$(element).text(fixedJsCode);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(element).text(fixedJsCode);
|
$("style").each((...[, element]) => {
|
||||||
});
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
|
"cssCode": $(element).html()!,
|
||||||
|
buildOptions,
|
||||||
|
});
|
||||||
|
|
||||||
$("style").each((...[, element]) => {
|
$(element).text(fixedCssCode);
|
||||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
|
||||||
"cssCode": $(element).html()!,
|
|
||||||
"urlPathname": params.urlPathname,
|
|
||||||
urlOrigin,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$(element).text(fixedCssCode);
|
(
|
||||||
});
|
[
|
||||||
|
["link", "href"],
|
||||||
|
["script", "src"],
|
||||||
|
] as const
|
||||||
|
).forEach(([selector, attrName]) =>
|
||||||
|
$(selector).each((...[, element]) => {
|
||||||
|
const href = $(element).attr(attrName);
|
||||||
|
|
||||||
(
|
if (href === undefined) {
|
||||||
[
|
return;
|
||||||
["link", "href"],
|
}
|
||||||
["script", "src"],
|
|
||||||
] as const
|
|
||||||
).forEach(([selector, attrName]) =>
|
|
||||||
$(selector).each((...[, element]) => {
|
|
||||||
const href = $(element).attr(attrName);
|
|
||||||
|
|
||||||
if (href === undefined) {
|
$(element).attr(
|
||||||
return;
|
attrName,
|
||||||
}
|
buildOptions.isStandalone
|
||||||
|
? href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
|
||||||
|
: href.replace(/^\//, `${buildOptions.urlOrigin}/`),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
$(element).attr(
|
if (Object.keys(cssGlobalsToDefine).length !== 0) {
|
||||||
attrName,
|
$("head").prepend(
|
||||||
urlOrigin !== undefined
|
[
|
||||||
? href.replace(/^\//, `${urlOrigin}/`)
|
"",
|
||||||
: href.replace(new RegExp(`^${urlPathname.replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/"),
|
"<style>",
|
||||||
|
generateCssCodeToDefineGlobals({
|
||||||
|
cssGlobalsToDefine,
|
||||||
|
buildOptions,
|
||||||
|
}).cssCodeToPrependInHead,
|
||||||
|
"</style>",
|
||||||
|
"",
|
||||||
|
].join("\n"),
|
||||||
);
|
);
|
||||||
}),
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
|
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
|
||||||
const replaceValueBySearchValue = {
|
const replaceValueBySearchValue = {
|
||||||
@ -95,18 +153,6 @@ export function generateFtlFilesCodeFactory(params: {
|
|||||||
|
|
||||||
$("head").prepend(
|
$("head").prepend(
|
||||||
[
|
[
|
||||||
...(Object.keys(cssGlobalsToDefine).length === 0
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
"",
|
|
||||||
"<style>",
|
|
||||||
generateCssCodeToDefineGlobals({
|
|
||||||
cssGlobalsToDefine,
|
|
||||||
urlPathname,
|
|
||||||
}).cssCodeToPrependInHead,
|
|
||||||
"</style>",
|
|
||||||
"",
|
|
||||||
]),
|
|
||||||
"<script>",
|
"<script>",
|
||||||
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
|
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
|
||||||
"</script>",
|
"</script>",
|
||||||
|
@ -1,37 +1,39 @@
|
|||||||
import * as url from "url";
|
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
import type { BuildOptions } from "./BuildOptions";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = {
|
||||||
|
themeName: string;
|
||||||
|
groupId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
export function generateJavaStackFiles(params: {
|
export function generateJavaStackFiles(params: {
|
||||||
version: string;
|
version: string;
|
||||||
themeName: string;
|
|
||||||
homepage?: string;
|
|
||||||
keycloakThemeBuildingDirPath: string;
|
keycloakThemeBuildingDirPath: string;
|
||||||
doBundleEmailTemplate: boolean;
|
doBundlesEmailTemplate: boolean;
|
||||||
|
buildOptions: BuildOptionsLike;
|
||||||
}): {
|
}): {
|
||||||
jarFilePath: string;
|
jarFilePath: string;
|
||||||
} {
|
} {
|
||||||
const { themeName, version, homepage, keycloakThemeBuildingDirPath, doBundleEmailTemplate } = params;
|
const {
|
||||||
|
version,
|
||||||
|
buildOptions: { groupId, themeName },
|
||||||
|
keycloakThemeBuildingDirPath,
|
||||||
|
doBundlesEmailTemplate,
|
||||||
|
} = params;
|
||||||
|
|
||||||
{
|
{
|
||||||
const { pomFileCode } = (function generatePomFileCode(): {
|
const { pomFileCode } = (function generatePomFileCode(): {
|
||||||
pomFileCode: string;
|
pomFileCode: string;
|
||||||
} {
|
} {
|
||||||
const groupId = (() => {
|
|
||||||
const fallbackGroupId = `there.was.no.homepage.field.in.the.package.json.${themeName}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
(!homepage
|
|
||||||
? fallbackGroupId
|
|
||||||
: url
|
|
||||||
.parse(homepage)
|
|
||||||
.host?.replace(/:[0-9]+$/, "")
|
|
||||||
?.split(".")
|
|
||||||
.reverse()
|
|
||||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const artefactId = `${themeName}-keycloak-theme`;
|
const artefactId = `${themeName}-keycloak-theme`;
|
||||||
|
|
||||||
const pomFileCode = [
|
const pomFileCode = [
|
||||||
@ -69,7 +71,7 @@ export function generateJavaStackFiles(params: {
|
|||||||
"themes": [
|
"themes": [
|
||||||
{
|
{
|
||||||
"name": themeName,
|
"name": themeName,
|
||||||
"types": ["login", ...(doBundleEmailTemplate ? ["email"] : [])],
|
"types": ["login", ...(doBundlesEmailTemplate ? ["email"] : [])],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -1,48 +1,76 @@
|
|||||||
import { transformCodebase } from "../tools/transformCodebase";
|
import { transformCodebase } from "../tools/transformCodebase";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin, basename as pathBasename } from "path";
|
import { join as pathJoin, basename as pathBasename } from "path";
|
||||||
import { replaceImportsInCssCode, replaceImportsFromStaticInJsCode } from "./replaceImportFromStatic";
|
import { replaceImportsFromStaticInJsCode } from "./replacers/replaceImportsFromStaticInJsCode";
|
||||||
|
import { replaceImportsInCssCode } from "./replacers/replaceImportsInCssCode";
|
||||||
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
|
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
|
||||||
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
|
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath";
|
import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath";
|
||||||
import { isInside } from "../tools/isInside";
|
import { isInside } from "../tools/isInside";
|
||||||
|
import type { BuildOptions } from "./BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptionsLike {
|
||||||
|
export type Common = {
|
||||||
|
themeName: string;
|
||||||
|
extraPages?: string[];
|
||||||
|
extraThemeProperties?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||||
|
|
||||||
|
export namespace ExternalAssets {
|
||||||
|
export type CommonExternalAssets = Common & {
|
||||||
|
isStandalone: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SameDomain = CommonExternalAssets & {
|
||||||
|
isAppAndKeycloakServerSharingSameDomain: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DifferentDomains = CommonExternalAssets & {
|
||||||
|
isAppAndKeycloakServerSharingSameDomain: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
export function generateKeycloakThemeResources(params: {
|
export function generateKeycloakThemeResources(params: {
|
||||||
themeName: string;
|
|
||||||
reactAppBuildDirPath: string;
|
reactAppBuildDirPath: string;
|
||||||
keycloakThemeBuildingDirPath: string;
|
keycloakThemeBuildingDirPath: string;
|
||||||
keycloakThemeEmailDirPath: string;
|
keycloakThemeEmailDirPath: string;
|
||||||
urlPathname: string;
|
|
||||||
//If urlOrigin is not undefined then it means --externals-assets
|
|
||||||
urlOrigin: undefined | string;
|
|
||||||
extraPagesId: string[];
|
|
||||||
extraThemeProperties: string[];
|
|
||||||
keycloakVersion: string;
|
keycloakVersion: string;
|
||||||
}): { doBundleEmailTemplate: boolean } {
|
buildOptions: BuildOptionsLike;
|
||||||
const {
|
}): { doBundlesEmailTemplate: boolean } {
|
||||||
themeName,
|
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
|
||||||
reactAppBuildDirPath,
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
keycloakThemeEmailDirPath,
|
|
||||||
urlPathname,
|
|
||||||
urlOrigin,
|
|
||||||
extraPagesId,
|
|
||||||
extraThemeProperties,
|
|
||||||
keycloakVersion,
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName, "login");
|
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login");
|
||||||
|
|
||||||
let allCssGlobalsToDefine: Record<string, string> = {};
|
let allCssGlobalsToDefine: Record<string, string> = {};
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
"destDirPath": urlOrigin === undefined ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
|
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
|
||||||
"srcDirPath": reactAppBuildDirPath,
|
"srcDirPath": reactAppBuildDirPath,
|
||||||
"transformSourceCode": ({ filePath, sourceCode }) => {
|
"transformSourceCode": ({ filePath, sourceCode }) => {
|
||||||
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
||||||
if (
|
if (
|
||||||
urlOrigin === undefined &&
|
buildOptions.isStandalone &&
|
||||||
isInside({
|
isInside({
|
||||||
"dirPath": pathJoin(reactAppBuildDirPath, mockTestingSubDirOfPublicDirBasename),
|
"dirPath": pathJoin(reactAppBuildDirPath, mockTestingSubDirOfPublicDirBasename),
|
||||||
filePath,
|
filePath,
|
||||||
@ -51,7 +79,11 @@ export function generateKeycloakThemeResources(params: {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (urlOrigin === undefined && /\.css?$/i.test(filePath)) {
|
if (/\.css?$/i.test(filePath)) {
|
||||||
|
if (!buildOptions.isStandalone) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
|
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
|
||||||
"cssCode": sourceCode.toString("utf8"),
|
"cssCode": sourceCode.toString("utf8"),
|
||||||
});
|
});
|
||||||
@ -61,27 +93,27 @@ export function generateKeycloakThemeResources(params: {
|
|||||||
...cssGlobalsToDefine,
|
...cssGlobalsToDefine,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
|
||||||
"modifiedSourceCode": Buffer.from(fixedCssCode, "utf8"),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\.js?$/i.test(filePath)) {
|
if (/\.js?$/i.test(filePath)) {
|
||||||
|
if (!buildOptions.isStandalone && buildOptions.isAppAndKeycloakServerSharingSameDomain) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
"jsCode": sourceCode.toString("utf8"),
|
"jsCode": sourceCode.toString("utf8"),
|
||||||
urlOrigin,
|
buildOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
|
||||||
"modifiedSourceCode": Buffer.from(fixedJsCode, "utf8"),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return urlOrigin === undefined ? { "modifiedSourceCode": sourceCode } : undefined;
|
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let doBundleEmailTemplate: boolean;
|
let doBundlesEmailTemplate: boolean;
|
||||||
|
|
||||||
email: {
|
email: {
|
||||||
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
|
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
|
||||||
@ -91,11 +123,11 @@ export function generateKeycloakThemeResources(params: {
|
|||||||
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`,
|
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`,
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
);
|
);
|
||||||
doBundleEmailTemplate = false;
|
doBundlesEmailTemplate = false;
|
||||||
break email;
|
break email;
|
||||||
}
|
}
|
||||||
|
|
||||||
doBundleEmailTemplate = true;
|
doBundlesEmailTemplate = true;
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
"srcDirPath": keycloakThemeEmailDirPath,
|
"srcDirPath": keycloakThemeEmailDirPath,
|
||||||
@ -104,13 +136,12 @@ export function generateKeycloakThemeResources(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||||
"cssGlobalsToDefine": allCssGlobalsToDefine,
|
|
||||||
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
|
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
|
||||||
urlPathname,
|
"cssGlobalsToDefine": allCssGlobalsToDefine,
|
||||||
urlOrigin,
|
"buildOptions": buildOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
[...pageIds, ...extraPagesId].forEach(pageId => {
|
[...pageIds, ...(buildOptions.extraPages ?? [])].forEach(pageId => {
|
||||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||||
|
|
||||||
fs.mkdirSync(themeDirPath, { "recursive": true });
|
fs.mkdirSync(themeDirPath, { "recursive": true });
|
||||||
@ -161,8 +192,8 @@ export function generateKeycloakThemeResources(params: {
|
|||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
pathJoin(themeDirPath, "theme.properties"),
|
pathJoin(themeDirPath, "theme.properties"),
|
||||||
Buffer.from("parent=keycloak".concat("\n\n", extraThemeProperties.join("\n\n")), "utf8"),
|
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8"),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { doBundleEmailTemplate };
|
return { doBundlesEmailTemplate };
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,34 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
import type { BuildOptions } from "./BuildOptions";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = {
|
||||||
|
themeName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
|
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
|
||||||
|
|
||||||
const containerName = "keycloak-testing-container";
|
const containerName = "keycloak-testing-container";
|
||||||
|
|
||||||
/** Files for being able to run a hot reload keycloak container */
|
/** Files for being able to run a hot reload keycloak container */
|
||||||
export function generateStartKeycloakTestingContainer(params: { keycloakVersion: string; themeName: string; keycloakThemeBuildingDirPath: string }) {
|
export function generateStartKeycloakTestingContainer(params: {
|
||||||
const { themeName, keycloakThemeBuildingDirPath, keycloakVersion } = params;
|
keycloakVersion: string;
|
||||||
|
keycloakThemeBuildingDirPath: string;
|
||||||
|
buildOptions: BuildOptionsLike;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
keycloakThemeBuildingDirPath,
|
||||||
|
keycloakVersion,
|
||||||
|
buildOptions: { themeName },
|
||||||
|
} = params;
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
|
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
|
||||||
|
@ -1,119 +0,0 @@
|
|||||||
import * as crypto from "crypto";
|
|
||||||
import { ftlValuesGlobalName } from "./ftlValuesGlobalName";
|
|
||||||
|
|
||||||
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; urlOrigin: undefined | string }): { fixedJsCode: string } {
|
|
||||||
/*
|
|
||||||
NOTE:
|
|
||||||
|
|
||||||
When we have urlOrigin defined it means that
|
|
||||||
we are building with --external-assets
|
|
||||||
so we have to make sur that the fixed js code will run
|
|
||||||
inside and outside keycloak.
|
|
||||||
|
|
||||||
When urlOrigin isn't defined we can assume the fixedJsCode
|
|
||||||
will always run in keycloak context.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { jsCode, urlOrigin } = params;
|
|
||||||
|
|
||||||
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
|
|
||||||
new RegExp(`([a-zA-Z]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
|
|
||||||
(...[, n, u, e]) => `
|
|
||||||
${n}[(function(){
|
|
||||||
${
|
|
||||||
urlOrigin === undefined
|
|
||||||
? `
|
|
||||||
Object.defineProperty(${n}, "p", {
|
|
||||||
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
|
|
||||||
set: function (){}
|
|
||||||
});
|
|
||||||
`
|
|
||||||
: `
|
|
||||||
var p= "";
|
|
||||||
Object.defineProperty(${n}, "p", {
|
|
||||||
get: function() { return ("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + p; },
|
|
||||||
set: function (value){ p = value;}
|
|
||||||
});
|
|
||||||
`
|
|
||||||
}
|
|
||||||
return "${u}";
|
|
||||||
})()] = function(${e}) { return "${urlOrigin === undefined ? "/build/" : ""}static/${language}/"`,
|
|
||||||
];
|
|
||||||
|
|
||||||
const fixedJsCode = jsCode
|
|
||||||
.replace(...getReplaceArgs("js"))
|
|
||||||
.replace(...getReplaceArgs("css"))
|
|
||||||
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
|
|
||||||
urlOrigin === undefined
|
|
||||||
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
|
|
||||||
: `("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group} + "static/`,
|
|
||||||
)
|
|
||||||
//TODO: Write a test case for this
|
|
||||||
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
|
|
||||||
urlOrigin === undefined
|
|
||||||
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
|
|
||||||
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group2} + ${group3},`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { fixedJsCode };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function replaceImportsInInlineCssCode(params: { cssCode: string; urlPathname: string; urlOrigin: undefined | string }): {
|
|
||||||
fixedCssCode: string;
|
|
||||||
} {
|
|
||||||
const { cssCode, urlPathname, urlOrigin } = params;
|
|
||||||
|
|
||||||
const fixedCssCode = cssCode.replace(
|
|
||||||
urlPathname === "/" ? /url\(["']?\/([^/][^)"']+)["']?\)/g : new RegExp(`url\\(["']?${urlPathname}([^)"']+)["']?\\)`, "g"),
|
|
||||||
(...[, group]) => `url(${urlOrigin === undefined ? "${url.resourcesPath}/build/" + group : params.urlOrigin + urlPathname + group})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { fixedCssCode };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function replaceImportsInCssCode(params: { cssCode: string }): {
|
|
||||||
fixedCssCode: string;
|
|
||||||
cssGlobalsToDefine: Record<string, string>;
|
|
||||||
} {
|
|
||||||
const { cssCode } = params;
|
|
||||||
|
|
||||||
const cssGlobalsToDefine: Record<string, string> = {};
|
|
||||||
|
|
||||||
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
|
|
||||||
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match),
|
|
||||||
);
|
|
||||||
|
|
||||||
let fixedCssCode = cssCode;
|
|
||||||
|
|
||||||
Object.keys(cssGlobalsToDefine).forEach(
|
|
||||||
cssVariableName =>
|
|
||||||
//NOTE: split/join pattern ~ replace all
|
|
||||||
(fixedCssCode = fixedCssCode.split(cssGlobalsToDefine[cssVariableName]).join(`var(--${cssVariableName})`)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { fixedCssCode, cssGlobalsToDefine };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Record<string, string>; urlPathname: string }): {
|
|
||||||
cssCodeToPrependInHead: string;
|
|
||||||
} {
|
|
||||||
const { cssGlobalsToDefine, urlPathname } = params;
|
|
||||||
|
|
||||||
return {
|
|
||||||
"cssCodeToPrependInHead": [
|
|
||||||
":root {",
|
|
||||||
...Object.keys(cssGlobalsToDefine)
|
|
||||||
.map(cssVariableName =>
|
|
||||||
[
|
|
||||||
`--${cssVariableName}:`,
|
|
||||||
cssGlobalsToDefine[cssVariableName].replace(
|
|
||||||
new RegExp(`url\\(${urlPathname.replace(/\//g, "\\/")}`, "g"),
|
|
||||||
"url(${url.resourcesPath}/build/",
|
|
||||||
),
|
|
||||||
].join(" "),
|
|
||||||
)
|
|
||||||
.map(line => ` ${line};`),
|
|
||||||
"}",
|
|
||||||
].join("\n"),
|
|
||||||
};
|
|
||||||
}
|
|
@ -0,0 +1,83 @@
|
|||||||
|
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptionsLike {
|
||||||
|
export type Standalone = {
|
||||||
|
isStandalone: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = {
|
||||||
|
isStandalone: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }): { fixedJsCode: string } {
|
||||||
|
/*
|
||||||
|
NOTE:
|
||||||
|
|
||||||
|
When we have urlOrigin defined it means that
|
||||||
|
we are building with --external-assets
|
||||||
|
so we have to make sur that the fixed js code will run
|
||||||
|
inside and outside keycloak.
|
||||||
|
|
||||||
|
When urlOrigin isn't defined we can assume the fixedJsCode
|
||||||
|
will always run in keycloak context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { jsCode, buildOptions } = params;
|
||||||
|
|
||||||
|
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
|
||||||
|
new RegExp(`([a-zA-Z]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
|
||||||
|
(...[, n, u, e]) => `
|
||||||
|
${n}[(function(){
|
||||||
|
${
|
||||||
|
buildOptions.isStandalone
|
||||||
|
? `
|
||||||
|
Object.defineProperty(${n}, "p", {
|
||||||
|
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
|
||||||
|
set: function (){}
|
||||||
|
});
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
var p= "";
|
||||||
|
Object.defineProperty(${n}, "p", {
|
||||||
|
get: function() { return ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : "") + p; },
|
||||||
|
set: function (value){ p = value;}
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}
|
||||||
|
return "${u}";
|
||||||
|
})()] = function(${e}) { return "${buildOptions.isStandalone ? "/build/" : ""}static/${language}/"`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const fixedJsCode = jsCode
|
||||||
|
.replace(...getReplaceArgs("js"))
|
||||||
|
.replace(...getReplaceArgs("css"))
|
||||||
|
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
|
||||||
|
buildOptions.isStandalone
|
||||||
|
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
|
||||||
|
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : "") + ${group} + "static/`,
|
||||||
|
)
|
||||||
|
//TODO: Write a test case for this
|
||||||
|
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
|
||||||
|
buildOptions.isStandalone
|
||||||
|
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
|
||||||
|
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : "") + ${group2} + ${group3},`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { fixedJsCode };
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
import * as crypto from "crypto";
|
||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = {
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceImportsInCssCode(params: { cssCode: string }): {
|
||||||
|
fixedCssCode: string;
|
||||||
|
cssGlobalsToDefine: Record<string, string>;
|
||||||
|
} {
|
||||||
|
const { cssCode } = params;
|
||||||
|
|
||||||
|
const cssGlobalsToDefine: Record<string, string> = {};
|
||||||
|
|
||||||
|
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
|
||||||
|
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match),
|
||||||
|
);
|
||||||
|
|
||||||
|
let fixedCssCode = cssCode;
|
||||||
|
|
||||||
|
Object.keys(cssGlobalsToDefine).forEach(
|
||||||
|
cssVariableName =>
|
||||||
|
//NOTE: split/join pattern ~ replace all
|
||||||
|
(fixedCssCode = fixedCssCode.split(cssGlobalsToDefine[cssVariableName]).join(`var(--${cssVariableName})`)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { fixedCssCode, cssGlobalsToDefine };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Record<string, string>; buildOptions: BuildOptionsLike }): {
|
||||||
|
cssCodeToPrependInHead: string;
|
||||||
|
} {
|
||||||
|
const { cssGlobalsToDefine, buildOptions } = params;
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cssCodeToPrependInHead": [
|
||||||
|
":root {",
|
||||||
|
...Object.keys(cssGlobalsToDefine)
|
||||||
|
.map(cssVariableName =>
|
||||||
|
[
|
||||||
|
`--${cssVariableName}:`,
|
||||||
|
cssGlobalsToDefine[cssVariableName].replace(
|
||||||
|
new RegExp(`url\\(${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`, "g"),
|
||||||
|
"url(${url.resourcesPath}/build/",
|
||||||
|
),
|
||||||
|
].join(" "),
|
||||||
|
)
|
||||||
|
.map(line => ` ${line};`),
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptionsLike {
|
||||||
|
export type Common = {
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = Common & {
|
||||||
|
isStandalone: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
|
||||||
|
fixedCssCode: string;
|
||||||
|
} {
|
||||||
|
const { cssCode, buildOptions } = params;
|
||||||
|
|
||||||
|
const fixedCssCode = cssCode.replace(
|
||||||
|
buildOptions.urlPathname === undefined
|
||||||
|
? /url\(["']?\/([^/][^)"']+)["']?\)/g
|
||||||
|
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
|
||||||
|
(...[, group]) =>
|
||||||
|
`url(${
|
||||||
|
buildOptions.isStandalone ? "${url.resourcesPath}/build/" + group : buildOptions.urlOrigin + (buildOptions.urlPathname ?? "/") + group
|
||||||
|
})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { fixedCssCode };
|
||||||
|
}
|
@ -5,13 +5,15 @@ import { setupSampleReactProject, sampleReactProjectDirPath } from "./setupSampl
|
|||||||
setupSampleReactProject();
|
setupSampleReactProject();
|
||||||
|
|
||||||
generateKeycloakThemeResources({
|
generateKeycloakThemeResources({
|
||||||
"themeName": "keycloakify-demo-app",
|
|
||||||
"reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"),
|
"reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"),
|
||||||
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"),
|
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"),
|
||||||
"keycloakThemeEmailDirPath": pathJoin(sampleReactProjectDirPath, "keycloak_email"),
|
"keycloakThemeEmailDirPath": pathJoin(sampleReactProjectDirPath, "keycloak_email"),
|
||||||
"urlPathname": "/keycloakify-demo-app/",
|
|
||||||
"urlOrigin": undefined,
|
|
||||||
"extraPagesId": ["my-custom-page.ftl"],
|
|
||||||
"extraThemeProperties": ["env=test"],
|
|
||||||
"keycloakVersion": "11.0.3",
|
"keycloakVersion": "11.0.3",
|
||||||
|
"buildOptions": {
|
||||||
|
"themeName": "keycloakify-demo-app",
|
||||||
|
"extraPages": ["my-custom-page.ftl"],
|
||||||
|
"extraThemeProperties": ["env=test"],
|
||||||
|
"isStandalone": true,
|
||||||
|
"urlPathname": "/keycloakify-demo-app/",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,20 +1,10 @@
|
|||||||
import {
|
import { replaceImportsFromStaticInJsCode } from "../../bin/build-keycloak-theme/replacers/replaceImportsFromStaticInJsCode";
|
||||||
replaceImportsFromStaticInJsCode,
|
import { generateCssCodeToDefineGlobals, replaceImportsInCssCode } from "../../bin/build-keycloak-theme/replacers/replaceImportsInCssCode";
|
||||||
replaceImportsInInlineCssCode,
|
import { replaceImportsInInlineCssCode } from "../../bin/build-keycloak-theme/replacers/replaceImportsInInlineCssCode";
|
||||||
replaceImportsInCssCode,
|
|
||||||
generateCssCodeToDefineGlobals,
|
|
||||||
} from "../../bin/build-keycloak-theme/replaceImportFromStatic";
|
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { same } from "evt/tools/inDepth/same";
|
import { same } from "evt/tools/inDepth/same";
|
||||||
import { assetIsSameCode } from "../tools/assertIsSameCode";
|
import { assetIsSameCode } from "../tools/assertIsSameCode";
|
||||||
|
|
||||||
/*
|
|
||||||
NOTES:
|
|
||||||
When not compiled with --external-assets urlOrigin will always be undefined regardless of the "homepage" field.
|
|
||||||
When compiled with --external-assets and we have a home page filed like "https://example.com" or "https://example.com/x/y/z" urlOrigin will be "https://example.com"
|
|
||||||
Regardless of if it's compiled with --external-assets or not, if "homepage" is like "https://example.com/x/y/z" urlPathname will be "/x/y/z/"
|
|
||||||
*/
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const jsCodeUntransformed = `
|
const jsCodeUntransformed = `
|
||||||
function f() {
|
function f() {
|
||||||
@ -46,7 +36,9 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
|
|||||||
{
|
{
|
||||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
"jsCode": jsCodeUntransformed,
|
"jsCode": jsCodeUntransformed,
|
||||||
"urlOrigin": undefined,
|
"buildOptions": {
|
||||||
|
"isStandalone": true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fixedJsCodeExpected = `
|
const fixedJsCodeExpected = `
|
||||||
@ -97,7 +89,10 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
|
|||||||
{
|
{
|
||||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
"jsCode": jsCodeUntransformed,
|
"jsCode": jsCodeUntransformed,
|
||||||
"urlOrigin": "https://demo-app.keycloakify.dev",
|
"buildOptions": {
|
||||||
|
"isStandalone": false,
|
||||||
|
"urlOrigin": "https://demo-app.keycloakify.dev",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fixedJsCodeExpected = `
|
const fixedJsCodeExpected = `
|
||||||
@ -189,7 +184,9 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
|
|||||||
|
|
||||||
const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({
|
const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({
|
||||||
cssGlobalsToDefine,
|
cssGlobalsToDefine,
|
||||||
"urlPathname": "/",
|
"buildOptions": {
|
||||||
|
"urlPathname": undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const cssCodeToPrependInHeadExpected = `
|
const cssCodeToPrependInHeadExpected = `
|
||||||
@ -244,7 +241,9 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
|
|||||||
|
|
||||||
const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({
|
const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({
|
||||||
cssGlobalsToDefine,
|
cssGlobalsToDefine,
|
||||||
"urlPathname": "/x/y/z/",
|
"buildOptions": {
|
||||||
|
"urlPathname": "/x/y/z/",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const cssCodeToPrependInHeadExpected = `
|
const cssCodeToPrependInHeadExpected = `
|
||||||
@ -292,8 +291,10 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
|
|||||||
{
|
{
|
||||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
cssCode,
|
cssCode,
|
||||||
"urlOrigin": undefined,
|
"buildOptions": {
|
||||||
"urlPathname": "/",
|
"isStandalone": true,
|
||||||
|
"urlPathname": undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fixedCssCodeExpected = `
|
const fixedCssCodeExpected = `
|
||||||
@ -337,8 +338,11 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
|
|||||||
{
|
{
|
||||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
cssCode,
|
cssCode,
|
||||||
"urlOrigin": "https://demo-app.keycloakify.dev",
|
"buildOptions": {
|
||||||
"urlPathname": "/",
|
"isStandalone": false,
|
||||||
|
"urlOrigin": "https://demo-app.keycloakify.dev",
|
||||||
|
"urlPathname": undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fixedCssCodeExpected = `
|
const fixedCssCodeExpected = `
|
||||||
@ -415,8 +419,10 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
|
|||||||
{
|
{
|
||||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
cssCode,
|
cssCode,
|
||||||
"urlOrigin": undefined,
|
"buildOptions": {
|
||||||
"urlPathname": "/x/y/z/",
|
"isStandalone": true,
|
||||||
|
"urlPathname": "/x/y/z/",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fixedCssCodeExpected = `
|
const fixedCssCodeExpected = `
|
||||||
@ -460,8 +466,11 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
|
|||||||
{
|
{
|
||||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
cssCode,
|
cssCode,
|
||||||
"urlOrigin": "https://demo-app.keycloakify.dev",
|
"buildOptions": {
|
||||||
"urlPathname": "/x/y/z/",
|
"isStandalone": false,
|
||||||
|
"urlOrigin": "https://demo-app.keycloakify.dev",
|
||||||
|
"urlPathname": "/x/y/z/",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fixedCssCodeExpected = `
|
const fixedCssCodeExpected = `
|
||||||
|
@ -1914,3 +1914,8 @@ yocto-queue@^0.1.0:
|
|||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
|
zod@^3.17.10:
|
||||||
|
version "3.17.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.10.tgz#8716a05e6869df6faaa878a44ffe3c79e615defb"
|
||||||
|
integrity sha512-IHXnQYQuOOOL/XgHhgl8YjNxBHi3xX0mVcHmqsvJgcxKkEczPshoWdxqyFwsARpf41E0v9U95WUROqsHHxt0UQ==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user