diff --git a/src/bin/keycloakify/generateFtl/generateFtl.ts b/src/bin/keycloakify/generateFtl/generateFtl.ts index 582aeeae..2a81687e 100644 --- a/src/bin/keycloakify/generateFtl/generateFtl.ts +++ b/src/bin/keycloakify/generateFtl/generateFtl.ts @@ -1,7 +1,6 @@ import cheerio from "cheerio"; import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode"; -import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode"; -import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode"; +import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode"; import * as fs from "fs"; import { join as pathJoin } from "path"; import type { BuildContext } from "../../shared/buildContext"; @@ -28,7 +27,6 @@ assert(); export function generateFtlFilesCodeFactory(params: { themeName: string; indexHtmlCode: string; - cssGlobalsToDefine: Record; buildContext: BuildContextLike; keycloakifyVersion: string; themeType: ThemeType; @@ -36,7 +34,6 @@ export function generateFtlFilesCodeFactory(params: { }) { const { themeName, - cssGlobalsToDefine, indexHtmlCode, buildContext, keycloakifyVersion, @@ -65,8 +62,9 @@ export function generateFtlFilesCodeFactory(params: { assert(cssCode !== null); - const { fixedCssCode } = replaceImportsInInlineCssCode({ + const { fixedCssCode } = replaceImportsInCssCode({ cssCode, + fileRelativeDirPath: ".", buildContext }); @@ -97,21 +95,6 @@ export function generateFtlFilesCodeFactory(params: { ); }) ); - - if (Object.keys(cssGlobalsToDefine).length !== 0) { - $("head").prepend( - [ - "", - "", - "" - ].join("\n") - ); - } } //FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later. diff --git a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts index 15c5eb7c..1911310c 100644 --- a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts +++ b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts @@ -1,6 +1,11 @@ import { transformCodebase } from "../../tools/transformCodebase"; 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 { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode"; import { @@ -64,8 +69,6 @@ export async function generateResourcesForMainTheme(params: { return pathJoin(resourcesDirPath, "theme", themeName, themeType); }; - const cssGlobalsToDefine: Record = {}; - for (const themeType of ["login", "account"] as const) { if (!buildContext.recordIsImplementedByThemeType[themeType]) { continue; @@ -127,21 +130,14 @@ export async function generateResourcesForMainTheme(params: { transformCodebase({ srcDirPath: buildContext.projectBuildDirPath, destDirPath, - transformSourceCode: ({ filePath, sourceCode }) => { + transformSourceCode: ({ filePath, fileRelativePath, sourceCode }) => { if (filePath.endsWith(".css")) { - const { - cssGlobalsToDefine: cssGlobalsToDefineForThisFile, - fixedCssCode - } = replaceImportsInCssCode({ - cssCode: sourceCode.toString("utf8") + const { fixedCssCode } = replaceImportsInCssCode({ + cssCode: sourceCode.toString("utf8"), + fileRelativeDirPath: pathDirname(fileRelativePath), + buildContext }); - Object.entries(cssGlobalsToDefineForThisFile).forEach( - ([key, value]) => { - cssGlobalsToDefine[key] = value; - } - ); - return { modifiedSourceCode: Buffer.from(fixedCssCode, "utf8") }; @@ -168,7 +164,6 @@ export async function generateResourcesForMainTheme(params: { indexHtmlCode: fs .readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html")) .toString("utf8"), - cssGlobalsToDefine, buildContext, keycloakifyVersion: readThisNpmPackageVersion(), themeType, diff --git a/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts b/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts index 3f20978d..445fff1c 100644 --- a/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts +++ b/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts @@ -1,7 +1,6 @@ -import * as crypto from "crypto"; import type { BuildContext } from "../../shared/buildContext"; import { assert } from "tsafe/assert"; -import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants"; +import { posix } from "path"; export type BuildContextLike = { urlPathname: string | undefined; @@ -9,68 +8,37 @@ export type BuildContextLike = { assert(); -export function replaceImportsInCssCode(params: { cssCode: string }): { - fixedCssCode: string; - cssGlobalsToDefine: Record; -} { - const { cssCode } = params; - - const cssGlobalsToDefine: Record = {}; - - 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; +export function replaceImportsInCssCode(params: { + cssCode: string; + fileRelativeDirPath: string; buildContext: BuildContextLike; }): { - cssCodeToPrependInHead: string; + fixedCssCode: string; } { - const { cssGlobalsToDefine, buildContext } = params; + const { cssCode, fileRelativeDirPath, buildContext } = params; - return { - cssCodeToPrependInHead: [ - ":root {", - ...Object.keys(cssGlobalsToDefine) - .map(cssVariableName => - [ - `--${cssVariableName}:`, - cssGlobalsToDefine[cssVariableName].replace( - new RegExp( - `url\\(${(buildContext.urlPathname ?? "/").replace( - /\//g, - "\\/" - )}`, - "g" - ), - `url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/` - ) - ].join(" ") - ) - .map(line => ` ${line};`), - "}" - ].join("\n") - }; + const fixedCssCode = cssCode.replace( + /url\(["']?(\/[^/][^)"']+)["']?\)/g, + (match, assetFileAbsoluteUrlPathname) => { + if (buildContext.urlPathname !== undefined) { + if (!assetFileAbsoluteUrlPathname.startsWith(buildContext.urlPathname)) { + // NOTE: Should never happen + return match; + } + assetFileAbsoluteUrlPathname = assetFileAbsoluteUrlPathname.replace( + buildContext.urlPathname, + "/" + ); + } + + const assetFileRelativeUrlPathname = posix.relative( + fileRelativeDirPath.replace(/\\/g, "/"), + assetFileAbsoluteUrlPathname.replace(/^\//, "") + ); + + return `url(${assetFileRelativeUrlPathname})`; + } + ); + + return { fixedCssCode }; } diff --git a/src/bin/keycloakify/replacers/replaceImportsInInlineCssCode.ts b/src/bin/keycloakify/replacers/replaceImportsInInlineCssCode.ts deleted file mode 100644 index 60faea32..00000000 --- a/src/bin/keycloakify/replacers/replaceImportsInInlineCssCode.ts +++ /dev/null @@ -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(); - -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 }; -} diff --git a/test/bin/replacers.spec.ts b/test/bin/replacers.spec.ts index 7cdaa31d..39ac725b 100644 --- a/test/bin/replacers.spec.ts +++ b/test/bin/replacers.spec.ts @@ -1,11 +1,6 @@ import { replaceImportsInJsCode_vite } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/vite"; import { replaceImportsInJsCode_webpack } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/webpack"; -import { - generateCssCodeToDefineGlobals, - replaceImportsInCssCode -} from "keycloakify/bin/keycloakify/replacers/replaceImportsInCssCode"; -import { replaceImportsInInlineCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInInlineCssCode"; -import { same } from "evt/tools/inDepth/same"; +import { replaceImportsInCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInCssCode"; import { expect, it, describe } from "vitest"; import { basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/shared/constants"; @@ -385,279 +380,80 @@ describe("js replacer - webpack", () => { }); describe("css replacer", () => { - it("transforms absolute urls to css globals properly with no urlPathname", () => { - const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({ + it("replaceImportsInCssCode - 1", () => { + const { fixedCssCode } = replaceImportsInCssCode({ cssCode: ` .my-div { - background: url(/logo192.png) no-repeat center center; + background: url(/background.png) no-repeat center center; } .my-div2 { - background: url(/logo192.png) repeat center center; + background: url(/assets/background.png) repeat center center; } - .my-div { - background-image: url(/static/media/something.svg); + .my-div3 { + background-image: url(/assets/media/something.svg); } - ` - }); - - 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, + `, + fileRelativeDirPath: "assets/", buildContext: { 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 = ` .my-div { - background: var(--url749a3139386b2c8) no-repeat center center; + background: url(../background.png) no-repeat center center; } .my-div2 { - background: var(--url749a3139386b2c8) no-repeat center center; + background: url(background.png) repeat center center; } - .my-div { - background-image: var(--url8bdc0887b97ac9a); + .my-div3 { + background-image: url(media/something.svg); } `; expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true); + }); - const cssGlobalsToDefineExpected = { - url749a3139386b2c8: "url(/x/y/z/logo192.png)", - url8bdc0887b97ac9a: "url(/x/y/z/static/media/something.svg)" - }; - - expect(same(cssGlobalsToDefine, cssGlobalsToDefineExpected)).toBe(true); - - const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({ - cssGlobalsToDefine, + it("replaceImportsInCssCode - 2", () => { + const { fixedCssCode } = replaceImportsInCssCode({ + cssCode: ` + .my-div { + background: url(/a/b/background.png) no-repeat center center; + } + + .my-div2 { + background: url(/a/b/assets/background.png) repeat center center; + } + + .my-div3 { + background-image: url(/a/b/assets/media/something.svg); + } + `, + fileRelativeDirPath: "assets/", buildContext: { - urlPathname: "/x/y/z/" + urlPathname: "/a/b/" } }); - const cssCodeToPrependInHeadExpected = ` - :root { - --url749a3139386b2c8: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/logo192.png); - --url8bdc0887b97ac9a: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/static/media/something.svg); + const fixedCssCodeExpected = ` + .my-div { + background: url(../background.png) no-repeat center center; + } + + .my-div2 { + background: url(background.png) repeat center center; + } + + .my-div3 { + background-image: url(media/something.svg); } `; - expect(isSameCode(cssCodeToPrependInHead, cssCodeToPrependInHeadExpected)).toBe( - 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); - }); + expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true); }); });