Compare commits

...

9 Commits

Author SHA1 Message Date
f6dfcfbae9 Fix scripts build 2023-06-21 04:01:11 +02:00
69e9595db9 Bump version 2023-06-21 03:56:30 +02:00
de390678fd Deprecate the extraPages options, analyze the code to detect extra pages 2023-06-21 03:54:43 +02:00
cf9a7b8c60 Update json-schema 2023-06-21 02:56:55 +02:00
73e9c16a8d Fix some types approximations 2023-06-21 02:55:44 +02:00
9775623981 Bump version 2023-06-19 03:17:26 +02:00
20b7bb3c99 Fix error with inital select state 2023-06-19 03:17:12 +02:00
3defc16658 Bump version 2023-06-19 02:00:13 +02:00
0dbe592182 Match even if there's a cariage return after pritIfExists... 2023-06-19 02:00:02 +02:00
15 changed files with 100 additions and 87 deletions

View File

@ -117,6 +117,10 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
# Changelog highlights # Changelog highlights
## 7.14
- Deprecate the `extraPages` build option. Keycloakify is now able to analyze your code to detect extra pages.
## 7.13 ## 7.13
- Deprecate `customUserAttribute`, Keycloakify now analyze your code to predict field name usage. [See doc](https://docs.keycloakify.dev/build-options#customuserattributes). - Deprecate `customUserAttribute`, Keycloakify now analyze your code to predict field name usage. [See doc](https://docs.keycloakify.dev/build-options#customuserattributes).

View File

@ -30,18 +30,6 @@
"type": "string" "type": "string"
} }
}, },
"extraLoginPages": {
"type": "array",
"items": {
"type": "string"
}
},
"extraAccountPages": {
"type": "array",
"items": {
"type": "string"
}
},
"extraThemeProperties": { "extraThemeProperties": {
"type": "array", "type": "array",
"items": { "items": {
@ -70,12 +58,6 @@
"keycloakifyBuildDirPath": { "keycloakifyBuildDirPath": {
"type": "string" "type": "string"
}, },
"customUserAttributes": {
"type": "array",
"items": {
"type": "string"
}
},
"themeName": { "themeName": {
"type": "string" "type": "string"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "7.13.0", "version": "7.14.0",
"description": "Create Keycloak themes using React", "description": "Create Keycloak themes using React",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -37,7 +37,10 @@ async function main() {
const baseThemeDirPath = pathJoin(tmpDirPath, "base"); const baseThemeDirPath = pathJoin(tmpDirPath, "base");
const re = new RegExp(`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`); const re = new RegExp(`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`);
crawl(baseThemeDirPath).forEach(filePath => { crawl({
"dirPath": baseThemeDirPath,
"returnedPathsType": "relative to dirPath"
}).forEach(filePath => {
const match = filePath.match(re); const match = filePath.match(re);
if (match === null) { if (match === null) {

View File

@ -10,7 +10,7 @@ export function getThemeSrcDirPath(params: { projectDirPath: string }) {
const srcDirPath = pathJoin(projectDirPath, "src"); const srcDirPath = pathJoin(projectDirPath, "src");
const themeSrcDirPath: string | undefined = crawl(srcDirPath) const themeSrcDirPath: string | undefined = crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })
.map(fileRelativePath => { .map(fileRelativePath => {
const split = fileRelativePath.split(themeSrcDirBasename); const split = fileRelativePath.split(themeSrcDirBasename);

View File

@ -17,9 +17,7 @@ export namespace BuildOptions {
themeVersion: string; themeVersion: string;
themeName: string; themeName: string;
extraThemeNames: string[]; extraThemeNames: string[];
extraLoginPages: string[] | undefined; extraThemeProperties: string[] | undefined;
extraAccountPages: string[] | undefined;
extraThemeProperties?: string[];
groupId: string; groupId: string;
artifactId: string; artifactId: string;
bundler: Bundler; bundler: Bundler;
@ -108,17 +106,7 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv:
const common: BuildOptions.Common = (() => { const common: BuildOptions.Common = (() => {
const { name, keycloakify = {}, version, homepage } = parsedPackageJson; const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
const { const { extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets, extraThemeNames = [] } = keycloakify ?? {};
extraPages,
extraLoginPages,
extraAccountPages,
extraThemeProperties,
groupId,
artifactId,
bundler,
keycloakVersionDefaultAssets,
extraThemeNames = []
} = keycloakify ?? {};
const themeName = const themeName =
keycloakify.themeName ?? keycloakify.themeName ??
@ -160,8 +148,6 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv:
); );
})(), })(),
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0", "themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
"extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])],
extraAccountPages,
extraThemeProperties, extraThemeProperties,
"isSilent": isSilentCliParamProvided, "isSilent": isSilentCliParamProvided,
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3", "keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",

View File

@ -9,7 +9,7 @@ export type BuildOptionsLike = {
themeName: string; themeName: string;
extraThemeNames: string[]; extraThemeNames: string[];
groupId: string; groupId: string;
artifactId?: string; artifactId: string;
themeVersion: string; themeVersion: string;
}; };

View File

@ -10,15 +10,14 @@ import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources"; import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources";
import { readFieldNameUsage } from "./readFieldNameUsage"; import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets; export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike { export namespace BuildOptionsLike {
export type Common = { export type Common = {
themeName: string; themeName: string;
extraLoginPages?: string[]; extraThemeProperties: string[] | undefined;
extraAccountPages?: string[];
extraThemeProperties?: string[];
isSilent: boolean; isSilent: boolean;
themeVersion: string; themeVersion: string;
keycloakVersionDefaultAssets: string; keycloakVersionDefaultAssets: string;
@ -163,14 +162,12 @@ export async function generateTheme(params: {
return accountThemePageIds; return accountThemePageIds;
} }
})(), })(),
...((() => { ...(themeSrcDirPath === undefined
switch (themeType) { ? []
case "login": : readExtraPagesNames({
return buildOptions.extraLoginPages; themeType,
case "account": themeSrcDirPath
return buildOptions.extraAccountPages; }))
}
})() ?? [])
].forEach(pageId => { ].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId }); const { ftlCode } = generateFtlFilesCode({ pageId });

View File

@ -0,0 +1,38 @@
import { crawl } from "../../tools/crawl";
import { type ThemeType, accountThemePageIds, loginThemePageIds } from "../generateFtl";
import { id } from "tsafe/id";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import * as fs from "fs";
import { join as pathJoin } from "path";
export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
const { themeSrcDirPath, themeType } = params;
const filePaths = crawl({
"dirPath": pathJoin(themeSrcDirPath, themeType),
"returnedPathsType": "absolute"
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
const candidateFilePaths = filePaths.filter(filePath => /kcContext\.[^.]+$/.test(filePath));
if (candidateFilePaths.length === 0) {
candidateFilePaths.push(...filePaths);
}
const extraPages: string[] = [];
for (const candidateFilPath of candidateFilePaths) {
const rawSourceFile = fs.readFileSync(candidateFilPath).toString("utf8");
extraPages.push(...Array.from(rawSourceFile.matchAll(/["']?pageId["']?\s*:\s*["']([^.]+.ftl)["']/g), m => m[1]));
}
return extraPages.reduce(...removeDuplicates<string>()).filter(pageId => {
switch (themeType) {
case "account":
return !id<readonly string[]>(accountThemePageIds).includes(pageId);
case "login":
return !id<readonly string[]>(loginThemePageIds).includes(pageId);
}
});
}

View File

@ -73,9 +73,7 @@ export function readFieldNameUsage(params: {
})() })()
] as const ] as const
).filter(exclude(undefined))) { ).filter(exclude(undefined))) {
const filePaths = crawl(srcDirPath) const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
.filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath))
.map(filePath => pathJoin(srcDirPath, filePath));
for (const filePath of filePaths) { for (const filePath of filePaths) {
const rawSourceFile = fs.readFileSync(filePath).toString("utf8"); const rawSourceFile = fs.readFileSync(filePath).toString("utf8");
@ -85,7 +83,7 @@ export function readFieldNameUsage(params: {
} }
fieldNames.push( fieldNames.push(
...Array.from(rawSourceFile.matchAll(/(?:(?:printIfExists)|(?:existsError)|(?:get)|(?:exists))\(["']([^"']+)["']/g), m => m[1]) ...Array.from(rawSourceFile.matchAll(/(?:(?:printIfExists)|(?:existsError)|(?:get)|(?:exists))\(\s*["']([^"']+)["']/g), m => m[1])
); );
} }
} }

View File

@ -11,10 +11,6 @@ export type ParsedPackageJson = {
version?: string; version?: string;
homepage?: string; homepage?: string;
keycloakify?: { keycloakify?: {
/** @deprecated: use extraLoginPages instead */
extraPages?: string[];
extraLoginPages?: string[];
extraAccountPages?: string[];
extraThemeProperties?: string[]; extraThemeProperties?: string[];
areAppAndKeycloakServerSharingSameDomain?: boolean; areAppAndKeycloakServerSharingSameDomain?: boolean;
artifactId?: string; artifactId?: string;
@ -34,9 +30,6 @@ export const zParsedPackageJson = z.object({
"homepage": z.string().optional(), "homepage": z.string().optional(),
"keycloakify": z "keycloakify": z
.object({ .object({
"extraPages": z.array(z.string()).optional(),
"extraLoginPages": z.array(z.string()).optional(),
"extraAccountPages": z.array(z.string()).optional(),
"extraThemeProperties": z.array(z.string()).optional(), "extraThemeProperties": z.array(z.string()).optional(),
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(), "areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
"artifactId": z.string().optional(), "artifactId": z.string().optional(),

View File

@ -1,9 +1,7 @@
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
/** List all files in a given directory return paths relative to the dir_path */ const crawlRec = (dir_path: string, paths: string[]) => {
export const crawl = (() => {
const crawlRec = (dir_path: string, paths: string[]) => {
for (const file_name of fs.readdirSync(dir_path)) { for (const file_name of fs.readdirSync(dir_path)) {
const file_path = path.join(dir_path, file_name); const file_path = path.join(dir_path, file_name);
@ -15,13 +13,20 @@ export const crawl = (() => {
paths.push(file_path); paths.push(file_path);
} }
}; };
return function crawl(dir_path: string): string[] { /** List all files in a given directory return paths relative to the dir_path */
const paths: string[] = []; export function crawl(params: { dirPath: string; returnedPathsType: "absolute" | "relative to dirPath" }): string[] {
const { dirPath, returnedPathsType } = params;
crawlRec(dir_path, paths); const filePaths: string[] = [];
return paths.map(file_path => path.relative(dir_path, file_path)); crawlRec(dirPath, filePaths);
};
})(); switch (returnedPathsType) {
case "absolute":
return filePaths;
case "relative to dirPath":
return filePaths.map(filePath => path.relative(dirPath, filePath));
}
}

View File

@ -20,12 +20,12 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
})) }))
} = params; } = params;
for (const file_relative_path of crawl(srcDirPath)) { for (const file_relative_path of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) {
const filePath = path.join(srcDirPath, file_relative_path); const filePath = path.join(srcDirPath, file_relative_path);
const transformSourceCodeResult = transformSourceCode({ const transformSourceCodeResult = transformSourceCode({
"sourceCode": fs.readFileSync(filePath), "sourceCode": fs.readFileSync(filePath),
"filePath": path.join(srcDirPath, file_relative_path) filePath
}); });
if (transformSourceCodeResult === undefined) { if (transformSourceCodeResult === undefined) {

View File

@ -211,7 +211,8 @@ const keycloakifyExtraMessages = {
"shouldBeDifferent": "{0} should be different to {1}", "shouldBeDifferent": "{0} should be different to {1}",
"shouldMatchPattern": "Pattern should match: `/{0}/`", "shouldMatchPattern": "Pattern should match: `/{0}/`",
"mustBeAnInteger": "Must be an integer", "mustBeAnInteger": "Must be an integer",
"notAValidOption": "Not a valid option" "notAValidOption": "Not a valid option",
"selectAnOption": "Select an option"
}, },
"fr": { "fr": {
/* spell-checker: disable */ /* spell-checker: disable */
@ -223,7 +224,8 @@ const keycloakifyExtraMessages = {
"logoutConfirmTitle": "Déconnexion", "logoutConfirmTitle": "Déconnexion",
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?", "logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
"doLogout": "Se déconnecter" "doLogout": "Se déconnecter",
"selectAnOption": "Sélectionner une option"
/* spell-checker: enable */ /* spell-checker: enable */
} }
}; };

View File

@ -17,7 +17,7 @@ export type UserProfileFormFieldsProps = {
export function UserProfileFormFields(props: UserProfileFormFieldsProps) { export function UserProfileFormFields(props: UserProfileFormFieldsProps) {
const { kcContext, onIsFormSubmittableValueChange, i18n, getClassName, BeforeField, AfterField } = props; const { kcContext, onIsFormSubmittableValueChange, i18n, getClassName, BeforeField, AfterField } = props;
const { advancedMsg } = i18n; const { advancedMsg, msg } = i18n;
const { const {
formValidationState: { fieldStateByAttributeName, isFormSubmittable }, formValidationState: { fieldStateByAttributeName, isFormSubmittable },
@ -98,11 +98,16 @@ export function UserProfileFormFields(props: UserProfileFormFieldsProps) {
} }
value={value} value={value}
> >
<>
<option value="" selected disabled hidden>
{msg("selectAnOption")}
</option>
{options.options.map(option => ( {options.options.map(option => (
<option key={option} value={option}> <option key={option} value={option}>
{option} {option}
</option> </option>
))} ))}
</>
</select> </select>
); );
} }