Merge pull request #404 from keycloakify/smaller_jar_size

Smaller jar size
This commit is contained in:
Joseph Garrone 2023-08-24 09:02:03 +02:00 committed by GitHub
commit 7cc40e2453
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 685 additions and 659 deletions

View File

@ -24,9 +24,9 @@ async function main() {
fs.rmSync(tmpDirPath, { "recursive": true, "force": true }); fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
await downloadBuiltinKeycloakTheme({ await downloadBuiltinKeycloakTheme({
"projectDirPath": getProjectRoot(),
keycloakVersion, keycloakVersion,
"destDirPath": tmpDirPath, "destDirPath": tmpDirPath
isSilent
}); });
type Dictionary = { [idiomId: string]: string }; type Dictionary = { [idiomId: string]: string };

View File

@ -17,9 +17,11 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
const { isReady } = usePrepareTemplate({ const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss, "doFetchDefaultThemeResources": doUseDefaultCss,
url, "styles": [
"stylesCommon": ["node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css"], `${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
"styles": ["css/account.css"], `${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
`${url.resourcesPath}/css/account.css`
],
"htmlClassName": undefined, "htmlClassName": undefined,
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass")) "bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass"))
}); });

View File

@ -24,10 +24,11 @@ import * as fs from "fs";
for (const themeType of themeTypes) { for (const themeType of themeTypes) {
await downloadKeycloakStaticResources({ await downloadKeycloakStaticResources({
"isSilent": false, projectDirPath,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets, "keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
"themeType": themeType, "themeType": themeType,
"themeDirPath": keycloakDirInPublicDir "themeDirPath": keycloakDirInPublicDir,
"usedResources": undefined
}); });
} }

View File

@ -4,19 +4,76 @@ import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion"; import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getLogger } from "./tools/logger"; import { getLogger } from "./tools/logger";
import { readBuildOptions } from "./keycloakify/BuildOptions"; import { readBuildOptions } from "./keycloakify/BuildOptions";
import * as child_process from "child_process";
import * as fs from "fs";
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) { export async function downloadBuiltinKeycloakTheme(params: { projectDirPath: string; keycloakVersion: string; destDirPath: string }) {
const { keycloakVersion, destDirPath } = params; const { projectDirPath, keycloakVersion, destDirPath } = params;
await Promise.all( const start = Date.now();
["", "-community"].map(ext =>
downloadAndUnzip({ await downloadAndUnzip({
"destDirPath": destDirPath, "doUseCache": true,
projectDirPath,
destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`, "url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme` "specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
}) "preCacheTransform": {
) "actionCacheId": "npm install and build",
); "action": async ({ destDirPath }) => {
install_common_node_modules: {
const commonResourcesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources");
if (!fs.existsSync(commonResourcesDirPath)) {
break install_common_node_modules;
}
if (!fs.existsSync(pathJoin(commonResourcesDirPath, "package.json"))) {
break install_common_node_modules;
}
if (fs.existsSync(pathJoin(commonResourcesDirPath, "node_modules"))) {
break install_common_node_modules;
}
child_process.execSync("npm install --omit=dev", {
"cwd": commonResourcesDirPath,
"stdio": "ignore"
});
}
install_and_move_to_common_resources_generated_in_keycloak_v2: {
const accountV2DirSrcDirPath = pathJoin(destDirPath, "keycloak.v2", "account", "src");
if (!fs.existsSync(accountV2DirSrcDirPath)) {
break install_and_move_to_common_resources_generated_in_keycloak_v2;
}
child_process.execSync("npm install", { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
const packageJsonFilePath = pathJoin(accountV2DirSrcDirPath, "package.json");
const packageJsonRaw = fs.readFileSync(packageJsonFilePath);
const parsedPackageJson = JSON.parse(packageJsonRaw.toString("utf8"));
parsedPackageJson.scripts.build = parsedPackageJson.scripts.build
.replace("npm run check-types", "true")
.replace("npm run babel", "true");
fs.writeFileSync(packageJsonFilePath, Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8"));
child_process.execSync("npm run build", { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
fs.writeFileSync(packageJsonFilePath, packageJsonRaw);
fs.rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true });
}
}
}
});
console.log("Downloaded Keycloak theme in", Date.now() - start, "ms");
} }
async function main() { async function main() {
@ -33,9 +90,9 @@ async function main() {
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`); logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
await downloadBuiltinKeycloakTheme({ await downloadBuiltinKeycloakTheme({
"projectDirPath": process.cwd(),
keycloakVersion, keycloakVersion,
destDirPath, destDirPath
"isSilent": buildOptions.isSilent
}); });
} }

View File

@ -10,15 +10,17 @@ import { getLogger } from "./tools/logger";
import { getThemeSrcDirPath } from "./getSrcDirPath"; import { getThemeSrcDirPath } from "./getSrcDirPath";
export async function main() { export async function main() {
const projectDirPath = process.cwd();
const { isSilent } = readBuildOptions({ const { isSilent } = readBuildOptions({
"projectDirPath": process.cwd(), projectDirPath,
"processArgv": process.argv.slice(2) "processArgv": process.argv.slice(2)
}); });
const logger = getLogger({ isSilent }); const logger = getLogger({ isSilent });
const { themeSrcDirPath } = getThemeSrcDirPath({ const { themeSrcDirPath } = getThemeSrcDirPath({
"projectDirPath": process.cwd() projectDirPath
}); });
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email"); const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
@ -34,9 +36,9 @@ export async function main() {
const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme"); const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
await downloadBuiltinKeycloakTheme({ await downloadBuiltinKeycloakTheme({
projectDirPath,
keycloakVersion, keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath, "destDirPath": builtinKeycloakThemeTmpDirPath
isSilent
}); });
transformCodebase({ transformCodebase({

View File

@ -4,15 +4,11 @@ import { parse as urlParse } from "url";
import { typeGuard } from "tsafe/typeGuard"; import { typeGuard } from "tsafe/typeGuard";
import { symToStr } from "tsafe/symToStr"; import { symToStr } from "tsafe/symToStr";
import { bundlers, getParsedPackageJson, type Bundler } from "./parsedPackageJson"; import { bundlers, getParsedPackageJson, type Bundler } from "./parsedPackageJson";
import * as fs from "fs";
import { join as pathJoin, sep as pathSep } from "path"; import { join as pathJoin, sep as pathSep } from "path";
import parseArgv from "minimist"; import parseArgv from "minimist";
/** Consolidated build option gathered form CLI arguments and config in package.json */ /** Consolidated build option gathered form CLI arguments and config in package.json */
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets; export type BuildOptions = {
export namespace BuildOptions {
export type Common = {
isSilent: boolean; isSilent: boolean;
themeVersion: string; themeVersion: string;
themeName: string; themeName: string;
@ -26,84 +22,24 @@ export namespace BuildOptions {
reactAppBuildDirPath: string; reactAppBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */ /** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
}; /** If your app is hosted under a subpath, it's the case in CRA if you have "homepage": "https://example.com/my-app" in your package.json
* In this case the urlPathname will be "/my-app/" */
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined; urlPathname: string | undefined;
}; };
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = Common & {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
export function readBuildOptions(params: { projectDirPath: string; processArgv: string[] }): BuildOptions { export function readBuildOptions(params: { projectDirPath: string; processArgv: string[] }): BuildOptions {
const { projectDirPath, processArgv } = params; const { projectDirPath, processArgv } = params;
const { isExternalAssetsCliParamProvided, isSilentCliParamProvided } = (() => { const { isSilentCliParamProvided } = (() => {
const argv = parseArgv(processArgv); const argv = parseArgv(processArgv);
return { return {
"isSilentCliParamProvided": typeof argv["silent"] === "boolean" ? argv["silent"] : false, "isSilentCliParamProvided": typeof argv["silent"] === "boolean" ? argv["silent"] : false
"isExternalAssetsCliParamProvided": typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
}; };
})(); })();
const parsedPackageJson = getParsedPackageJson({ projectDirPath }); const parsedPackageJson = getParsedPackageJson({ projectDirPath });
const url = (() => {
const { homepage } = parsedPackageJson;
let url: URL | undefined = undefined;
if (homepage !== undefined) {
url = new URL(homepage);
}
const CNAME = (() => {
const cnameFilePath = pathJoin(projectDirPath, "public", "CNAME");
if (!fs.existsSync(cnameFilePath)) {
return undefined;
}
return fs.readFileSync(cnameFilePath).toString("utf8");
})();
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 { name, keycloakify = {}, version, homepage } = parsedPackageJson;
const { extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets, extraThemeNames = [] } = keycloakify ?? {}; const { extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets, extraThemeNames = [] } = keycloakify ?? {};
@ -122,10 +58,7 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv:
const { KEYCLOAKIFY_BUNDLER } = process.env; const { KEYCLOAKIFY_BUNDLER } = process.env;
assert( assert(
typeGuard<Bundler | undefined>( typeGuard<Bundler | undefined>(KEYCLOAKIFY_BUNDLER, [undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)),
KEYCLOAKIFY_BUNDLER,
[undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)
),
`${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}` `${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}`
); );
@ -184,48 +117,22 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv:
} }
return keycloakifyBuildDirPath; return keycloakifyBuildDirPath;
})(),
"urlPathname": (() => {
const { homepage } = parsedPackageJson;
let url: URL | undefined = undefined;
if (homepage !== undefined) {
url = new URL(homepage);
}
if (url === undefined) {
return undefined;
}
const out = url.pathname.replace(/([^/])$/, "$1/");
return out === "/" ? undefined : out;
})() })()
}; };
})();
if (isExternalAssetsCliParamProvided) {
const commonExternalAssets = id<BuildOptions.ExternalAssets.CommonExternalAssets>({
...common,
"isStandalone": false
});
if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) {
return id<BuildOptions.ExternalAssets.SameDomain>({
...commonExternalAssets,
"areAppAndKeycloakServerSharingSameDomain": 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": { "areAppAndKeycloakServerSharingSameDomain": true }',
"in your package.json"
].join(" ")
);
return id<BuildOptions.ExternalAssets.DifferentDomains>({
...commonExternalAssets,
"areAppAndKeycloakServerSharingSameDomain": false,
"urlOrigin": url.origin,
"urlPathname": url.pathname
});
}
}
return id<BuildOptions.Standalone>({
...common,
"isStandalone": true,
"urlPathname": url?.pathname
});
} }

View File

@ -13,40 +13,12 @@ export const themeTypes = ["login", "account"] as const;
export type ThemeType = (typeof themeTypes)[number]; export type ThemeType = (typeof themeTypes)[number];
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets; export type BuildOptionsLike = {
export namespace BuildOptionsLike {
export type Common = {
themeName: string; themeName: string;
themeVersion: string; themeVersion: string;
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined; urlPathname: string | undefined;
}; };
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = {
isStandalone: false;
};
export type SameDomain = Common &
CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = Common &
CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
assert<BuildOptions extends BuildOptionsLike ? true : false>(); assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function generateFtlFilesCodeFactory(params: { export function generateFtlFilesCodeFactory(params: {
@ -63,22 +35,23 @@ export function generateFtlFilesCodeFactory(params: {
const $ = cheerio.load(indexHtmlCode); const $ = cheerio.load(indexHtmlCode);
fix_imports_statements: { fix_imports_statements: {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
break fix_imports_statements;
}
$("script:not([src])").each((...[, element]) => { $("script:not([src])").each((...[, element]) => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({ const jsCode = $(element).html();
"jsCode": $(element).html()!,
buildOptions assert(jsCode !== null);
});
const { fixedJsCode } = replaceImportsFromStaticInJsCode({ jsCode });
$(element).text(fixedJsCode); $(element).text(fixedJsCode);
}); });
$("style").each((...[, element]) => { $("style").each((...[, element]) => {
const cssCode = $(element).html();
assert(cssCode !== null);
const { fixedCssCode } = replaceImportsInInlineCssCode({ const { fixedCssCode } = replaceImportsInInlineCssCode({
"cssCode": $(element).html()!, cssCode,
buildOptions buildOptions
}); });
@ -100,9 +73,7 @@ export function generateFtlFilesCodeFactory(params: {
$(element).attr( $(element).attr(
attrName, attrName,
buildOptions.isStandalone href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
? href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
: href.replace(/^\//, `${buildOptions.urlOrigin}/`)
); );
}) })
); );

View File

@ -1,7 +1,6 @@
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 { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions"; import type { BuildOptions } from "./BuildOptions";
import type { ThemeType } from "./generateFtl"; import type { ThemeType } from "./generateFtl";
@ -13,11 +12,7 @@ export type BuildOptionsLike = {
themeVersion: string; themeVersion: string;
}; };
{ assert<BuildOptions extends BuildOptionsLike ? true : false>();
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function generateJavaStackFiles(params: { export function generateJavaStackFiles(params: {
keycloakThemeBuildingDirPath: string; keycloakThemeBuildingDirPath: string;

View File

@ -1,7 +1,6 @@
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 { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions"; import type { BuildOptions } from "./BuildOptions";
export type BuildOptionsLike = { export type BuildOptionsLike = {
@ -9,11 +8,7 @@ export type BuildOptionsLike = {
extraThemeNames: string[]; extraThemeNames: string[];
}; };
{ assert<BuildOptions extends BuildOptionsLike ? true : false>();
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";

View File

@ -13,13 +13,23 @@ import * as crypto from "crypto";
export async function downloadKeycloakStaticResources( export async function downloadKeycloakStaticResources(
// prettier-ignore // prettier-ignore
params: { params: {
projectDirPath: string;
themeType: ThemeType; themeType: ThemeType;
themeDirPath: string; themeDirPath: string;
isSilent: boolean;
keycloakVersion: string; keycloakVersion: string;
usedResources: {
resourcesCommonFilePaths: string[];
resourcesFilePaths: string[];
} | undefined
} }
) { ) {
const { themeType, isSilent, themeDirPath, keycloakVersion } = params; const { projectDirPath, themeType, themeDirPath, keycloakVersion, usedResources } = params;
console.log({
themeDirPath,
keycloakVersion,
usedResources
});
const tmpDirPath = pathJoin( const tmpDirPath = pathJoin(
themeDirPath, themeDirPath,
@ -28,19 +38,39 @@ export async function downloadKeycloakStaticResources(
); );
await downloadBuiltinKeycloakTheme({ await downloadBuiltinKeycloakTheme({
projectDirPath,
keycloakVersion, keycloakVersion,
"destDirPath": tmpDirPath, "destDirPath": tmpDirPath
isSilent
}); });
transformCodebase({ transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"), "srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesDirPathRelativeToPublicDir)) "destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesDirPathRelativeToPublicDir)),
"transformSourceCode":
usedResources === undefined
? undefined
: ({ fileRelativePath, sourceCode }) => {
if (!usedResources.resourcesFilePaths.includes(fileRelativePath)) {
return undefined;
}
return { "modifiedSourceCode": sourceCode };
}
}); });
transformCodebase({ transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"), "srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesCommonDirPathRelativeToPublicDir)) "destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesCommonDirPathRelativeToPublicDir)),
"transformSourceCode":
usedResources === undefined
? undefined
: ({ fileRelativePath, sourceCode }) => {
if (!usedResources.resourcesCommonFilePaths.includes(fileRelativePath)) {
return undefined;
}
return { "modifiedSourceCode": sourceCode };
}
}); });
fs.rmSync(tmpDirPath, { "recursive": true, "force": true }); fs.rmSync(tmpDirPath, { "recursive": true, "force": true });

View File

@ -12,45 +12,20 @@ import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResourc
import { readFieldNameUsage } from "./readFieldNameUsage"; import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames"; import { readExtraPagesNames } from "./readExtraPageNames";
import { generateMessageProperties } from "./generateMessageProperties"; import { generateMessageProperties } from "./generateMessageProperties";
import { readStaticResourcesUsage } from "./readStaticResourcesUsage";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets; export type BuildOptionsLike = {
export namespace BuildOptionsLike {
export type Common = {
themeName: string; themeName: string;
extraThemeProperties: string[] | undefined; extraThemeProperties: string[] | undefined;
isSilent: boolean;
themeVersion: string; themeVersion: string;
keycloakVersionDefaultAssets: string; keycloakVersionDefaultAssets: string;
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined; urlPathname: string | undefined;
}; };
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = Common & {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
assert<BuildOptions extends BuildOptionsLike ? true : false>(); assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function generateTheme(params: { export async function generateTheme(params: {
projectDirPath: string;
reactAppBuildDirPath: string; reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string; keycloakThemeBuildingDirPath: string;
themeSrcDirPath: string; themeSrcDirPath: string;
@ -58,7 +33,15 @@ export async function generateTheme(params: {
buildOptions: BuildOptionsLike; buildOptions: BuildOptionsLike;
keycloakifyVersion: string; keycloakifyVersion: string;
}): Promise<void> { }): Promise<void> {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion } = params; const {
projectDirPath,
reactAppBuildDirPath,
keycloakThemeBuildingDirPath,
themeSrcDirPath,
keycloakifySrcDirPath,
buildOptions,
keycloakifyVersion
} = params;
const getThemeDirPath = (themeType: ThemeType | "email") => const getThemeDirPath = (themeType: ThemeType | "email") =>
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType); pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
@ -77,17 +60,16 @@ export async function generateTheme(params: {
copy_app_resources_to_theme_path: { copy_app_resources_to_theme_path: {
const isFirstPass = themeType.indexOf(themeType) === 0; const isFirstPass = themeType.indexOf(themeType) === 0;
if (!isFirstPass && !buildOptions.isStandalone) { if (!isFirstPass) {
break copy_app_resources_to_theme_path; break copy_app_resources_to_theme_path;
} }
transformCodebase({ transformCodebase({
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath, "destDirPath": pathJoin(themeDirPath, "resources", "build"),
"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 (
buildOptions.isStandalone &&
isInside({ isInside({
"dirPath": pathJoin(reactAppBuildDirPath, basenameOfKeycloakDirInPublicDir), "dirPath": pathJoin(reactAppBuildDirPath, basenameOfKeycloakDirInPublicDir),
filePath filePath
@ -97,10 +79,6 @@ export async function generateTheme(params: {
} }
if (/\.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")
}); });
@ -120,19 +98,14 @@ export async function generateTheme(params: {
} }
if (/\.js?$/i.test(filePath)) { if (/\.js?$/i.test(filePath)) {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined;
}
const { fixedJsCode } = replaceImportsFromStaticInJsCode({ const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"), "jsCode": sourceCode.toString("utf8")
buildOptions
}); });
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") }; return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
} }
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined; return { "modifiedSourceCode": sourceCode };
} }
}); });
} }
@ -197,10 +170,11 @@ export async function generateTheme(params: {
} }
await downloadKeycloakStaticResources({ await downloadKeycloakStaticResources({
"isSilent": buildOptions.isSilent, projectDirPath,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets, "keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
"themeDirPath": keycloakDirInPublicDir, "themeDirPath": keycloakDirInPublicDir,
themeType themeType,
"usedResources": undefined
}); });
if (themeType !== themeTypes[0]) { if (themeType !== themeTypes[0]) {
@ -222,10 +196,15 @@ export async function generateTheme(params: {
} }
await downloadKeycloakStaticResources({ await downloadKeycloakStaticResources({
"isSilent": buildOptions.isSilent, projectDirPath,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets, "keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
themeDirPath, themeDirPath,
themeType,
"usedResources": readStaticResourcesUsage({
keycloakifySrcDirPath,
themeSrcDirPath,
themeType themeType
})
}); });
fs.writeFileSync( fs.writeFileSync(

View File

@ -3,7 +3,6 @@ import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import * as fs from "fs"; import * as fs from "fs";
import type { ThemeType } from "../generateFtl"; import type { ThemeType } from "../generateFtl";
import { exclude } from "tsafe/exclude";
/** Assumes the theme type exists */ /** Assumes the theme type exists */
export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): string[] { export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): string[] {
@ -11,9 +10,7 @@ export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; them
const fieldNames: string[] = []; const fieldNames: string[] = [];
for (const srcDirPath of ([pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)] as const).filter( for (const srcDirPath of [pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)]) {
exclude(undefined)
)) {
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath)); const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
for (const filePath of filePaths) { for (const filePath of filePaths) {

View File

@ -0,0 +1,85 @@
import { crawl } from "../../tools/crawl";
import { join as pathJoin } from "path";
import * as fs from "fs";
import type { ThemeType } from "../generateFtl";
/** Assumes the theme type exists */
export function readStaticResourcesUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): {
resourcesCommonFilePaths: string[];
resourcesFilePaths: string[];
} {
const { keycloakifySrcDirPath, themeSrcDirPath, themeType } = params;
const resourcesCommonFilePaths = new Set<string>();
const resourcesFilePaths = new Set<string>();
for (const srcDirPath of [pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)]) {
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
for (const filePath of filePaths) {
const rawSourceFile = fs.readFileSync(filePath).toString("utf8");
if (!rawSourceFile.includes("resourcesCommonPath") && !rawSourceFile.includes("resourcesPath")) {
continue;
}
console.log("=========>", filePath);
const wrap = readPaths({ rawSourceFile });
wrap.resourcesCommonFilePaths.forEach(filePath => resourcesCommonFilePaths.add(filePath));
wrap.resourcesFilePaths.forEach(filePath => resourcesFilePaths.add(filePath));
}
}
return {
"resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths),
"resourcesFilePaths": Array.from(resourcesFilePaths)
};
}
/** Exported for testing purpose */
export function readPaths(params: { rawSourceFile: string }): {
resourcesCommonFilePaths: string[];
resourcesFilePaths: string[];
} {
const { rawSourceFile } = params;
const resourcesCommonFilePaths = new Set<string>();
const resourcesFilePaths = new Set<string>();
for (const isCommon of [true, false]) {
const set = isCommon ? resourcesCommonFilePaths : resourcesFilePaths;
{
const regexp = new RegExp(`resources${isCommon ? "Common" : ""}Path\\s*}([^\`]+)\``, "g");
const matches = [...rawSourceFile.matchAll(regexp)];
for (const match of matches) {
const filePath = match[1];
set.add(filePath);
}
}
{
const regexp = new RegExp(`resources${isCommon ? "Common" : ""}Path\\s*[+,]\\s*["']([^"'\`]+)["'\`]`, "g");
const matches = [...rawSourceFile.matchAll(regexp)];
for (const match of matches) {
const filePath = match[1];
set.add(filePath);
}
}
}
const removePrefixSlash = (filePath: string) => (filePath.startsWith("/") ? filePath.slice(1) : filePath);
return {
"resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths).map(removePrefixSlash),
"resourcesFilePaths": Array.from(resourcesFilePaths).map(removePrefixSlash)
};
}

View File

@ -30,6 +30,7 @@ export async function main() {
for (const themeName of [buildOptions.themeName, ...buildOptions.extraThemeNames]) { for (const themeName of [buildOptions.themeName, ...buildOptions.extraThemeNames]) {
await generateTheme({ await generateTheme({
projectDirPath,
"keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath, "keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath,
themeSrcDirPath, themeSrcDirPath,
"keycloakifySrcDirPath": pathJoin(keycloakifyDirPath, "src"), "keycloakifySrcDirPath": pathJoin(keycloakifyDirPath, "src"),

View File

@ -1,31 +1,6 @@
import { ftlValuesGlobalName } from "../ftlValuesGlobalName"; 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 function replaceImportsFromStaticInJsCode(params: { jsCode: string }): { fixedJsCode: string } {
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: NOTE:
@ -38,7 +13,7 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string; build
will always run in keycloak context. will always run in keycloak context.
*/ */
const { jsCode, buildOptions } = params; const { jsCode } = params;
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [ 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"), new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
@ -46,40 +21,23 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string; build
${n}[(function(){ ${n}[(function(){
var pd= Object.getOwnPropertyDescriptor(${n}, "p"); var pd= Object.getOwnPropertyDescriptor(${n}, "p");
if( pd === undefined || pd.configurable ){ if( pd === undefined || pd.configurable ){
${
buildOptions.isStandalone
? `
Object.defineProperty(${n}, "p", { Object.defineProperty(${n}, "p", {
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; }, get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
set: function (){} set: function (){}
}); });
`
: `
var p= "";
Object.defineProperty(${n}, "p", {
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
set: function (value){ p = value;}
});
`
}
} }
return "${u}"; return "${u}";
})()] = function(${e}) { return "${buildOptions.isStandalone ? "/build/" : ""}static/${language}/"` })()] = function(${e}) { return "${true ? "/build/" : ""}static/${language}/"`
]; ];
const fixedJsCode = jsCode const fixedJsCode = jsCode
.replace(...getReplaceArgs("js")) .replace(...getReplaceArgs("js"))
.replace(...getReplaceArgs("css")) .replace(...getReplaceArgs("css"))
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) => .replace(/[a-zA-Z]+\.[a-zA-Z]+\+"static\//g, `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`)
buildOptions.isStandalone
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group}) + "static/`
)
//TODO: Write a test case for this //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]) => .replace(
buildOptions.isStandalone /".chunk.css",([a-zA-Z])+=[a-zA-Z]+\.[a-zA-Z]+\+([a-zA-Z]+),/,
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},` (...[, group1, group2]) => `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group2},`
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group2}) + ${group3},`
); );
return { fixedJsCode }; return { fixedJsCode };

View File

@ -1,20 +1,12 @@
import * as crypto from "crypto"; import * as crypto from "crypto";
import type { BuildOptions } from "../BuildOptions"; import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = { export type BuildOptionsLike = {
urlPathname: string | undefined; urlPathname: string | undefined;
}; };
{ assert<BuildOptions extends BuildOptionsLike ? true : false>();
const buildOptions = Reflect<BuildOptions>();
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function replaceImportsInCssCode(params: { cssCode: string }): { export function replaceImportsInCssCode(params: { cssCode: string }): {
fixedCssCode: string; fixedCssCode: string;

View File

@ -1,32 +1,11 @@
import type { BuildOptions } from "../BuildOptions"; import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets; export type BuildOptionsLike = {
export namespace BuildOptionsLike {
export type Common = {
urlPathname: string | undefined; urlPathname: string | undefined;
}; };
export type Standalone = Common & { assert<BuildOptions extends BuildOptionsLike ? true : false>();
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 }): { export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
fixedCssCode: string; fixedCssCode: string;
@ -37,10 +16,7 @@ export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOp
buildOptions.urlPathname === undefined buildOptions.urlPathname === undefined
? /url\(["']?\/([^/][^)"']+)["']?\)/g ? /url\(["']?\/([^/][^)"']+)["']?\)/g
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"), : new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
(...[, group]) => (...[, group]) => `url(\${url.resourcesPath}/build/${group})`
`url(${
buildOptions.isStandalone ? "${url.resourcesPath}/build/" + group : buildOptions.urlOrigin + (buildOptions.urlPathname ?? "/") + group
})`
); );
return { fixedCssCode }; return { fixedCssCode };

View File

@ -1,18 +1,55 @@
import { exec as execCallback } from "child_process"; import { exec as execCallback } from "child_process";
import { createHash } from "crypto"; import { createHash } from "crypto";
import { mkdir, readFile, stat, writeFile } from "fs/promises"; import { mkdir, readFile, stat, writeFile, unlink, rm } from "fs/promises";
import fetch, { type FetchOptions } from "make-fetch-happen"; import fetch, { type FetchOptions } from "make-fetch-happen";
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep } from "path"; import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { promisify } from "util"; import { promisify } from "util";
import { getProjectRoot } from "./getProjectRoot";
import { transformCodebase } from "./transformCodebase"; import { transformCodebase } from "./transformCodebase";
import { unzip } from "./unzip"; import { unzip, zip } from "./unzip";
const exec = promisify(execCallback); const exec = promisify(execCallback);
function hash(s: string) { function generateFileNameFromURL(params: {
return createHash("sha256").update(s).digest("hex"); url: string;
preCacheTransform:
| {
actionCacheId: string;
actionFootprint: string;
}
| undefined;
}): string {
const { preCacheTransform } = params;
// Parse the URL
const url = new URL(params.url);
// Extract pathname and remove leading slashes
let fileName = url.pathname.replace(/^\//, "").replace(/\//g, "_");
// Optionally, add query parameters replacing special characters
if (url.search) {
fileName += url.search.replace(/[&=?]/g, "-");
}
// Replace any characters that are not valid in filenames
fileName = fileName.replace(/[^a-zA-Z0-9-_]/g, "");
// Trim or pad the fileName to a specific length
fileName = fileName.substring(0, 50);
add_pre_cache_transform: {
if (preCacheTransform === undefined) {
break add_pre_cache_transform;
}
// Sanitize actionCacheId the same way as other components
const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
}
return fileName;
} }
async function exists(path: string) { async function exists(path: string) {
@ -113,14 +150,43 @@ async function getFetchOptions(): Promise<Pick<FetchOptions, "proxy" | "noProxy"
return { proxy, noProxy, strictSSL, cert, ca: ca.length === 0 ? undefined : ca }; return { proxy, noProxy, strictSSL, cert, ca: ca.length === 0 ? undefined : ca };
} }
export async function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string }) { export async function downloadAndUnzip(
const { url, destDirPath, pathOfDirToExtractInArchive } = params; params: {
url: string;
destDirPath: string;
specificDirsToExtract?: string[];
preCacheTransform?: {
actionCacheId: string;
action: (params: { destDirPath: string }) => Promise<void>;
};
} & (
| {
doUseCache: true;
projectDirPath: string;
}
| {
doUseCache: false;
}
)
) {
const { url, destDirPath, specificDirsToExtract, preCacheTransform, ...rest } = params;
const downloadHash = hash(JSON.stringify({ url })).substring(0, 15); const zipFileBasename = generateFileNameFromURL({
const projectRoot = getProjectRoot(); url,
const cacheRoot = process.env.XDG_CACHE_HOME ?? pathJoin(projectRoot, "node_modules", ".cache"); "preCacheTransform":
const zipFilePath = pathJoin(cacheRoot, "keycloakify", "zip", `_${downloadHash}.zip`); preCacheTransform === undefined
const extractDirPath = pathJoin(cacheRoot, "keycloakify", "unzip", `_${downloadHash}`); ? undefined
: {
"actionCacheId": preCacheTransform.actionCacheId,
"actionFootprint": preCacheTransform.action.toString()
}
});
const cacheRoot = !rest.doUseCache
? `tmp_${Math.random().toString().slice(2, 12)}`
: pathJoin(process.env.XDG_CACHE_HOME ?? pathJoin(rest.projectDirPath, "node_modules", ".cache"), "keycloakify");
const zipFilePath = pathJoin(cacheRoot, `${zipFileBasename}.zip`);
const extractDirPath = pathJoin(cacheRoot, `tmp_unzip_${zipFileBasename}`);
if (!(await exists(zipFilePath))) { if (!(await exists(zipFilePath))) {
const opts = await getFetchOptions(); const opts = await getFetchOptions();
@ -136,12 +202,32 @@ export async function downloadAndUnzip(params: { url: string; destDirPath: strin
response.body?.setMaxListeners(Number.MAX_VALUE); response.body?.setMaxListeners(Number.MAX_VALUE);
assert(typeof response.body !== "undefined" && response.body != null); assert(typeof response.body !== "undefined" && response.body != null);
await writeFile(zipFilePath, response.body); await writeFile(zipFilePath, response.body);
if (specificDirsToExtract !== undefined || preCacheTransform !== undefined) {
await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
await preCacheTransform?.action({
"destDirPath": extractDirPath
});
await unlink(zipFilePath);
await zip(extractDirPath, zipFilePath);
await rm(extractDirPath, { "recursive": true });
}
} }
await unzip(zipFilePath, extractDirPath, pathOfDirToExtractInArchive); await unzip(zipFilePath, extractDirPath);
transformCodebase({ transformCodebase({
"srcDirPath": extractDirPath, "srcDirPath": extractDirPath,
"destDirPath": destDirPath "destDirPath": destDirPath
}); });
if (!rest.doUseCache) {
await rm(cacheRoot, { "recursive": true });
} else {
await rm(extractDirPath, { "recursive": true });
}
} }

View File

@ -3,7 +3,7 @@ import * as path from "path";
import { crawl } from "./crawl"; import { crawl } from "./crawl";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string }) => type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; fileRelativePath: string }) =>
| { | {
modifiedSourceCode: Buffer; modifiedSourceCode: Buffer;
newFileName?: string; newFileName?: string;
@ -20,26 +20,27 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
})) }))
} = params; } = params;
for (const file_relative_path of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) { for (const fileRelativePath of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) {
const filePath = path.join(srcDirPath, file_relative_path); const filePath = path.join(srcDirPath, fileRelativePath);
const transformSourceCodeResult = transformSourceCode({ const transformSourceCodeResult = transformSourceCode({
"sourceCode": fs.readFileSync(filePath), "sourceCode": fs.readFileSync(filePath),
filePath filePath,
fileRelativePath
}); });
if (transformSourceCodeResult === undefined) { if (transformSourceCodeResult === undefined) {
continue; continue;
} }
fs.mkdirSync(path.dirname(path.join(destDirPath, file_relative_path)), { fs.mkdirSync(path.dirname(path.join(destDirPath, fileRelativePath)), {
"recursive": true "recursive": true
}); });
const { newFileName, modifiedSourceCode } = transformSourceCodeResult; const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
fs.writeFileSync( fs.writeFileSync(
path.join(path.dirname(path.join(destDirPath, file_relative_path)), newFileName ?? path.basename(file_relative_path)), path.join(path.dirname(path.join(destDirPath, fileRelativePath)), newFileName ?? path.basename(fileRelativePath)),
modifiedSourceCode modifiedSourceCode
); );
} }

View File

@ -2,6 +2,7 @@ import fsp from "node:fs/promises";
import fs from "fs"; import fs from "fs";
import path from "node:path"; import path from "node:path";
import yauzl from "yauzl"; import yauzl from "yauzl";
import yazl from "yazl";
import stream from "node:stream"; import stream from "node:stream";
import { promisify } from "node:util"; import { promisify } from "node:util";
@ -19,12 +20,17 @@ async function pathExists(path: string) {
} }
} }
export async function unzip(file: string, targetFolder: string, unzipSubPath?: string) { // Handlings of non posix path is not implemented correctly
// add trailing slash to unzipSubPath and targetFolder // it work by coincidence. Don't have the time to fix but it should be fixed.
if (unzipSubPath && (!unzipSubPath.endsWith("/") || !unzipSubPath.endsWith("\\"))) { export async function unzip(file: string, targetFolder: string, specificDirsToExtract?: string[]) {
unzipSubPath += "/"; specificDirsToExtract = specificDirsToExtract?.map(dirPath => {
if (!dirPath.endsWith("/") || !dirPath.endsWith("\\")) {
dirPath += "/";
} }
return dirPath;
});
if (!targetFolder.endsWith("/") || !targetFolder.endsWith("\\")) { if (!targetFolder.endsWith("/") || !targetFolder.endsWith("\\")) {
targetFolder += "/"; targetFolder += "/";
} }
@ -42,15 +48,17 @@ export async function unzip(file: string, targetFolder: string, unzipSubPath?: s
zipfile.readEntry(); zipfile.readEntry();
zipfile.on("entry", async entry => { zipfile.on("entry", async entry => {
if (unzipSubPath) { if (specificDirsToExtract !== undefined) {
const dirPath = specificDirsToExtract.find(dirPath => entry.fileName.startsWith(dirPath));
// Skip files outside of the unzipSubPath // Skip files outside of the unzipSubPath
if (!entry.fileName.startsWith(unzipSubPath)) { if (dirPath === undefined) {
zipfile.readEntry(); zipfile.readEntry();
return; return;
} }
// Remove the unzipSubPath from the file name // Remove the unzipSubPath from the file name
entry.fileName = entry.fileName.substring(unzipSubPath.length); entry.fileName = entry.fileName.substring(dirPath.length);
} }
const target = path.join(targetFolder, entry.fileName); const target = path.join(targetFolder, entry.fileName);
@ -77,6 +85,8 @@ export async function unzip(file: string, targetFolder: string, unzipSubPath?: s
return; return;
} }
await fsp.mkdir(path.dirname(target), { "recursive": true });
await pipeline(readStream, fs.createWriteStream(target)); await pipeline(readStream, fs.createWriteStream(target));
zipfile.readEntry(); zipfile.readEntry();
@ -90,3 +100,42 @@ export async function unzip(file: string, targetFolder: string, unzipSubPath?: s
}); });
}); });
} }
// NOTE: This code was directly copied from ChatGPT and appears to function as expected.
// However, confidence in its complete accuracy and robustness is limited.
export async function zip(sourceFolder: string, targetZip: string) {
return new Promise<void>(async (resolve, reject) => {
const zipfile = new yazl.ZipFile();
const files: string[] = [];
// Recursive function to explore directories and their subdirectories
async function exploreDir(dir: string) {
const dirContent = await fsp.readdir(dir);
for (const file of dirContent) {
const filePath = path.join(dir, file);
const stat = await fsp.stat(filePath);
if (stat.isDirectory()) {
await exploreDir(filePath);
} else if (stat.isFile()) {
files.push(filePath);
}
}
}
// Collecting all files to be zipped
await exploreDir(sourceFolder);
// Adding files to zip
for (const file of files) {
const relativePath = path.relative(sourceFolder, file);
zipfile.addFile(file, relativePath);
}
zipfile.outputStream
.pipe(fs.createWriteStream(targetZip))
.on("close", () => resolve())
.on("error", err => reject(err)); // Listen to error events
zipfile.end();
});
}

View File

@ -1,21 +1,15 @@
import { useReducer, useEffect } from "react"; import { useReducer, useEffect } from "react";
import { headInsert } from "keycloakify/tools/headInsert"; import { headInsert } from "keycloakify/tools/headInsert";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { clsx } from "keycloakify/tools/clsx"; import { clsx } from "keycloakify/tools/clsx";
export function usePrepareTemplate(params: { export function usePrepareTemplate(params: {
doFetchDefaultThemeResources: boolean; doFetchDefaultThemeResources: boolean;
stylesCommon?: string[];
styles?: string[]; styles?: string[];
scripts?: string[]; scripts?: string[];
url: {
resourcesCommonPath: string;
resourcesPath: string;
};
htmlClassName: string | undefined; htmlClassName: string | undefined;
bodyClassName: string | undefined; bodyClassName: string | undefined;
}) { }) {
const { doFetchDefaultThemeResources, stylesCommon = [], styles = [], url, scripts = [], htmlClassName, bodyClassName } = params; const { doFetchDefaultThemeResources, styles = [], scripts = [], htmlClassName, bodyClassName } = params;
const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources); const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources);
@ -31,12 +25,7 @@ export function usePrepareTemplate(params: {
(async () => { (async () => {
const prLoadedArray: Promise<void>[] = []; const prLoadedArray: Promise<void>[] = [];
[ styles.reverse().forEach(href => {
...stylesCommon.map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
...styles.map(relativePath => pathJoin(url.resourcesPath, relativePath))
]
.reverse()
.forEach(href => {
const { prLoaded, remove } = headInsert({ const { prLoaded, remove } = headInsert({
"type": "css", "type": "css",
"position": "prepend", "position": "prepend",
@ -57,10 +46,10 @@ export function usePrepareTemplate(params: {
setReady(); setReady();
})(); })();
scripts.forEach(relativePath => { scripts.forEach(src => {
const { remove } = headInsert({ const { remove } = headInsert({
"type": "javascript", "type": "javascript",
"src": pathJoin(url.resourcesPath, relativePath) src
}); });
removeArray.push(remove); removeArray.push(remove);

View File

@ -31,13 +31,12 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
const { isReady } = usePrepareTemplate({ const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss, "doFetchDefaultThemeResources": doUseDefaultCss,
url, "styles": [
"stylesCommon": [ `${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
"node_modules/patternfly/dist/css/patternfly.min.css", `${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
"node_modules/patternfly/dist/css/patternfly-additions.min.css", `${url.resourcesCommonPath}/lib/zocial/zocial.css`,
"lib/zocial/zocial.css" `${url.resourcesPath}/css/login.css`
], ],
"styles": ["css/login.css"],
"htmlClassName": getClassName("kcHtmlClass"), "htmlClassName": getClassName("kcHtmlClass"),
"bodyClassName": undefined "bodyClassName": undefined
}); });

View File

@ -1,6 +1,5 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { headInsert } from "keycloakify/tools/headInsert"; import { headInsert } from "keycloakify/tools/headInsert";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { clsx } from "keycloakify/tools/clsx"; import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName"; import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
@ -24,7 +23,7 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
const { prLoaded, remove } = headInsert({ const { prLoaded, remove } = headInsert({
"type": "javascript", "type": "javascript",
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js") "src": `${kcContext.url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js`
}); });
(async () => { (async () => {

View File

@ -0,0 +1,108 @@
import { readPaths } from "keycloakify/bin/keycloakify/generateTheme/readStaticResourcesUsage";
import { same } from "evt/tools/inDepth/same";
import { expect, it, describe } from "vitest";
describe("Ensure it's able to extract used Keycloak resources", () => {
const expectedPaths = {
"resourcesCommonFilePaths": [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
"lib/zocial/zocial.css",
"node_modules/jquery/dist/jquery.min.js"
],
"resourcesFilePaths": ["css/login.css"]
};
it("works with coding style n°1", () => {
const paths = readPaths({
"rawSourceFile": `
const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss,
"styles": [
\`\${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css\`,
\`\${
url.resourcesCommonPath
}/node_modules/patternfly/dist/css/patternfly-additions.min.css\`,
\`\${resourcesCommonPath }/lib/zocial/zocial.css\`,
\`\${url.resourcesPath}/css/login.css\`
],
"htmlClassName": getClassName("kcHtmlClass"),
"bodyClassName": undefined
});
const { prLoaded, remove } = headInsert({
"type": "javascript",
"src": \`\${kcContext.url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js\`
});
`
});
expect(same(paths, expectedPaths)).toBe(true);
});
it("works with coding style n°2", () => {
const paths = readPaths({
"rawSourceFile": `
const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss,
"styles": [
url.resourcesCommonPath + "/node_modules/patternfly/dist/css/patternfly.min.css",
url.resourcesCommonPath + '/node_modules/patternfly/dist/css/patternfly-additions.min.css',
url.resourcesCommonPath
+ "/lib/zocial/zocial.css",
url.resourcesPath +
'/css/login.css'
],
"htmlClassName": getClassName("kcHtmlClass"),
"bodyClassName": undefined
});
const { prLoaded, remove } = headInsert({
"type": "javascript",
"src": kcContext.url.resourcesCommonPath + "/node_modules/jquery/dist/jquery.min.js\"
});
`
});
console.log(paths);
console.log(expectedPaths);
expect(same(paths, expectedPaths)).toBe(true);
});
it("works with coding style n°3", () => {
const paths = readPaths({
"rawSourceFile": `
const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss,
"styles": [
path.join(resourcesCommonPath,"/node_modules/patternfly/dist/css/patternfly.min.css"),
path.join(url.resourcesCommonPath, '/node_modules/patternfly/dist/css/patternfly-additions.min.css'),
path.join(url.resourcesCommonPath,
"/lib/zocial/zocial.css"),
pathJoin(
url.resourcesPath,
'css/login.css'
)
],
"htmlClassName": getClassName("kcHtmlClass"),
"bodyClassName": undefined
});
const { prLoaded, remove } = headInsert({
"type": "javascript",
"src": path.join(kcContext.url.resourcesCommonPath, "/node_modules/jquery/dist/jquery.min.js")
});
`
});
expect(same(paths, expectedPaths)).toBe(true);
});
});

View File

@ -35,10 +35,7 @@ describe("bin/js-transforms", () => {
`; `;
it("transforms standalone code properly", () => { it("transforms standalone code properly", () => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({ const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": jsCodeUntransformed, "jsCode": jsCodeUntransformed
"buildOptions": {
"isStandalone": true
}
}); });
const fixedJsCodeExpected = ` const fixedJsCodeExpected = `
@ -89,66 +86,6 @@ describe("bin/js-transforms", () => {
`; `;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
it("transforms external app code properly", () => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": jsCodeUntransformed,
"buildOptions": {
"isStandalone": false,
"urlOrigin": "https://demo-app.keycloakify.dev"
}
});
const fixedJsCodeExpected = `
function f() {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev/" : a.p) + "static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
function sameAsF() {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev/" : a.p) + "static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
__webpack_require__[(function (){
var pd= Object.getOwnPropertyDescriptor(__webpack_require__, "p");
if( pd === undefined || pd.configurable ){
var p= "";
Object.defineProperty(__webpack_require__, "p", {
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev/" : p; },
set: function (value){ p = value; }
});
}
return "u";
})()] = function(e) {
return "static/js/" + e + "." + {
147: "6c5cee76",
787: "8da10fcf",
922: "be170a73"
} [e] + ".chunk.js"
}
t[(function (){
var pd= Object.getOwnPropertyDescriptor(t, "p");
if( pd === undefined || pd.configurable ){
var p= "";
Object.defineProperty(t, "p", {
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev/" : p; },
set: function (value){ p = value; }
});
}
return "miniCssF";
})()] = function(e) {
return "static/css/" + e + "." + {
164:"dcfd7749",
908:"67c9ed2c"
} [e] + ".chunk.css"
}
`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true); expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
}); });
}); });
@ -304,7 +241,6 @@ describe("bin/css-inline-transforms", () => {
const { fixedCssCode } = replaceImportsInInlineCssCode({ const { fixedCssCode } = replaceImportsInInlineCssCode({
cssCode, cssCode,
"buildOptions": { "buildOptions": {
"isStandalone": true,
"urlPathname": undefined "urlPathname": undefined
} }
}); });
@ -344,53 +280,6 @@ describe("bin/css-inline-transforms", () => {
} }
`; `;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
});
it("transforms css for external app properly", () => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
cssCode,
"buildOptions": {
"isStandalone": false,
"urlOrigin": "https://demo-app.keycloakify.dev",
"urlPathname": undefined
}
});
const fixedCssCodeExpected = `
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://demo-app.keycloakify.dev/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(https://demo-app.keycloakify.dev/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(https://demo-app.keycloakify.dev/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(https://demo-app.keycloakify.dev/fonts/WorkSans/worksans-bold-webfont.woff2)
format("woff2");
}
`;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true); expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
}); });
}); });
@ -430,7 +319,6 @@ describe("bin/css-inline-transforms", () => {
const { fixedCssCode } = replaceImportsInInlineCssCode({ const { fixedCssCode } = replaceImportsInInlineCssCode({
cssCode, cssCode,
"buildOptions": { "buildOptions": {
"isStandalone": true,
"urlPathname": "/x/y/z/" "urlPathname": "/x/y/z/"
} }
}); });
@ -470,53 +358,6 @@ describe("bin/css-inline-transforms", () => {
} }
`; `;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
});
it("transforms css for external app properly", () => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
cssCode,
"buildOptions": {
"isStandalone": false,
"urlOrigin": "https://demo-app.keycloakify.dev",
"urlPathname": "/x/y/z/"
}
});
const fixedCssCodeExpected = `
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://demo-app.keycloakify.dev/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(https://demo-app.keycloakify.dev/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(https://demo-app.keycloakify.dev/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(https://demo-app.keycloakify.dev/x/y/z/fonts/WorkSans/worksans-bold-webfont.woff2)
format("woff2");
}
`;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true); expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
}); });
}); });

View File

@ -12,7 +12,8 @@ export const sampleReactProjectDirPath = pathJoin(getProjectRoot(), "sample_reac
async function setupSampleReactProject(destDir: string) { async function setupSampleReactProject(destDir: string) {
await downloadAndUnzip({ await downloadAndUnzip({
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip", "url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
"destDirPath": destDir "destDirPath": destDir,
"doUseCache": false
}); });
} }
let parsedPackageJson: Record<string, unknown> = {}; let parsedPackageJson: Record<string, unknown> = {};
@ -51,17 +52,19 @@ describe("Sample Project", () => {
await setupSampleReactProject(sampleReactProjectDirPath); await setupSampleReactProject(sampleReactProjectDirPath);
await initializeEmailTheme(); await initializeEmailTheme();
const projectDirPath = process.cwd();
const destDirPath = pathJoin( const destDirPath = pathJoin(
readBuildOptions({ readBuildOptions({
"processArgv": ["--silent"], "processArgv": ["--silent"],
"projectDirPath": process.cwd() projectDirPath
}).keycloakifyBuildDirPath, }).keycloakifyBuildDirPath,
"src", "src",
"main", "main",
"resources", "resources",
"theme" "theme"
); );
await downloadBuiltinKeycloakTheme({ destDirPath, keycloakVersion: "11.0.3", "isSilent": false }); await downloadBuiltinKeycloakTheme({ destDirPath, "keycloakVersion": "11.0.3", projectDirPath });
}, },
{ timeout: 90000 } { timeout: 90000 }
); );
@ -77,17 +80,19 @@ describe("Sample Project", () => {
await setupSampleReactProject(pathJoin(sampleReactProjectDirPath, "custom_input")); await setupSampleReactProject(pathJoin(sampleReactProjectDirPath, "custom_input"));
await initializeEmailTheme(); await initializeEmailTheme();
const projectDirPath = process.cwd();
const destDirPath = pathJoin( const destDirPath = pathJoin(
readBuildOptions({ readBuildOptions({
"processArgv": ["--silent"], "processArgv": ["--silent"],
"projectDirPath": process.cwd() projectDirPath
}).keycloakifyBuildDirPath, }).keycloakifyBuildDirPath,
"src", "src",
"main", "main",
"resources", "resources",
"theme" "theme"
); );
await downloadBuiltinKeycloakTheme({ destDirPath, "keycloakVersion": "11.0.3", "isSilent": false }); await downloadBuiltinKeycloakTheme({ destDirPath, "keycloakVersion": "11.0.3", projectDirPath });
}, },
{ timeout: 90000 } { timeout: 90000 }
); );

View File

@ -3,6 +3,7 @@ import { downloadAndUnzip } from "keycloakify/bin/tools/downloadAndUnzip";
export async function setupSampleReactProject(destDirPath: string) { export async function setupSampleReactProject(destDirPath: string) {
await downloadAndUnzip({ await downloadAndUnzip({
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip", "url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
"destDirPath": destDirPath "destDirPath": destDirPath,
"doUseCache": false
}); });
} }