Patch CSS for Keycloak by using relative paths instead of css variables

This commit is contained in:
Joseph Garrone 2024-06-19 01:41:22 +02:00
parent aba725372e
commit 5423a07c47
5 changed files with 87 additions and 373 deletions

View File

@ -1,7 +1,6 @@
import cheerio from "cheerio"; import cheerio from "cheerio";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode"; import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode"; import { replaceImportsInCssCode } 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 type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
@ -28,7 +27,6 @@ assert<BuildContext extends BuildContextLike ? true : false>();
export function generateFtlFilesCodeFactory(params: { export function generateFtlFilesCodeFactory(params: {
themeName: string; themeName: string;
indexHtmlCode: string; indexHtmlCode: string;
cssGlobalsToDefine: Record<string, string>;
buildContext: BuildContextLike; buildContext: BuildContextLike;
keycloakifyVersion: string; keycloakifyVersion: string;
themeType: ThemeType; themeType: ThemeType;
@ -36,7 +34,6 @@ export function generateFtlFilesCodeFactory(params: {
}) { }) {
const { const {
themeName, themeName,
cssGlobalsToDefine,
indexHtmlCode, indexHtmlCode,
buildContext, buildContext,
keycloakifyVersion, keycloakifyVersion,
@ -65,8 +62,9 @@ export function generateFtlFilesCodeFactory(params: {
assert(cssCode !== null); assert(cssCode !== null);
const { fixedCssCode } = replaceImportsInInlineCssCode({ const { fixedCssCode } = replaceImportsInCssCode({
cssCode, cssCode,
fileRelativeDirPath: ".",
buildContext buildContext
}); });
@ -97,21 +95,6 @@ export function generateFtlFilesCodeFactory(params: {
); );
}) })
); );
if (Object.keys(cssGlobalsToDefine).length !== 0) {
$("head").prepend(
[
"",
"<style>",
generateCssCodeToDefineGlobals({
cssGlobalsToDefine,
buildContext
}).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.

View File

@ -1,6 +1,11 @@
import { transformCodebase } from "../../tools/transformCodebase"; import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin, resolve as pathResolve, relative as pathRelative } from "path"; import {
join as pathJoin,
resolve as pathResolve,
relative as pathRelative,
dirname as pathDirname
} from "path";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode"; import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode"; import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { import {
@ -64,8 +69,6 @@ export async function generateResourcesForMainTheme(params: {
return pathJoin(resourcesDirPath, "theme", themeName, themeType); return pathJoin(resourcesDirPath, "theme", themeName, themeType);
}; };
const cssGlobalsToDefine: Record<string, string> = {};
for (const themeType of ["login", "account"] as const) { for (const themeType of ["login", "account"] as const) {
if (!buildContext.recordIsImplementedByThemeType[themeType]) { if (!buildContext.recordIsImplementedByThemeType[themeType]) {
continue; continue;
@ -127,21 +130,14 @@ export async function generateResourcesForMainTheme(params: {
transformCodebase({ transformCodebase({
srcDirPath: buildContext.projectBuildDirPath, srcDirPath: buildContext.projectBuildDirPath,
destDirPath, destDirPath,
transformSourceCode: ({ filePath, sourceCode }) => { transformSourceCode: ({ filePath, fileRelativePath, sourceCode }) => {
if (filePath.endsWith(".css")) { if (filePath.endsWith(".css")) {
const { const { fixedCssCode } = replaceImportsInCssCode({
cssGlobalsToDefine: cssGlobalsToDefineForThisFile, cssCode: sourceCode.toString("utf8"),
fixedCssCode fileRelativeDirPath: pathDirname(fileRelativePath),
} = replaceImportsInCssCode({ buildContext
cssCode: sourceCode.toString("utf8")
}); });
Object.entries(cssGlobalsToDefineForThisFile).forEach(
([key, value]) => {
cssGlobalsToDefine[key] = value;
}
);
return { return {
modifiedSourceCode: Buffer.from(fixedCssCode, "utf8") modifiedSourceCode: Buffer.from(fixedCssCode, "utf8")
}; };
@ -168,7 +164,6 @@ export async function generateResourcesForMainTheme(params: {
indexHtmlCode: fs indexHtmlCode: fs
.readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html")) .readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html"))
.toString("utf8"), .toString("utf8"),
cssGlobalsToDefine,
buildContext, buildContext,
keycloakifyVersion: readThisNpmPackageVersion(), keycloakifyVersion: readThisNpmPackageVersion(),
themeType, themeType,

View File

@ -1,7 +1,6 @@
import * as crypto from "crypto";
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants"; import { posix } from "path";
export type BuildContextLike = { export type BuildContextLike = {
urlPathname: string | undefined; urlPathname: string | undefined;
@ -9,68 +8,37 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export function replaceImportsInCssCode(params: { cssCode: string }): { export function replaceImportsInCssCode(params: {
fixedCssCode: string; cssCode: string;
cssGlobalsToDefine: Record<string, string>; fileRelativeDirPath: 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>;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): { }): {
cssCodeToPrependInHead: string; fixedCssCode: string;
} { } {
const { cssGlobalsToDefine, buildContext } = params; const { cssCode, fileRelativeDirPath, buildContext } = params;
return { const fixedCssCode = cssCode.replace(
cssCodeToPrependInHead: [ /url\(["']?(\/[^/][^)"']+)["']?\)/g,
":root {", (match, assetFileAbsoluteUrlPathname) => {
...Object.keys(cssGlobalsToDefine) if (buildContext.urlPathname !== undefined) {
.map(cssVariableName => if (!assetFileAbsoluteUrlPathname.startsWith(buildContext.urlPathname)) {
[ // NOTE: Should never happen
`--${cssVariableName}:`, return match;
cssGlobalsToDefine[cssVariableName].replace( }
new RegExp( assetFileAbsoluteUrlPathname = assetFileAbsoluteUrlPathname.replace(
`url\\(${(buildContext.urlPathname ?? "/").replace( buildContext.urlPathname,
/\//g, "/"
"\\/" );
)}`, }
"g"
), const assetFileRelativeUrlPathname = posix.relative(
`url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/` fileRelativeDirPath.replace(/\\/g, "/"),
) assetFileAbsoluteUrlPathname.replace(/^\//, "")
].join(" ") );
)
.map(line => ` ${line};`), return `url(${assetFileRelativeUrlPathname})`;
"}" }
].join("\n") );
};
return { fixedCssCode };
} }

View File

@ -1,28 +0,0 @@
import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants";
export type BuildContextLike = {
urlPathname: string | undefined;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function replaceImportsInInlineCssCode(params: {
cssCode: string;
buildContext: BuildContextLike;
}): {
fixedCssCode: string;
} {
const { cssCode, buildContext } = params;
const fixedCssCode = cssCode.replace(
buildContext.urlPathname === undefined
? /url\(["']?\/([^/][^)"']+)["']?\)/g
: new RegExp(`url\\(["']?${buildContext.urlPathname}([^)"']+)["']?\\)`, "g"),
(...[, group]) =>
`url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/${group})`
);
return { fixedCssCode };
}

View File

@ -1,11 +1,6 @@
import { replaceImportsInJsCode_vite } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/vite"; import { replaceImportsInJsCode_vite } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/vite";
import { replaceImportsInJsCode_webpack } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/webpack"; import { replaceImportsInJsCode_webpack } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/webpack";
import { import { replaceImportsInCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInCssCode";
generateCssCodeToDefineGlobals,
replaceImportsInCssCode
} from "keycloakify/bin/keycloakify/replacers/replaceImportsInCssCode";
import { replaceImportsInInlineCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInInlineCssCode";
import { same } from "evt/tools/inDepth/same";
import { expect, it, describe } from "vitest"; import { expect, it, describe } from "vitest";
import { basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/shared/constants"; import { basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/shared/constants";
@ -385,279 +380,80 @@ describe("js replacer - webpack", () => {
}); });
describe("css replacer", () => { describe("css replacer", () => {
it("transforms absolute urls to css globals properly with no urlPathname", () => { it("replaceImportsInCssCode - 1", () => {
const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({ const { fixedCssCode } = replaceImportsInCssCode({
cssCode: ` cssCode: `
.my-div { .my-div {
background: url(/logo192.png) no-repeat center center; background: url(/background.png) no-repeat center center;
} }
.my-div2 { .my-div2 {
background: url(/logo192.png) repeat center center; background: url(/assets/background.png) repeat center center;
} }
.my-div { .my-div3 {
background-image: url(/static/media/something.svg); background-image: url(/assets/media/something.svg);
} }
` `,
}); fileRelativeDirPath: "assets/",
const fixedCssCodeExpected = `
.my-div {
background: var(--urla882a969fd39473) no-repeat center center;
}
.my-div2 {
background: var(--urla882a969fd39473) repeat center center;
}
.my-div {
background-image: var(--urldd75cab58377c19);
}
`;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
const cssGlobalsToDefineExpected = {
urla882a969fd39473: "url(/logo192.png)",
urldd75cab58377c19: "url(/static/media/something.svg)"
};
expect(same(cssGlobalsToDefine, cssGlobalsToDefineExpected)).toBe(true);
const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({
cssGlobalsToDefine,
buildContext: { buildContext: {
urlPathname: undefined urlPathname: undefined
} }
}); });
const cssCodeToPrependInHeadExpected = `
:root {
--urla882a969fd39473: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/logo192.png);
--urldd75cab58377c19: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/static/media/something.svg);
}
`;
expect(isSameCode(cssCodeToPrependInHead, cssCodeToPrependInHeadExpected)).toBe(
true
);
});
it("transforms absolute urls to css globals properly with custom urlPathname", () => {
const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({
cssCode: `
.my-div {
background: url(/x/y/z/logo192.png) no-repeat center center;
}
.my-div2 {
background: url(/x/y/z/logo192.png) no-repeat center center;
}
.my-div {
background-image: url(/x/y/z/static/media/something.svg);
}
`
});
const fixedCssCodeExpected = ` const fixedCssCodeExpected = `
.my-div { .my-div {
background: var(--url749a3139386b2c8) no-repeat center center; background: url(../background.png) no-repeat center center;
} }
.my-div2 { .my-div2 {
background: var(--url749a3139386b2c8) no-repeat center center; background: url(background.png) repeat center center;
} }
.my-div { .my-div3 {
background-image: var(--url8bdc0887b97ac9a); background-image: url(media/something.svg);
} }
`; `;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true); expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
});
const cssGlobalsToDefineExpected = { it("replaceImportsInCssCode - 2", () => {
url749a3139386b2c8: "url(/x/y/z/logo192.png)", const { fixedCssCode } = replaceImportsInCssCode({
url8bdc0887b97ac9a: "url(/x/y/z/static/media/something.svg)" cssCode: `
}; .my-div {
background: url(/a/b/background.png) no-repeat center center;
expect(same(cssGlobalsToDefine, cssGlobalsToDefineExpected)).toBe(true); }
const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({ .my-div2 {
cssGlobalsToDefine, background: url(/a/b/assets/background.png) repeat center center;
}
.my-div3 {
background-image: url(/a/b/assets/media/something.svg);
}
`,
fileRelativeDirPath: "assets/",
buildContext: { buildContext: {
urlPathname: "/x/y/z/" urlPathname: "/a/b/"
} }
}); });
const cssCodeToPrependInHeadExpected = ` const fixedCssCodeExpected = `
:root { .my-div {
--url749a3139386b2c8: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/logo192.png); background: url(../background.png) no-repeat center center;
--url8bdc0887b97ac9a: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/static/media/something.svg); }
.my-div2 {
background: url(background.png) repeat center center;
}
.my-div3 {
background-image: url(media/something.svg);
} }
`; `;
expect(isSameCode(cssCodeToPrependInHead, cssCodeToPrependInHeadExpected)).toBe( expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
true
);
});
});
describe("inline css replacer", () => {
describe("no url pathName", () => {
const cssCode = `
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/fonts/WorkSans/worksans-regular-webfont.woff2") format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("/fonts/WorkSans/worksans-medium-webfont.woff2") format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/fonts/WorkSans/worksans-semibold-webfont.woff2") format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/fonts/WorkSans/worksans-bold-webfont.woff2") format("woff2");
}
`;
it("transforms css for standalone app properly", () => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
cssCode,
buildContext: {
urlPathname: undefined
}
});
const fixedCssCodeExpected = `
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/fonts/WorkSans/worksans-regular-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/fonts/WorkSans/worksans-medium-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/fonts/WorkSans/worksans-semibold-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/fonts/WorkSans/worksans-bold-webfont.woff2)
format("woff2");
}
`;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
});
});
describe("with url pathName", () => {
const cssCode = `
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/x/y/z/fonts/WorkSans/worksans-regular-webfont.woff2") format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("/x/y/z/fonts/WorkSans/worksans-medium-webfont.woff2") format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/x/y/z/fonts/WorkSans/worksans-semibold-webfont.woff2") format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/x/y/z/fonts/WorkSans/worksans-bold-webfont.woff2") format("woff2");
}
`;
it("transforms css for standalone app properly", () => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
cssCode,
buildContext: {
urlPathname: "/x/y/z/"
}
});
const fixedCssCodeExpected = `
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/fonts/WorkSans/worksans-regular-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/fonts/WorkSans/worksans-medium-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/fonts/WorkSans/worksans-semibold-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/fonts/WorkSans/worksans-bold-webfont.woff2)
format("woff2");
}
`;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
});
}); });
}); });