Compare commits

...

27 Commits

Author SHA1 Message Date
20937c4f72 Relase candidate 2023-03-19 15:53:13 +01:00
dbbfa07639 Feature new script: Eject-keycloak-page 2023-03-19 15:52:41 +01:00
9e1a4cad5c Update homepage 2023-03-19 15:49:27 +01:00
02bbedcfca Remove no longer used tools 2023-03-19 14:56:30 +01:00
cd70d90914 Refactor completed 2023-03-19 14:48:01 +01:00
819f297de8 Better i18n API 2023-03-19 14:03:06 +01:00
0608adde89 Better naming convention for i18n API 2023-03-19 13:54:39 +01:00
ad7bcf4669 Fix lining script 2023-03-18 19:05:27 +01:00
2eccc86e83 Remove eventEmitter warning 2023-03-18 19:02:17 +01:00
16d18f23a1 Refactor of the main component and i18n 2023-03-18 18:54:33 +01:00
5631ae1b6c Better naming convention 2023-03-18 18:27:50 +01:00
5fb29992f6 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2023-03-18 16:58:21 +01:00
910d633ac2 Fix build test app 2023-03-18 16:57:58 +01:00
32f8380e56 Fix build:test 2023-03-18 16:20:21 +01:00
43e4dd6bb6 Fix build for real 2023-03-18 16:17:33 +01:00
4f0b1688db Thank you @justkey007 for tsc-alias 2023-03-18 15:49:45 +01:00
9ae8822e00 Fix build 2023-03-18 06:25:19 +01:00
babffd1fe6 Make the project compile 2023-03-18 06:14:05 +01:00
5615d62032 Scripts dir outside of the src dir 2023-03-18 02:12:12 +01:00
4b89d15c1e progressing 2023-03-17 20:40:29 +01:00
815f510d5f Exclude tsbuild info of the bin dir from the bundle 2023-03-16 23:03:18 +01:00
199ba193be Rename getKcContext dir to kcContext 2023-03-16 23:02:06 +01:00
4ae9bd3f9a Fix repo url in package.json 2023-03-16 22:57:24 +01:00
1c9cf639ea Remove console log 2023-03-16 22:44:44 +01:00
0040464ca1 Move lib up one level 2023-03-16 22:43:09 +01:00
79997efbb6 First commit towars supporting account theme 2023-03-16 22:13:46 +01:00
0e42009798 Fix bin test script 2023-03-16 14:39:40 +01:00
316 changed files with 2354 additions and 2375 deletions

View File

@ -1,32 +1,34 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "6.13.2", "version": "7.0.0-rc.0",
"description": "Create Keycloak themes using React", "description": "Create Keycloak themes using React",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/garronej/keycloakify.git" "url": "git://github.com/inseefrlab/keycloakify.git"
}, },
"main": "dist/lib/index.js", "main": "dist/index.js",
"types": "dist/lib/index.d.ts", "types": "dist/index.d.ts",
"scripts": { "scripts": {
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src/lib && yarn grant-exec-perms && yarn copy-files dist/", "build": "rimraf dist/ && tsc -p src/bin && tsc -p src/tsconfig.json && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/",
"build:test": "rimraf dist_test/ && tsc -p src/test && yarn copy-files dist_test/", "build:watch": "tsc -p src/tsconfig.json && (concurrently \"tsc -p src/tsconfig.json -w\" \"tsc-alias -p src/tsconfig.json\")",
"build:test": "rimraf dist_test/ && tsc -p test/tsconfig.json && tsc-alias -p test/tsconfig.json && yarn copy-files dist_test/src",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js", "grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl", "copy-files": "copyfiles -u 1 src/**/*.ftl",
"pretest": "yarn build:test", "test": "yarn build:test && node dist_test/test/bin && node dist_test/test/lib",
"test": "node dist_test/test/bin && node dist_test/test/lib", "test:sample-app": "yarn build:test && node dist_test/test/bin/main.js",
"_format": "prettier '**/*.{ts,tsx,json,md}'", "_format": "prettier '**/*.{ts,tsx,json,md}'",
"format": "yarn _format --write", "format": "yarn _format --write",
"format:check": "yarn _format --list-different", "format:check": "yarn _format --list-different",
"generate-messages": "ts-node --skipProject src/scripts/generate-i18n-messages.ts", "generate-messages": "ts-node --skipProject scripts/generate-i18n-messages.ts",
"link-in-app": "ts-node --skipProject src/scripts/link-in-app.ts", "link-in-app": "ts-node --skipProject scripts/link-in-app.ts",
"link-in-starter": "yarn link-in-app keycloakify-starter", "link-in-starter": "yarn link-in-app keycloakify-starter",
"tsc-watch": "tsc -p src/bin -w & tsc -p src/lib -w " "tsc-watch": "tsc -p src/bin -w & tsc -p src/lib -w "
}, },
"bin": { "bin": {
"keycloakify": "dist/bin/keycloakify/index.js", "keycloakify": "dist/bin/keycloakify/index.js",
"create-keycloak-email-directory": "dist/bin/create-keycloak-email-directory.js", "create-keycloak-email-directory": "dist/bin/create-keycloak-email-directory.js",
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js" "download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js",
"eject-keycloak-page": "dist/bin/eject-keycloak-page.js"
}, },
"lint-staged": { "lint-staged": {
"*.{ts,tsx,json,md}": [ "*.{ts,tsx,json,md}": [
@ -42,9 +44,9 @@
"license": "MIT", "license": "MIT",
"files": [ "files": [
"src/", "src/",
"!src/scripts",
"dist/", "dist/",
"!dist/tsconfig.tsbuildinfo" "!dist/tsconfig.tsbuildinfo",
"!dist/bin/tsconfig.tsbuildinfo"
], ],
"keywords": [ "keywords": [
"bluehats", "bluehats",
@ -56,7 +58,7 @@
"login", "login",
"register" "register"
], ],
"homepage": "https://github.com/garronej/keycloakify", "homepage": "https://www.keycloakify.dev",
"peerDependencies": { "peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}, },
@ -66,6 +68,7 @@
"@types/minimist": "^1.2.2", "@types/minimist": "^1.2.2",
"@types/node": "^18.14.1", "@types/node": "^18.14.1",
"@types/react": "18.0.9", "@types/react": "18.0.9",
"concurrently": "^7.6.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"husky": "^4.3.8", "husky": "^4.3.8",
"lint-staged": "^11.0.0", "lint-staged": "^11.0.0",
@ -75,7 +78,8 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"scripting-tools": "^0.19.13", "scripting-tools": "^0.19.13",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.9.5" "tsc-alias": "^1.8.3",
"typescript": "^5.0.1-rc"
}, },
"dependencies": { "dependencies": {
"@octokit/rest": "^18.12.0", "@octokit/rest": "^18.12.0",

View File

@ -1,11 +1,11 @@
import "minimal-polyfills/Object.fromEntries"; import "minimal-polyfills/Object.fromEntries";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { crawl } from "../bin/tools/crawl"; import { crawl } from "../src/bin/tools/crawl";
import { downloadBuiltinKeycloakTheme } from "../bin/download-builtin-keycloak-theme"; import { downloadBuiltinKeycloakTheme } from "../src/bin/download-builtin-keycloak-theme";
import { getProjectRoot } from "../bin/tools/getProjectRoot"; import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
import { getCliOptions } from "../bin/tools/cliOptions"; import { getCliOptions } from "../src/bin/tools/cliOptions";
import { getLogger } from "../bin/tools/logger"; import { getLogger } from "../src/bin/tools/logger";
//NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files, //NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
// update the version array for generating for newer version. // update the version array for generating for newer version.

View File

@ -1,10 +1,11 @@
import { execSync } from "child_process"; import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path"; import { join as pathJoin, relative as pathRelative } from "path";
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
import * as fs from "fs"; import * as fs from "fs";
const singletonDependencies: string[] = ["react", "@types/react"]; const singletonDependencies: string[] = ["react", "@types/react"];
const rootDirPath = pathJoin(__dirname, "..", ".."); const rootDirPath = getProjectRoot();
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58 //NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
fs.writeFileSync( fs.writeFileSync(

81
src/Fallback.tsx Normal file
View File

@ -0,0 +1,81 @@
import { lazy, Suspense } from "react";
import type { PageProps } from "keycloakify/pages/PageProps";
import type { I18n } from "keycloakify/i18n";
import type { KcContext } from "./kcContext";
const Login = lazy(() => import("keycloakify/pages/Login"));
const Register = lazy(() => import("keycloakify/pages/Register"));
const RegisterUserProfile = lazy(() => import("keycloakify/pages/RegisterUserProfile"));
const Info = lazy(() => import("keycloakify/pages/Info"));
const Error = lazy(() => import("keycloakify/pages/Error"));
const LoginResetPassword = lazy(() => import("keycloakify/pages/LoginResetPassword"));
const LoginVerifyEmail = lazy(() => import("keycloakify/pages/LoginVerifyEmail"));
const Terms = lazy(() => import("keycloakify/pages/Terms"));
const LoginOtp = lazy(() => import("keycloakify/pages/LoginOtp"));
const LoginPassword = lazy(() => import("keycloakify/pages/LoginPassword"));
const LoginUsername = lazy(() => import("keycloakify/pages/LoginUsername"));
const WebauthnAuthenticate = lazy(() => import("keycloakify/pages/WebauthnAuthenticate"));
const LoginUpdatePassword = lazy(() => import("keycloakify/pages/LoginUpdatePassword"));
const LoginUpdateProfile = lazy(() => import("keycloakify/pages/LoginUpdateProfile"));
const LoginIdpLinkConfirm = lazy(() => import("keycloakify/pages/LoginIdpLinkConfirm"));
const LoginPageExpired = lazy(() => import("keycloakify/pages/LoginPageExpired"));
const LoginIdpLinkEmail = lazy(() => import("keycloakify/pages/LoginIdpLinkEmail"));
const LoginConfigTotp = lazy(() => import("keycloakify/pages/LoginConfigTotp"));
const LogoutConfirm = lazy(() => import("keycloakify/pages/LogoutConfirm"));
const UpdateUserProfile = lazy(() => import("keycloakify/pages/UpdateUserProfile"));
const IdpReviewUserProfile = lazy(() => import("keycloakify/pages/IdpReviewUserProfile"));
export default function Fallback(props: PageProps<KcContext, I18n>) {
const { kcContext, ...rest } = props;
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
case "login.ftl":
return <Login kcContext={kcContext} {...rest} />;
case "register.ftl":
return <Register kcContext={kcContext} {...rest} />;
case "register-user-profile.ftl":
return <RegisterUserProfile kcContext={kcContext} {...rest} />;
case "info.ftl":
return <Info kcContext={kcContext} {...rest} />;
case "error.ftl":
return <Error kcContext={kcContext} {...rest} />;
case "login-reset-password.ftl":
return <LoginResetPassword kcContext={kcContext} {...rest} />;
case "login-verify-email.ftl":
return <LoginVerifyEmail kcContext={kcContext} {...rest} />;
case "terms.ftl":
return <Terms kcContext={kcContext} {...rest} />;
case "login-otp.ftl":
return <LoginOtp kcContext={kcContext} {...rest} />;
case "login-username.ftl":
return <LoginUsername kcContext={kcContext} {...rest} />;
case "login-password.ftl":
return <LoginPassword kcContext={kcContext} {...rest} />;
case "webauthn-authenticate.ftl":
return <WebauthnAuthenticate kcContext={kcContext} {...rest} />;
case "login-update-password.ftl":
return <LoginUpdatePassword kcContext={kcContext} {...rest} />;
case "login-update-profile.ftl":
return <LoginUpdateProfile kcContext={kcContext} {...rest} />;
case "login-idp-link-confirm.ftl":
return <LoginIdpLinkConfirm kcContext={kcContext} {...rest} />;
case "login-idp-link-email.ftl":
return <LoginIdpLinkEmail kcContext={kcContext} {...rest} />;
case "login-page-expired.ftl":
return <LoginPageExpired kcContext={kcContext} {...rest} />;
case "login-config-totp.ftl":
return <LoginConfigTotp kcContext={kcContext} {...rest} />;
case "logout-confirm.ftl":
return <LogoutConfirm kcContext={kcContext} {...rest} />;
case "update-user-profile.ftl":
return <UpdateUserProfile kcContext={kcContext} {...rest} />;
case "idp-review-user-profile.ftl":
return <IdpReviewUserProfile kcContext={kcContext} {...rest} />;
}
})()}
</Suspense>
);
}

View File

@ -1,13 +1,12 @@
import React, { useReducer, useEffect } from "react"; import { assert } from "keycloakify/tools/assert";
import { assert } from "./tools/assert"; import { clsx } from "keycloakify/tools/clsx";
import { headInsert } from "./tools/headInsert"; import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
import { pathJoin } from "../bin/tools/pathJoin"; import { type TemplateProps, defaultTemplateClasses } from "keycloakify/TemplateProps";
import { clsx } from "./tools/clsx"; import { useGetClassName } from "keycloakify/lib/useGetClassName";
import type { TemplateProps } from "./KcProps"; import type { KcContext } from "./kcContext";
import type { KcContextBase } from "./getKcContext/KcContextBase"; import type { I18n } from "./i18n";
import type { I18nBase } from "./i18n";
export default function Template(props: TemplateProps<KcContextBase.Common, I18nBase>) { export default function Template(props: TemplateProps<KcContext, I18n>) {
const { const {
displayInfo = false, displayInfo = false,
displayMessage = true, displayMessage = true,
@ -20,24 +19,29 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
infoNode = null, infoNode = null,
kcContext, kcContext,
i18n, i18n,
doFetchDefaultThemeResources, doUseDefaultCss,
stylesCommon, classes
styles,
scripts,
kcHtmlClass
} = props; } = props;
const { getClassName } = useGetClassName({
"defaultClasses": !doUseDefaultCss ? undefined : defaultTemplateClasses,
classes
});
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n; const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext; const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext;
const { isReady } = usePrepareTemplate({ const { isReady } = usePrepareTemplate({
doFetchDefaultThemeResources, "doFetchDefaultThemeResources": doUseDefaultCss,
stylesCommon,
styles,
scripts,
url, url,
kcHtmlClass "stylesCommon": [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
"lib/zocial/zocial.css"
],
"styles": ["css/login.css"],
"htmlClassName": getClassName("kcHtmlClass")
}); });
if (!isReady) { if (!isReady) {
@ -45,18 +49,18 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
} }
return ( return (
<div className={clsx(props.kcLoginClass)}> <div className={getClassName("kcLoginClass")}>
<div id="kc-header" className={clsx(props.kcHeaderClass)}> <div id="kc-header" className={getClassName("kcHeaderClass")}>
<div id="kc-header-wrapper" className={clsx(props.kcHeaderWrapperClass)}> <div id="kc-header-wrapper" className={getClassName("kcHeaderWrapperClass")}>
{msg("loginTitleHtml", realm.displayNameHtml)} {msg("loginTitleHtml", realm.displayNameHtml)}
</div> </div>
</div> </div>
<div className={clsx(props.kcFormCardClass, displayWide && props.kcFormCardAccountClass)}> <div className={clsx(getClassName("kcFormCardClass"), displayWide && getClassName("kcFormCardAccountClass"))}>
<header className={clsx(props.kcFormHeaderClass)}> <header className={getClassName("kcFormHeaderClass")}>
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && ( {realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
<div id="kc-locale"> <div id="kc-locale">
<div id="kc-locale-wrapper" className={clsx(props.kcLocaleWrapperClass)}> <div id="kc-locale-wrapper" className={getClassName("kcLocaleWrapperClass")}>
<div className="kc-dropdown" id="kc-locale-dropdown"> <div className="kc-dropdown" id="kc-locale-dropdown">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" id="kc-current-locale-link"> <a href="#" id="kc-current-locale-link">
@ -78,8 +82,8 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
)} )}
{!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? ( {!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
displayRequiredFields ? ( displayRequiredFields ? (
<div className={clsx(props.kcContentWrapperClass)}> <div className={getClassName("kcContentWrapperClass")}>
<div className={clsx(props.kcLabelWrapperClass, "subtitle")}> <div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
<span className="subtitle"> <span className="subtitle">
<span className="required">*</span> <span className="required">*</span>
{msg("requiredFields")} {msg("requiredFields")}
@ -93,20 +97,20 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
<h1 id="kc-page-title">{headerNode}</h1> <h1 id="kc-page-title">{headerNode}</h1>
) )
) : displayRequiredFields ? ( ) : displayRequiredFields ? (
<div className={clsx(props.kcContentWrapperClass)}> <div className={getClassName("kcContentWrapperClass")}>
<div className={clsx(props.kcLabelWrapperClass, "subtitle")}> <div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
<span className="subtitle"> <span className="subtitle">
<span className="required">*</span> {msg("requiredFields")} <span className="required">*</span> {msg("requiredFields")}
</span> </span>
</div> </div>
<div className="col-md-10"> <div className="col-md-10">
{showUsernameNode} {showUsernameNode}
<div className={clsx(props.kcFormGroupClass)}> <div className={getClassName("kcFormGroupClass")}>
<div id="kc-username"> <div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label> <label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}> <a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip"> <div className="kc-login-tooltip">
<i className={clsx(props.kcResetFlowIcon)}></i> <i className={getClassName("kcResetFlowIcon")}></i>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span> <span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div> </div>
</a> </a>
@ -117,12 +121,12 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
) : ( ) : (
<> <>
{showUsernameNode} {showUsernameNode}
<div className={clsx(props.kcFormGroupClass)}> <div className={getClassName("kcFormGroupClass")}>
<div id="kc-username"> <div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label> <label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}> <a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip"> <div className="kc-login-tooltip">
<i className={clsx(props.kcResetFlowIcon)}></i> <i className={getClassName("kcResetFlowIcon")}></i>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span> <span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div> </div>
</a> </a>
@ -136,10 +140,10 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */} {/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
{displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && ( {displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
<div className={clsx("alert", `alert-${message.type}`)}> <div className={clsx("alert", `alert-${message.type}`)}>
{message.type === "success" && <span className={clsx(props.kcFeedbackSuccessIcon)}></span>} {message.type === "success" && <span className={getClassName("kcFeedbackSuccessIcon")}></span>}
{message.type === "warning" && <span className={clsx(props.kcFeedbackWarningIcon)}></span>} {message.type === "warning" && <span className={getClassName("kcFeedbackWarningIcon")}></span>}
{message.type === "error" && <span className={clsx(props.kcFeedbackErrorIcon)}></span>} {message.type === "error" && <span className={getClassName("kcFeedbackErrorIcon")}></span>}
{message.type === "info" && <span className={clsx(props.kcFeedbackInfoIcon)}></span>} {message.type === "info" && <span className={getClassName("kcFeedbackInfoIcon")}></span>}
<span <span
className="kc-feedback-text" className="kc-feedback-text"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
@ -154,10 +158,14 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
id="kc-select-try-another-way-form" id="kc-select-try-another-way-form"
action={url.loginAction} action={url.loginAction}
method="post" method="post"
className={clsx(displayWide && props.kcContentWrapperClass)} className={clsx(displayWide && getClassName("kcContentWrapperClass"))}
> >
<div className={clsx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}> <div
<div className={clsx(props.kcFormGroupClass)}> className={clsx(
displayWide && [getClassName("kcFormSocialAccountContentClass"), getClassName("kcFormSocialAccountClass")]
)}
>
<div className={getClassName("kcFormGroupClass")}>
<input type="hidden" name="tryAnotherWay" value="on" /> <input type="hidden" name="tryAnotherWay" value="on" />
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a <a
@ -175,8 +183,8 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
</form> </form>
)} )}
{displayInfo && ( {displayInfo && (
<div id="kc-info" className={clsx(props.kcSignUpClass)}> <div id="kc-info" className={getClassName("kcSignUpClass")}>
<div id="kc-info-wrapper" className={clsx(props.kcInfoAreaWrapperClass)}> <div id="kc-info-wrapper" className={getClassName("kcInfoAreaWrapperClass")}>
{infoNode} {infoNode}
</div> </div>
</div> </div>
@ -187,79 +195,3 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
</div> </div>
); );
} }
export function usePrepareTemplate(params: {
doFetchDefaultThemeResources: boolean;
stylesCommon: string | readonly string[] | undefined;
styles: string | readonly string[] | undefined;
scripts: string | readonly string[] | undefined;
url: {
resourcesCommonPath: string;
resourcesPath: string;
};
kcHtmlClass: string | readonly string[] | undefined;
}) {
const { doFetchDefaultThemeResources, stylesCommon, styles, url, scripts, kcHtmlClass } = params;
const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources);
useEffect(() => {
if (!doFetchDefaultThemeResources) {
return;
}
let isUnmounted = false;
const toArr = (x: string | readonly string[] | undefined) => (typeof x === "string" ? x.split(" ") : x ?? []);
Promise.all(
[
...toArr(stylesCommon).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
...toArr(styles).map(relativePath => pathJoin(url.resourcesPath, relativePath))
]
.reverse()
.map(href =>
headInsert({
"type": "css",
href,
"position": "prepend"
})
)
).then(() => {
if (isUnmounted) {
return;
}
setReady();
});
toArr(scripts).forEach(relativePath =>
headInsert({
"type": "javascript",
"src": pathJoin(url.resourcesPath, relativePath)
})
);
return () => {
isUnmounted = true;
};
}, [kcHtmlClass]);
useEffect(() => {
if (kcHtmlClass === undefined) {
return;
}
const htmlClassList = document.getElementsByTagName("html")[0].classList;
const tokens = clsx(kcHtmlClass).split(" ");
htmlClassList.add(...tokens);
return () => {
htmlClassList.remove(...tokens);
};
}, [kcHtmlClass]);
return { isReady };
}

65
src/TemplateProps.ts Normal file
View File

@ -0,0 +1,65 @@
import type { ReactNode } from "react";
import type { KcContext } from "./kcContext";
import type { I18n } from "./i18n";
export type TemplateProps<KcContext extends KcContext.Common, I18nExtended extends I18n> = {
kcContext: KcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;
classes?: Partial<Record<TemplateClassKey, string>>;
formNode: ReactNode;
displayInfo?: boolean;
displayMessage?: boolean;
displayRequiredFields?: boolean;
displayWide?: boolean;
showAnotherWayIfPresent?: boolean;
headerNode: ReactNode;
showUsernameNode?: ReactNode;
infoNode?: ReactNode;
};
export type TemplateClassKey =
| "kcHtmlClass"
| "kcLoginClass"
| "kcHeaderClass"
| "kcHeaderWrapperClass"
| "kcFormCardClass"
| "kcFormCardAccountClass"
| "kcFormHeaderClass"
| "kcLocaleWrapperClass"
| "kcContentWrapperClass"
| "kcLabelWrapperClass"
| "kcFormGroupClass"
| "kcResetFlowIcon"
| "kcFeedbackSuccessIcon"
| "kcFeedbackWarningIcon"
| "kcFeedbackErrorIcon"
| "kcFeedbackInfoIcon"
| "kcFormSocialAccountContentClass"
| "kcFormSocialAccountClass"
| "kcSignUpClass"
| "kcInfoAreaWrapperClass";
export const defaultTemplateClasses: Record<TemplateClassKey, string | undefined> = {
"kcHtmlClass": "login-pf",
"kcLoginClass": "login-pf-page",
"kcContentWrapperClass": "row",
"kcHeaderClass": "login-pf-page-header",
"kcHeaderWrapperClass": undefined,
"kcFormCardClass": "card-pf",
"kcFormCardAccountClass": "login-pf-accounts",
"kcFormSocialAccountClass": "login-pf-social-section",
"kcFormSocialAccountContentClass": "col-xs-12 col-sm-6",
"kcFormHeaderClass": "login-pf-header",
"kcLocaleWrapperClass": undefined,
"kcFeedbackErrorIcon": "pficon pficon-error-circle-o",
"kcFeedbackWarningIcon": "pficon pficon-warning-triangle-o",
"kcFeedbackSuccessIcon": "pficon pficon-ok",
"kcFeedbackInfoIcon": "pficon pficon-info",
"kcResetFlowIcon": "pficon pficon-arrow fa-2x",
"kcFormGroupClass": "form-group",
"kcLabelWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcSignUpClass": "login-pf-signup",
"kcInfoAreaWrapperClass": undefined
};

View File

@ -0,0 +1,38 @@
#!/usr/bin/env node
import { getProjectRoot } from "./tools/getProjectRoot";
import cliSelect from "cli-select";
import { loginThemePageIds, type PageId } from "./keycloakify/generateFtl/generateFtl";
import { capitalize } from "tsafe/capitalize";
import { readFile, writeFile } from "fs/promises";
import { existsSync } from "fs";
import { join as pathJoin, relative as pathRelative } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
(async () => {
const projectRootDir = getProjectRoot();
const { value: pageId } = await cliSelect<PageId>({
"values": [...loginThemePageIds]
}).catch(() => {
console.log("Aborting");
process.exit(-1);
});
const pageBasename = `${capitalize(kebabCaseToCamelCase(pageId))}.tsx`;
console.log(pageId);
const targetFilePath = pathJoin(process.cwd(), "src", "keycloak-theme", "pages", pageBasename);
if (existsSync(targetFilePath)) {
console.log(`${pageId} is already ejected, ${pathRelative(process.cwd(), targetFilePath)} already exists`);
process.exit(-1);
}
writeFile(targetFilePath, await readFile(pathJoin(projectRootDir, "src", "pages", pageBasename)));
console.log(`${pathRelative(process.cwd(), targetFilePath)} created`);
})();

View File

@ -13,7 +13,10 @@ type ParsedPackageJson = {
version: string; version: string;
homepage?: string; homepage?: string;
keycloakify?: { keycloakify?: {
/** @deprecated: use extraLoginPages instead */
extraPages?: string[]; extraPages?: string[];
extraLoginPages?: string[];
extraAccountPages?: string[];
extraThemeProperties?: string[]; extraThemeProperties?: string[];
areAppAndKeycloakServerSharingSameDomain?: boolean; areAppAndKeycloakServerSharingSameDomain?: boolean;
artifactId?: string; artifactId?: string;
@ -29,6 +32,8 @@ const zParsedPackageJson = z.object({
"keycloakify": z "keycloakify": z
.object({ .object({
"extraPages": z.array(z.string()).optional(), "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(),
@ -48,7 +53,8 @@ export namespace BuildOptions {
isSilent: boolean; isSilent: boolean;
version: string; version: string;
themeName: string; themeName: string;
extraPages?: string[]; extraLoginPages: string[] | undefined;
extraAccountPages: string[] | undefined;
extraThemeProperties?: string[]; extraThemeProperties?: string[];
groupId: string; groupId: string;
artifactId: string; artifactId: string;
@ -119,7 +125,7 @@ export function readBuildOptions(params: {
const common: BuildOptions.Common = (() => { const common: BuildOptions.Common = (() => {
const { name, keycloakify = {}, version, homepage } = parsedPackageJson; const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
const { extraPages, extraThemeProperties, groupId, artifactId, bundler } = keycloakify ?? {}; const { extraPages, extraLoginPages, extraAccountPages, extraThemeProperties, groupId, artifactId, bundler } = keycloakify ?? {};
const themeName = name const themeName = name
.replace(/^@(.*)/, "$1") .replace(/^@(.*)/, "$1")
@ -158,7 +164,8 @@ export function readBuildOptions(params: {
); );
})(), })(),
"version": process.env.KEYCLOAKIFY_VERSION ?? version, "version": process.env.KEYCLOAKIFY_VERSION ?? version,
extraPages, "extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])],
extraAccountPages,
extraThemeProperties, extraThemeProperties,
isSilent isSilent
}; };

View File

@ -10,8 +10,11 @@ import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect"; import { Reflect } from "tsafe/Reflect";
// https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java export const themeTypes = ["login", "account"] as const;
export const pageIds = [
export type ThemeType = (typeof themeTypes)[number];
export const loginThemePageIds = [
"login.ftl", "login.ftl",
"login-username.ftl", "login-username.ftl",
"login-password.ftl", "login-password.ftl",
@ -35,6 +38,11 @@ export const pageIds = [
"idp-review-user-profile.ftl" "idp-review-user-profile.ftl"
] as const; ] as const;
export const accountThemePageIds = ["password.ftl"] as const;
export type PageId = (typeof loginThemePageIds)[number];
export type AccountThemePageId = (typeof accountThemePageIds)[number];
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets; export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike { export namespace BuildOptionsLike {
@ -68,8 +76,6 @@ export namespace BuildOptionsLike {
assert<typeof buildOptions extends BuildOptionsLike ? true : false>(); assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
} }
export type PageId = (typeof pageIds)[number];
export function generateFtlFilesCodeFactory(params: { export function generateFtlFilesCodeFactory(params: {
indexHtmlCode: string; indexHtmlCode: string;
//NOTE: Expected to be an empty object if external assets mode is enabled. //NOTE: Expected to be an empty object if external assets mode is enabled.

View File

@ -1,5 +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 { themeTypes } from "./generateFtl/generateFtl";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect"; import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions"; import type { BuildOptions } from "./BuildOptions";
@ -69,7 +70,7 @@ export function generateJavaStackFiles(params: {
"themes": [ "themes": [
{ {
"name": themeName, "name": themeName,
"types": ["login", ...(doBundlesEmailTemplate ? ["email"] : [])] "types": [...themeTypes, ...(doBundlesEmailTemplate ? ["email"] : [])]
} }
] ]
}, },

View File

@ -3,7 +3,7 @@ import * as fs from "fs";
import { join as pathJoin, basename as pathBasename } from "path"; import { join as pathJoin, basename as pathBasename } from "path";
import { replaceImportsFromStaticInJsCode } from "./replacers/replaceImportsFromStaticInJsCode"; import { replaceImportsFromStaticInJsCode } from "./replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInCssCode } from "./replacers/replaceImportsInCssCode"; import { replaceImportsInCssCode } from "./replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl"; import { generateFtlFilesCodeFactory, loginThemePageIds, themeTypes, ThemeType } from "./generateFtl";
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme"; import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath"; import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath";
import { isInside } from "../tools/isInside"; import { isInside } from "../tools/isInside";
@ -17,7 +17,8 @@ export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.Ex
export namespace BuildOptionsLike { export namespace BuildOptionsLike {
export type Common = { export type Common = {
themeName: string; themeName: string;
extraPages?: string[]; extraLoginPages?: string[];
extraAccountPages?: string[];
extraThemeProperties?: string[]; extraThemeProperties?: string[];
isSilent: boolean; isSilent: boolean;
}; };
@ -62,58 +63,154 @@ export async function generateKeycloakThemeResources(params: {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params; const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
const logger = getLogger({ isSilent: buildOptions.isSilent }); const logger = getLogger({ isSilent: buildOptions.isSilent });
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login");
const getThemeDirPath = (themeType: ThemeType | "email") =>
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
let allCssGlobalsToDefine: Record<string, string> = {}; let allCssGlobalsToDefine: Record<string, string> = {};
transformCodebase({ let generateFtlFilesCode_glob: ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"] | undefined = undefined;
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
"srcDirPath": reactAppBuildDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
if (
buildOptions.isStandalone &&
isInside({
"dirPath": pathJoin(reactAppBuildDirPath, mockTestingSubDirOfPublicDirBasename),
filePath
})
) {
return undefined;
}
if (/\.css?$/i.test(filePath)) { for (const themeType of themeTypes) {
if (!buildOptions.isStandalone) { if (themeType === "account") {
return undefined; continue;
}
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
"cssCode": sourceCode.toString("utf8")
});
allCssGlobalsToDefine = {
...allCssGlobalsToDefine,
...cssGlobalsToDefine
};
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
}
if (/\.js?$/i.test(filePath)) {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined;
}
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"),
buildOptions
});
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
}
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
} }
});
const themeDirPath = getThemeDirPath(themeType);
copy_app_resources_to_theme_path: {
const isFirstPass = themeType.indexOf(themeType) === 0;
if (!isFirstPass && !buildOptions.isStandalone) {
break copy_app_resources_to_theme_path;
}
transformCodebase({
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
"srcDirPath": reactAppBuildDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
if (
buildOptions.isStandalone &&
isInside({
"dirPath": pathJoin(reactAppBuildDirPath, mockTestingSubDirOfPublicDirBasename),
filePath
})
) {
return undefined;
}
if (/\.css?$/i.test(filePath)) {
if (!buildOptions.isStandalone) {
return undefined;
}
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
"cssCode": sourceCode.toString("utf8")
});
register_css_variables: {
if (!isFirstPass) {
break register_css_variables;
}
allCssGlobalsToDefine = {
...allCssGlobalsToDefine,
...cssGlobalsToDefine
};
}
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
}
if (/\.js?$/i.test(filePath)) {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined;
}
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"),
buildOptions
});
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
}
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
}
});
}
const generateFtlFilesCode = (() => {
if (generateFtlFilesCode_glob !== undefined) {
return generateFtlFilesCode_glob;
}
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine,
"buildOptions": buildOptions
});
return generateFtlFilesCode;
})();
[...loginThemePageIds, ...(buildOptions.extraLoginPages ?? [])].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeDirPath, { "recursive": true });
fs.writeFileSync(pathJoin(themeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
});
{
const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd");
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent: buildOptions.isSilent
});
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "login", "resources"),
"destDirPath": themeResourcesDirPath
});
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(mockTestingResourcesCommonPath))
});
transformCodebase({
"srcDirPath": themeResourcesDirPath,
"destDirPath": pathJoin(reactAppPublicDirPath, mockTestingResourcesPath)
});
const keycloakResourcesWithinPublicDirPath = pathJoin(reactAppPublicDirPath, mockTestingSubDirOfPublicDirBasename);
fs.writeFileSync(
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
Buffer.from(
["This is just a test folder that helps develop", "the login and register page without having to run a Keycloak container"].join(
" "
)
)
);
fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
fs.rmSync(tmpDirPath, { recursive: true, force: true });
}
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
);
}
let doBundlesEmailTemplate: boolean; let doBundlesEmailTemplate: boolean;
@ -133,69 +230,9 @@ export async function generateKeycloakThemeResources(params: {
transformCodebase({ transformCodebase({
"srcDirPath": keycloakThemeEmailDirPath, "srcDirPath": keycloakThemeEmailDirPath,
"destDirPath": pathJoin(themeDirPath, "..", "email") "destDirPath": getThemeDirPath("email")
}); });
} }
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine,
"buildOptions": buildOptions
});
[...pageIds, ...(buildOptions.extraPages ?? [])].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeDirPath, { "recursive": true });
fs.writeFileSync(pathJoin(themeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
});
{
const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd");
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent: buildOptions.isSilent
});
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "login", "resources"),
"destDirPath": themeResourcesDirPath
});
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(mockTestingResourcesCommonPath))
});
transformCodebase({
"srcDirPath": themeResourcesDirPath,
"destDirPath": pathJoin(reactAppPublicDirPath, mockTestingResourcesPath)
});
const keycloakResourcesWithinPublicDirPath = pathJoin(reactAppPublicDirPath, mockTestingSubDirOfPublicDirBasename);
fs.writeFileSync(
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
Buffer.from(
["This is just a test folder that helps develop", "the login and register page without having to run a Keycloak container"].join(" ")
)
);
fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
fs.rmSync(tmpDirPath, { recursive: true, force: true });
}
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
);
return { doBundlesEmailTemplate }; return { doBundlesEmailTemplate };
} }

View File

@ -130,6 +130,7 @@ async function unzip(zipFile: string, dir: string, archiveDir?: string): Promise
// Pull the file out of the archive, write it to the target directory // Pull the file out of the archive, write it to the target directory
const input = createRecordReadStream(); const input = createRecordReadStream();
const output = createWriteStream(filePath); const output = createWriteStream(filePath);
output.setMaxListeners(Infinity);
output.on("error", e => reject(Object.assign(e, { filePath }))); output.on("error", e => reject(Object.assign(e, { filePath })));
output.on("finish", () => resolve(filePath)); output.on("finish", () => resolve(filePath));
input.pipe(output); input.pipe(output);
@ -166,6 +167,7 @@ async function readFileChunk(file: string, start: number, end: number): Promise<
const chunks: Buffer[] = []; const chunks: Buffer[] = [];
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const stream = createReadStream(file, { start, end }); const stream = createReadStream(file, { start, end });
stream.setMaxListeners(Infinity);
stream.on("error", e => reject(e)); stream.on("error", e => reject(e));
stream.on("end", () => resolve(Buffer.concat(chunks))); stream.on("end", () => resolve(Buffer.concat(chunks)));
stream.on("data", chunk => chunks.push(chunk as Buffer)); stream.on("data", chunk => chunks.push(chunk as Buffer));

View File

@ -0,0 +1,7 @@
import { capitalize } from "tsafe/capitalize";
export function kebabCaseToCamelCase(kebabCaseString: string): string {
const [first, ...rest] = kebabCaseString.split("-");
return [first, rest.map(capitalize)].join("");
}

Some files were not shown because too many files have changed in this diff Show More