Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
d5bcfcd4dd | |||
8647f00bab | |||
da456ee3b8 | |||
c972a04dda | |||
1148a02d74 | |||
1d3112b2cd | |||
80556d599c | |||
a9826ebcc7 | |||
5f3f25eee9 | |||
ac085286a9 | |||
7af9c7177a | |||
67587012e5 | |||
18b5948136 | |||
9808bc7a14 | |||
3b96e51eef | |||
f03b89ea1e | |||
cd318a9eaf |
@ -158,34 +158,6 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zavoloklom",
|
||||
"name": "Sergey Kupletsky",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4151869?v=4",
|
||||
"profile": "https://github.com/zavoloklom",
|
||||
"contributions": [
|
||||
"test",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rome-user",
|
||||
"name": "rome-user",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/114131048?v=4",
|
||||
"profile": "https://github.com/rome-user",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "celinepelletier",
|
||||
"name": "Céline Pelletier",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/82821620?v=4",
|
||||
"profile": "https://github.com/celinepelletier",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
@ -193,6 +165,5 @@
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"projectName": "keycloakify",
|
||||
"projectOwner": "keycloakify",
|
||||
"commitType": "docs"
|
||||
"projectOwner": "keycloakify"
|
||||
}
|
||||
|
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@ -3,7 +3,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
@ -35,7 +34,7 @@ jobs:
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
#- run: yarn test:keycloakify-starter
|
||||
- run: yarn test:keycloakify-starter
|
||||
|
||||
storybook:
|
||||
runs-on: ubuntu-latest
|
||||
@ -130,10 +129,7 @@ jobs:
|
||||
echo "Can't publish on NPM, You must first create a secret called NPM_TOKEN that contains your NPM auth token. https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets"
|
||||
false
|
||||
fi
|
||||
EXTRA_ARGS=""
|
||||
if [ "$IS_PRE_RELEASE" = "true" ]; then
|
||||
EXTRA_ARGS="--tag next"
|
||||
fi
|
||||
EXTRA_ARGS="--tag v7"
|
||||
npm publish $EXTRA_ARGS
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
|
74
README.md
74
README.md
@ -47,12 +47,10 @@
|
||||
|
||||
> 📣 🛑 Account themes generated by Keycloakify are not currently compatible with Keycloak 22.
|
||||
> We are working on a solution. [Follow progress](https://github.com/keycloakify/keycloakify/issues/389).
|
||||
> **Login and email themes are not affected**.
|
||||
> UPDATE: [The PR](https://github.com/keycloak/keycloak/pull/22317) that should future proof Keycloakify account themes has been
|
||||
> merged into Keycloak! 🥳 Credit to @xgp. We are now waiting for a new Keycloak release to be published.
|
||||
> Login and email themes are not affected.
|
||||
|
||||
Keycloakify is fully compatible with Keycloak, starting from version 11 and is anticipated to maintain compatibility with all future versions.
|
||||
You can update your Keycloak, your Keycloakify generated theme won't break. (Well except for Keycloak 22's Account theme obviously but this was hopefully a one time debacle)
|
||||
You can update your Keycloak, your Keycloakify generated theme won't break.
|
||||
To understand the basis of my confidence in this, you can [visit this discussion thread where I've explained in detail](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791).
|
||||
|
||||
## Sponsor 👼
|
||||
@ -116,9 +114,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.gravitysoftware.be"><img src="https://avatars.githubusercontent.com/u/1140574?v=4?s=100" width="100px;" alt="Thomas Silvestre"/><br /><sub><b>Thomas Silvestre</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=thosil" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/satanshiro"><img src="https://avatars.githubusercontent.com/u/38865738?v=4?s=100" width="100px;" alt="satanshiro"/><br /><sub><b>satanshiro</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=satanshiro" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://poelhekke.dev"><img src="https://avatars.githubusercontent.com/u/1632377?v=4?s=100" width="100px;" alt="Koen Poelhekke"/><br /><sub><b>Koen Poelhekke</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kpoelhekke" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zavoloklom"><img src="https://avatars.githubusercontent.com/u/4151869?v=4?s=100" width="100px;" alt="Sergey Kupletsky"/><br /><sub><b>Sergey Kupletsky</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rome-user"><img src="https://avatars.githubusercontent.com/u/114131048?v=4?s=100" width="100px;" alt="rome-user"/><br /><sub><b>rome-user</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=rome-user" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/celinepelletier"><img src="https://avatars.githubusercontent.com/u/82821620?v=4?s=100" width="100px;" alt="Céline Pelletier"/><br /><sub><b>Céline Pelletier</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=celinepelletier" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -130,76 +125,15 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
|
||||
# Changelog highlights
|
||||
|
||||
## 8.0
|
||||
|
||||
- Much smaller .jar size. 70.2 MB -> 7.8 MB.
|
||||
Keycloakify now detects which of the static resources from the default theme are actually used by your theme and only include those in the .jar.
|
||||
- Build time: The first build is slowed but the subsequent build are faster. [Update your CI so that the cache is persisted across CI build](https://github.com/keycloakify/keycloakify-starter/commit/bc378d5afb67e796f520afbc348185f3e319d9d0).
|
||||
|
||||
### Breaking changes
|
||||
|
||||
There are very few breaking changes in this major version.
|
||||
|
||||
- The [`--external-assets` build option has been removed](https://docs.keycloakify.dev/v/v7/build-options#external-assets-deprecated) it was a performance optimization that is no longer relevant now that
|
||||
we have lazy loading.
|
||||
|
||||
- `kcContext.usernameEditDisabled` is now `kcContext.usernameHidden`, the type was lying, it has been updated to reflect what's actually on the `kcContext` at runtime.
|
||||
If you want to see in detail what should be updated [see issue](https://github.com/keycloakify/keycloakify/pull/399), or you can search and replace `usernameEditDisabled` -> `usernameHidden` it'll do the trick.
|
||||
|
||||
- The `usePrepareTemplate` prototype has been changed, you can search and replace:
|
||||
|
||||
`src/keycloak-theme/login/Template.tsx`
|
||||
|
||||
```ts
|
||||
url,
|
||||
"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"],
|
||||
```
|
||||
|
||||
by
|
||||
|
||||
```ts
|
||||
"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`
|
||||
],
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
`src/keycloak-theme/account/Template.css`
|
||||
|
||||
```ts
|
||||
url,
|
||||
"stylesCommon": ["node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css"],
|
||||
"styles": ["css/account.css"],
|
||||
```
|
||||
|
||||
by
|
||||
|
||||
```ts
|
||||
"styles": [
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
||||
`${url.resourcesPath}/css/account.css`
|
||||
],
|
||||
```
|
||||
|
||||
## 7.15
|
||||
|
||||
- The i18n messages you defines in your theme are now also maid available to Keycloak.
|
||||
In practice this mean that you can now customize the `kcContext.message.summary` that
|
||||
display a general alert and the values returned by `kcContext.messagesPerField.get()` that
|
||||
are used to display specific error on some field of the form.
|
||||
are used to display specific error on some field of the form.
|
||||
[See video](https://youtu.be/D6tZcemReTI)
|
||||
|
||||
## 7.14
|
||||
## 7.14
|
||||
|
||||
- Deprecate the `extraPages` build option. Keycloakify is now able to analyze your code to detect extra pages.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "8.4.1",
|
||||
"version": "7.15.12",
|
||||
"description": "Create Keycloak themes using React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -112,7 +112,7 @@
|
||||
"@babel/parser": "^7.22.7",
|
||||
"@babel/types": "^7.22.5",
|
||||
"@octokit/rest": "^18.12.0",
|
||||
"cheerio": "^1.0.0-rc.5",
|
||||
"cheerio": "1.0.0-rc.5",
|
||||
"cli-select": "^1.1.2",
|
||||
"evt": "^2.4.18",
|
||||
"make-fetch-happen": "^11.0.3",
|
||||
|
@ -24,9 +24,9 @@ async function main() {
|
||||
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
"projectDirPath": getProjectRoot(),
|
||||
keycloakVersion,
|
||||
"destDirPath": tmpDirPath
|
||||
"destDirPath": tmpDirPath,
|
||||
isSilent
|
||||
});
|
||||
|
||||
type Dictionary = { [idiomId: string]: string };
|
||||
|
@ -17,12 +17,10 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
|
||||
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.resourcesPath}/css/account.css`
|
||||
],
|
||||
"htmlClassName": getClassName("kcHtmlClass"),
|
||||
url,
|
||||
"stylesCommon": ["node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css"],
|
||||
"styles": ["css/account.css"],
|
||||
"htmlClassName": undefined,
|
||||
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass"))
|
||||
});
|
||||
|
||||
|
@ -11,4 +11,4 @@ export type TemplateProps<KcContext extends KcContext.Common, I18nExtended exten
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export type ClassKey = "kcHtmlClass" | "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";
|
||||
export type ClassKey = "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";
|
||||
|
@ -6,7 +6,6 @@ export type KcContext = KcContext.Password | KcContext.Account;
|
||||
|
||||
export declare namespace KcContext {
|
||||
export type Common = {
|
||||
themeVersion: string;
|
||||
keycloakifyVersion: string;
|
||||
themeType: "account";
|
||||
themeName: string;
|
||||
|
@ -7,7 +7,6 @@ import type { KcContext } from "./KcContext";
|
||||
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
|
||||
|
||||
export const kcContextCommonMock: KcContext.Common = {
|
||||
"themeVersion": "0.0.0",
|
||||
"keycloakifyVersion": "0.0.0",
|
||||
"themeType": "account",
|
||||
"themeName": "my-theme-name",
|
||||
|
@ -3,7 +3,6 @@ import type { ClassKey } from "keycloakify/account/TemplateProps";
|
||||
|
||||
export const { useGetClassName } = createUseClassName<ClassKey>({
|
||||
"defaultClasses": {
|
||||
"kcHtmlClass": undefined,
|
||||
"kcBodyClass": undefined,
|
||||
"kcButtonClass": "btn",
|
||||
"kcButtonPrimaryClass": "btn-primary",
|
||||
|
@ -51,7 +51,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
|
||||
id="username"
|
||||
name="username"
|
||||
disabled={!realm.editUsernameAllowed}
|
||||
defaultValue={account.username ?? ""}
|
||||
value={account.username ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -66,7 +66,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="text" className="form-control" id="email" name="email" autoFocus defaultValue={account.email ?? ""} />
|
||||
<input type="text" className="form-control" id="email" name="email" autoFocus value={account.email ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -79,7 +79,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="text" className="form-control" id="firstName" name="firstName" defaultValue={account.firstName ?? ""} />
|
||||
<input type="text" className="form-control" id="firstName" name="firstName" value={account.firstName ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -92,7 +92,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="text" className="form-control" id="lastName" name="lastName" defaultValue={account.lastName ?? ""} />
|
||||
<input type="text" className="form-control" id="lastName" name="lastName" value={account.lastName ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -24,11 +24,10 @@ import * as fs from "fs";
|
||||
|
||||
for (const themeType of themeTypes) {
|
||||
await downloadKeycloakStaticResources({
|
||||
projectDirPath,
|
||||
"isSilent": false,
|
||||
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
|
||||
"themeType": themeType,
|
||||
"themeDirPath": keycloakDirInPublicDir,
|
||||
"usedResources": undefined
|
||||
"themeDirPath": keycloakDirInPublicDir
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4,72 +4,19 @@ import { downloadAndUnzip } from "./tools/downloadAndUnzip";
|
||||
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
||||
import { getLogger } from "./tools/logger";
|
||||
import { readBuildOptions } from "./keycloakify/BuildOptions";
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
|
||||
export async function downloadBuiltinKeycloakTheme(params: { projectDirPath: string; keycloakVersion: string; destDirPath: string }) {
|
||||
const { projectDirPath, keycloakVersion, destDirPath } = params;
|
||||
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
|
||||
const { keycloakVersion, destDirPath } = params;
|
||||
|
||||
await downloadAndUnzip({
|
||||
"doUseCache": true,
|
||||
projectDirPath,
|
||||
destDirPath,
|
||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||
"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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(
|
||||
["", "-community"].map(ext =>
|
||||
downloadAndUnzip({
|
||||
"destDirPath": destDirPath,
|
||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@ -86,9 +33,9 @@ async function main() {
|
||||
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
"projectDirPath": process.cwd(),
|
||||
keycloakVersion,
|
||||
destDirPath
|
||||
destDirPath,
|
||||
"isSilent": buildOptions.isSilent
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { crawl } from "./tools/crawl";
|
||||
import { join as pathJoin } from "path";
|
||||
import { themeTypes } from "./keycloakify/generateFtl";
|
||||
|
||||
const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"];
|
||||
const themeSrcDirBasename = "keycloak-theme";
|
||||
|
||||
/** Can't catch error, if the directory isn't found, this function will just exit the process with an error message. */
|
||||
export function getThemeSrcDirPath(params: { projectDirPath: string }) {
|
||||
@ -14,13 +14,13 @@ export function getThemeSrcDirPath(params: { projectDirPath: string }) {
|
||||
|
||||
const themeSrcDirPath: string | undefined = crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })
|
||||
.map(fileRelativePath => {
|
||||
for (const themeSrcDirBasename of themeSrcDirBasenames) {
|
||||
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||
if (split.length === 2) {
|
||||
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||
}
|
||||
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||
|
||||
if (split.length !== 2) {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||
})
|
||||
.filter(exclude(undefined))[0];
|
||||
|
||||
@ -38,7 +38,7 @@ export function getThemeSrcDirPath(params: { projectDirPath: string }) {
|
||||
console.error(
|
||||
[
|
||||
"Can't locate your theme source directory. It should be either: ",
|
||||
"src/ or src/keycloak-theme or src/keycloak_theme.",
|
||||
"src/ or src/keycloak-theme.",
|
||||
"Example in the starter: https://github.com/keycloakify/keycloakify-starter/tree/main/src/keycloak-theme"
|
||||
].join("\n")
|
||||
);
|
||||
|
@ -10,17 +10,15 @@ import { getLogger } from "./tools/logger";
|
||||
import { getThemeSrcDirPath } from "./getSrcDirPath";
|
||||
|
||||
export async function main() {
|
||||
const projectDirPath = process.cwd();
|
||||
|
||||
const { isSilent } = readBuildOptions({
|
||||
projectDirPath,
|
||||
"projectDirPath": process.cwd(),
|
||||
"processArgv": process.argv.slice(2)
|
||||
});
|
||||
|
||||
const logger = getLogger({ isSilent });
|
||||
|
||||
const { themeSrcDirPath } = getThemeSrcDirPath({
|
||||
projectDirPath
|
||||
"projectDirPath": process.cwd()
|
||||
});
|
||||
|
||||
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
|
||||
@ -36,9 +34,9 @@ export async function main() {
|
||||
const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
projectDirPath,
|
||||
keycloakVersion,
|
||||
"destDirPath": builtinKeycloakThemeTmpDirPath
|
||||
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
||||
isSilent
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
|
@ -4,135 +4,228 @@ import { parse as urlParse } from "url";
|
||||
import { typeGuard } from "tsafe/typeGuard";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import { bundlers, getParsedPackageJson, type Bundler } from "./parsedPackageJson";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, sep as pathSep } from "path";
|
||||
import parseArgv from "minimist";
|
||||
|
||||
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
||||
export type BuildOptions = {
|
||||
isSilent: boolean;
|
||||
themeVersion: string;
|
||||
themeName: string;
|
||||
extraThemeNames: string[];
|
||||
extraThemeProperties: string[] | undefined;
|
||||
groupId: string;
|
||||
artifactId: string;
|
||||
bundler: Bundler;
|
||||
keycloakVersionDefaultAssets: string;
|
||||
/** Directory of your built react project. Defaults to {cwd}/build */
|
||||
reactAppBuildDirPath: string;
|
||||
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
|
||||
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/" */
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
|
||||
|
||||
export namespace BuildOptions {
|
||||
export type Common = {
|
||||
isSilent: boolean;
|
||||
themeVersion: string;
|
||||
themeName: string;
|
||||
extraThemeNames: string[];
|
||||
extraThemeProperties: string[] | undefined;
|
||||
groupId: string;
|
||||
artifactId: string;
|
||||
bundler: Bundler;
|
||||
keycloakVersionDefaultAssets: string;
|
||||
/** Directory of your built react project. Defaults to {cwd}/build */
|
||||
reactAppBuildDirPath: string;
|
||||
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
|
||||
keycloakifyBuildDirPath: string;
|
||||
};
|
||||
|
||||
export type Standalone = Common & {
|
||||
isStandalone: true;
|
||||
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 {
|
||||
const { projectDirPath, processArgv } = params;
|
||||
|
||||
const { isSilentCliParamProvided } = (() => {
|
||||
const { isExternalAssetsCliParamProvided, isSilentCliParamProvided } = (() => {
|
||||
const argv = parseArgv(processArgv);
|
||||
|
||||
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 { name, keycloakify = {}, version, homepage } = parsedPackageJson;
|
||||
const url = (() => {
|
||||
const { homepage } = parsedPackageJson;
|
||||
|
||||
const { extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets, extraThemeNames = [] } = keycloakify ?? {};
|
||||
let url: URL | undefined = undefined;
|
||||
|
||||
const themeName =
|
||||
keycloakify.themeName ??
|
||||
name
|
||||
.replace(/^@(.*)/, "$1")
|
||||
.split("/")
|
||||
.join("-");
|
||||
if (homepage !== undefined) {
|
||||
url = new URL(homepage);
|
||||
}
|
||||
|
||||
return {
|
||||
themeName,
|
||||
extraThemeNames,
|
||||
"bundler": (() => {
|
||||
const { KEYCLOAKIFY_BUNDLER } = process.env;
|
||||
const CNAME = (() => {
|
||||
const cnameFilePath = pathJoin(projectDirPath, "public", "CNAME");
|
||||
|
||||
assert(
|
||||
typeGuard<Bundler | undefined>(KEYCLOAKIFY_BUNDLER, [undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)),
|
||||
`${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}`
|
||||
);
|
||||
|
||||
return KEYCLOAKIFY_BUNDLER ?? bundler ?? "keycloakify";
|
||||
})(),
|
||||
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeName}-keycloak-theme`,
|
||||
"groupId": (() => {
|
||||
const fallbackGroupId = `${themeName}.keycloak`;
|
||||
|
||||
return (
|
||||
process.env.KEYCLOAKIFY_GROUP_ID ??
|
||||
groupId ??
|
||||
(!homepage
|
||||
? fallbackGroupId
|
||||
: urlParse(homepage)
|
||||
.host?.replace(/:[0-9]+$/, "")
|
||||
?.split(".")
|
||||
.reverse()
|
||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||
);
|
||||
})(),
|
||||
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
|
||||
extraThemeProperties,
|
||||
"isSilent": isSilentCliParamProvided,
|
||||
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",
|
||||
"reactAppBuildDirPath": (() => {
|
||||
let { reactAppBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
|
||||
|
||||
if (reactAppBuildDirPath === undefined) {
|
||||
return pathJoin(projectDirPath, "build");
|
||||
}
|
||||
|
||||
if (pathSep === "\\") {
|
||||
reactAppBuildDirPath = reactAppBuildDirPath.replace(/\//g, pathSep);
|
||||
}
|
||||
|
||||
if (reactAppBuildDirPath.startsWith(`.${pathSep}`)) {
|
||||
return pathJoin(projectDirPath, reactAppBuildDirPath);
|
||||
}
|
||||
|
||||
return reactAppBuildDirPath;
|
||||
})(),
|
||||
"keycloakifyBuildDirPath": (() => {
|
||||
let { keycloakifyBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
|
||||
|
||||
if (keycloakifyBuildDirPath === undefined) {
|
||||
return pathJoin(projectDirPath, "build_keycloak");
|
||||
}
|
||||
|
||||
if (pathSep === "\\") {
|
||||
keycloakifyBuildDirPath = keycloakifyBuildDirPath.replace(/\//g, pathSep);
|
||||
}
|
||||
|
||||
if (keycloakifyBuildDirPath.startsWith(`.${pathSep}`)) {
|
||||
return pathJoin(projectDirPath, keycloakifyBuildDirPath);
|
||||
}
|
||||
|
||||
return keycloakifyBuildDirPath;
|
||||
})(),
|
||||
"urlPathname": (() => {
|
||||
const { homepage } = parsedPackageJson;
|
||||
|
||||
let url: URL | undefined = undefined;
|
||||
|
||||
if (homepage !== undefined) {
|
||||
url = new URL(homepage);
|
||||
}
|
||||
|
||||
if (url === undefined) {
|
||||
if (!fs.existsSync(cnameFilePath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const out = url.pathname.replace(/([^/])$/, "$1/");
|
||||
return out === "/" ? undefined : out;
|
||||
})()
|
||||
};
|
||||
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 { extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets, extraThemeNames = [] } = keycloakify ?? {};
|
||||
|
||||
const themeName =
|
||||
keycloakify.themeName ??
|
||||
name
|
||||
.replace(/^@(.*)/, "$1")
|
||||
.split("/")
|
||||
.join("-");
|
||||
|
||||
return {
|
||||
themeName,
|
||||
extraThemeNames,
|
||||
"bundler": (() => {
|
||||
const { KEYCLOAKIFY_BUNDLER } = process.env;
|
||||
|
||||
assert(
|
||||
typeGuard<Bundler | undefined>(
|
||||
KEYCLOAKIFY_BUNDLER,
|
||||
[undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)
|
||||
),
|
||||
`${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}`
|
||||
);
|
||||
|
||||
return KEYCLOAKIFY_BUNDLER ?? bundler ?? "keycloakify";
|
||||
})(),
|
||||
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeName}-keycloak-theme`,
|
||||
"groupId": (() => {
|
||||
const fallbackGroupId = `${themeName}.keycloak`;
|
||||
|
||||
return (
|
||||
process.env.KEYCLOAKIFY_GROUP_ID ??
|
||||
groupId ??
|
||||
(!homepage
|
||||
? fallbackGroupId
|
||||
: urlParse(homepage)
|
||||
.host?.replace(/:[0-9]+$/, "")
|
||||
?.split(".")
|
||||
.reverse()
|
||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||
);
|
||||
})(),
|
||||
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
|
||||
extraThemeProperties,
|
||||
"isSilent": isSilentCliParamProvided,
|
||||
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",
|
||||
"reactAppBuildDirPath": (() => {
|
||||
let { reactAppBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
|
||||
|
||||
if (reactAppBuildDirPath === undefined) {
|
||||
return pathJoin(projectDirPath, "build");
|
||||
}
|
||||
|
||||
if (pathSep === "\\") {
|
||||
reactAppBuildDirPath = reactAppBuildDirPath.replace(/\//g, pathSep);
|
||||
}
|
||||
|
||||
if (reactAppBuildDirPath.startsWith(`.${pathSep}`)) {
|
||||
return pathJoin(projectDirPath, reactAppBuildDirPath);
|
||||
}
|
||||
|
||||
return reactAppBuildDirPath;
|
||||
})(),
|
||||
"keycloakifyBuildDirPath": (() => {
|
||||
let { keycloakifyBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
|
||||
|
||||
if (keycloakifyBuildDirPath === undefined) {
|
||||
return pathJoin(projectDirPath, "build_keycloak");
|
||||
}
|
||||
|
||||
if (pathSep === "\\") {
|
||||
keycloakifyBuildDirPath = keycloakifyBuildDirPath.replace(/\//g, pathSep);
|
||||
}
|
||||
|
||||
if (keycloakifyBuildDirPath.startsWith(`.${pathSep}`)) {
|
||||
return pathJoin(projectDirPath, keycloakifyBuildDirPath);
|
||||
}
|
||||
|
||||
return keycloakifyBuildDirPath;
|
||||
})()
|
||||
};
|
||||
})();
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
@ -471,34 +471,32 @@
|
||||
key == "identityProviderBrokerCtx" &&
|
||||
are_same_path(path, []) &&
|
||||
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
|
||||
) || (
|
||||
) || (
|
||||
"smtpConfig" == key &&
|
||||
are_same_path(path, ["realm"])
|
||||
) || (
|
||||
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
|
||||
are_same_path(path, ["realm"])
|
||||
) || (
|
||||
"error.ftl" == pageId &&
|
||||
are_same_path(path, ["realm"]) &&
|
||||
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
|
||||
) || (
|
||||
"applications.ftl" == pageId &&
|
||||
are_same_path(path, ["applications", "applications", "*", "client", "realm"])
|
||||
) || (
|
||||
"applications.ftl" == pageId &&
|
||||
"masterAdminClient" == key
|
||||
)
|
||||
>
|
||||
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#if pageId == "register.ftl" && key == "attemptedUsername" && are_same_path(path, ["auth"])>
|
||||
<#if key == "attemptedUsername" && are_same_path(path, ["auth"])>
|
||||
|
||||
<#attempt>
|
||||
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
|
||||
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
|
||||
<#continue>
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
</#if>
|
||||
|
||||
<#attempt>
|
||||
|
@ -13,11 +13,39 @@ export const themeTypes = ["login", "account"] as const;
|
||||
|
||||
export type ThemeType = (typeof themeTypes)[number];
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
themeName: string;
|
||||
themeVersion: string;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||
|
||||
export namespace BuildOptionsLike {
|
||||
export type Common = {
|
||||
themeName: string;
|
||||
themeVersion: string;
|
||||
};
|
||||
|
||||
export type Standalone = Common & {
|
||||
isStandalone: true;
|
||||
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>();
|
||||
|
||||
@ -35,23 +63,22 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
const $ = cheerio.load(indexHtmlCode);
|
||||
|
||||
fix_imports_statements: {
|
||||
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
|
||||
break fix_imports_statements;
|
||||
}
|
||||
|
||||
$("script:not([src])").each((...[, element]) => {
|
||||
const jsCode = $(element).html();
|
||||
|
||||
assert(jsCode !== null);
|
||||
|
||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({ jsCode });
|
||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||
"jsCode": $(element).html()!,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
$(element).text(fixedJsCode);
|
||||
});
|
||||
|
||||
$("style").each((...[, element]) => {
|
||||
const cssCode = $(element).html();
|
||||
|
||||
assert(cssCode !== null);
|
||||
|
||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||
cssCode,
|
||||
"cssCode": $(element).html()!,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
@ -73,7 +100,9 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
|
||||
$(element).attr(
|
||||
attrName,
|
||||
href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
|
||||
buildOptions.isStandalone
|
||||
? href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
|
||||
: href.replace(/^\//, `${buildOptions.urlOrigin}/`)
|
||||
);
|
||||
})
|
||||
);
|
||||
@ -124,27 +153,6 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
// Remove part of the document marked as ignored.
|
||||
{
|
||||
const startTags = $('meta[name="keycloakify-ignore-start"]');
|
||||
|
||||
startTags.each((...[, startTag]) => {
|
||||
const $startTag = $(startTag);
|
||||
const $endTag = $startTag.nextAll('meta[name="keycloakify-ignore-end"]').first();
|
||||
|
||||
if ($endTag.length) {
|
||||
let currentNode = $startTag.next();
|
||||
while (currentNode.length && !currentNode.is($endTag)) {
|
||||
currentNode.remove();
|
||||
currentNode = $startTag.next();
|
||||
}
|
||||
|
||||
$startTag.remove();
|
||||
$endTag.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const partiallyFixedIndexHtmlCode = $.html();
|
||||
|
||||
function generateFtlFilesCode(params: { pageId: string }): {
|
||||
|
@ -10,8 +10,6 @@ export const loginThemePageIds = [
|
||||
"login-reset-password.ftl",
|
||||
"login-verify-email.ftl",
|
||||
"terms.ftl",
|
||||
"login-oauth2-device-verify-user-code.ftl",
|
||||
"login-oauth-grant.ftl",
|
||||
"login-otp.ftl",
|
||||
"login-update-profile.ftl",
|
||||
"login-update-password.ftl",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
import type { BuildOptions } from "./BuildOptions";
|
||||
import type { ThemeType } from "./generateFtl";
|
||||
|
||||
@ -12,7 +13,11 @@ export type BuildOptionsLike = {
|
||||
themeVersion: string;
|
||||
};
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
{
|
||||
const buildOptions = Reflect<BuildOptions>();
|
||||
|
||||
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||
}
|
||||
|
||||
export function generateJavaStackFiles(params: {
|
||||
keycloakThemeBuildingDirPath: string;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
import type { BuildOptions } from "./BuildOptions";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
@ -8,7 +9,11 @@ export type BuildOptionsLike = {
|
||||
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";
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import type { ThemeType } from "../generateFtl";
|
||||
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
|
||||
import {
|
||||
@ -9,46 +9,17 @@ import {
|
||||
basenameOfKeycloakDirInPublicDir
|
||||
} from "../../mockTestingResourcesPath";
|
||||
import * as crypto from "crypto";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
export async function downloadKeycloakStaticResources(
|
||||
// prettier-ignore
|
||||
params: {
|
||||
projectDirPath: string;
|
||||
themeType: ThemeType;
|
||||
themeDirPath: string;
|
||||
isSilent: boolean;
|
||||
keycloakVersion: string;
|
||||
usedResources: {
|
||||
resourcesCommonFilePaths: string[];
|
||||
} | undefined
|
||||
}
|
||||
) {
|
||||
const { projectDirPath, themeType, themeDirPath, keycloakVersion } = params;
|
||||
|
||||
// NOTE: Hack for 427
|
||||
const usedResources = (() => {
|
||||
const { usedResources } = params;
|
||||
|
||||
if (usedResources === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
assert(usedResources !== undefined);
|
||||
|
||||
return {
|
||||
"resourcesCommonDirPaths": usedResources.resourcesCommonFilePaths.map(filePath => {
|
||||
{
|
||||
const splitArg = "/dist/";
|
||||
|
||||
if (filePath.includes(splitArg)) {
|
||||
return filePath.split(splitArg)[0] + splitArg;
|
||||
}
|
||||
}
|
||||
|
||||
return pathDirname(filePath);
|
||||
})
|
||||
};
|
||||
})();
|
||||
const { themeType, isSilent, themeDirPath, keycloakVersion } = params;
|
||||
|
||||
const tmpDirPath = pathJoin(
|
||||
themeDirPath,
|
||||
@ -57,9 +28,9 @@ export async function downloadKeycloakStaticResources(
|
||||
);
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
projectDirPath,
|
||||
keycloakVersion,
|
||||
"destDirPath": tmpDirPath
|
||||
"destDirPath": tmpDirPath,
|
||||
isSilent
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
@ -69,17 +40,7 @@ export async function downloadKeycloakStaticResources(
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
||||
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesCommonDirPathRelativeToPublicDir)),
|
||||
"transformSourceCode":
|
||||
usedResources === undefined
|
||||
? undefined
|
||||
: ({ fileRelativePath, sourceCode }) => {
|
||||
if (usedResources.resourcesCommonDirPaths.find(dirPath => fileRelativePath.startsWith(dirPath)) === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
}
|
||||
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesCommonDirPathRelativeToPublicDir))
|
||||
});
|
||||
|
||||
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
|
||||
|
@ -12,20 +12,45 @@ import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResourc
|
||||
import { readFieldNameUsage } from "./readFieldNameUsage";
|
||||
import { readExtraPagesNames } from "./readExtraPageNames";
|
||||
import { generateMessageProperties } from "./generateMessageProperties";
|
||||
import { readStaticResourcesUsage } from "./readStaticResourcesUsage";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
themeName: string;
|
||||
extraThemeProperties: string[] | undefined;
|
||||
themeVersion: string;
|
||||
keycloakVersionDefaultAssets: string;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||
|
||||
export namespace BuildOptionsLike {
|
||||
export type Common = {
|
||||
themeName: string;
|
||||
extraThemeProperties: string[] | undefined;
|
||||
isSilent: boolean;
|
||||
themeVersion: string;
|
||||
keycloakVersionDefaultAssets: string;
|
||||
};
|
||||
|
||||
export type Standalone = Common & {
|
||||
isStandalone: true;
|
||||
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>();
|
||||
|
||||
export async function generateTheme(params: {
|
||||
projectDirPath: string;
|
||||
reactAppBuildDirPath: string;
|
||||
keycloakThemeBuildingDirPath: string;
|
||||
themeSrcDirPath: string;
|
||||
@ -33,15 +58,7 @@ export async function generateTheme(params: {
|
||||
buildOptions: BuildOptionsLike;
|
||||
keycloakifyVersion: string;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
projectDirPath,
|
||||
reactAppBuildDirPath,
|
||||
keycloakThemeBuildingDirPath,
|
||||
themeSrcDirPath,
|
||||
keycloakifySrcDirPath,
|
||||
buildOptions,
|
||||
keycloakifyVersion
|
||||
} = params;
|
||||
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion } = params;
|
||||
|
||||
const getThemeDirPath = (themeType: ThemeType | "email") =>
|
||||
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
|
||||
@ -60,16 +77,17 @@ export async function generateTheme(params: {
|
||||
copy_app_resources_to_theme_path: {
|
||||
const isFirstPass = themeType.indexOf(themeType) === 0;
|
||||
|
||||
if (!isFirstPass) {
|
||||
if (!isFirstPass && !buildOptions.isStandalone) {
|
||||
break copy_app_resources_to_theme_path;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
"destDirPath": pathJoin(themeDirPath, "resources", "build"),
|
||||
"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, basenameOfKeycloakDirInPublicDir),
|
||||
filePath
|
||||
@ -79,6 +97,10 @@ export async function generateTheme(params: {
|
||||
}
|
||||
|
||||
if (/\.css?$/i.test(filePath)) {
|
||||
if (!buildOptions.isStandalone) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
|
||||
"cssCode": sourceCode.toString("utf8")
|
||||
});
|
||||
@ -98,14 +120,19 @@ export async function generateTheme(params: {
|
||||
}
|
||||
|
||||
if (/\.js?$/i.test(filePath)) {
|
||||
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||
"jsCode": sourceCode.toString("utf8")
|
||||
"jsCode": sourceCode.toString("utf8"),
|
||||
buildOptions
|
||||
});
|
||||
|
||||
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
|
||||
}
|
||||
|
||||
return { "modifiedSourceCode": sourceCode };
|
||||
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -170,11 +197,10 @@ export async function generateTheme(params: {
|
||||
}
|
||||
|
||||
await downloadKeycloakStaticResources({
|
||||
projectDirPath,
|
||||
"isSilent": buildOptions.isSilent,
|
||||
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
|
||||
"themeDirPath": keycloakDirInPublicDir,
|
||||
themeType,
|
||||
"usedResources": undefined
|
||||
themeType
|
||||
});
|
||||
|
||||
if (themeType !== themeTypes[0]) {
|
||||
@ -196,15 +222,10 @@ export async function generateTheme(params: {
|
||||
}
|
||||
|
||||
await downloadKeycloakStaticResources({
|
||||
projectDirPath,
|
||||
"isSilent": buildOptions.isSilent,
|
||||
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
|
||||
themeDirPath,
|
||||
themeType,
|
||||
"usedResources": readStaticResourcesUsage({
|
||||
keycloakifySrcDirPath,
|
||||
themeSrcDirPath,
|
||||
themeType
|
||||
})
|
||||
themeType
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
|
@ -3,6 +3,7 @@ import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
|
||||
import { join as pathJoin } from "path";
|
||||
import * as fs from "fs";
|
||||
import type { ThemeType } from "../generateFtl";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
|
||||
/** Assumes the theme type exists */
|
||||
export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): string[] {
|
||||
@ -10,7 +11,9 @@ export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; them
|
||||
|
||||
const fieldNames: string[] = [];
|
||||
|
||||
for (const srcDirPath of [pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)]) {
|
||||
for (const srcDirPath of ([pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)] as const).filter(
|
||||
exclude(undefined)
|
||||
)) {
|
||||
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
|
@ -1,76 +0,0 @@
|
||||
import { crawl } from "../../tools/crawl";
|
||||
import { join as pathJoin, sep as pathSep } 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[];
|
||||
} {
|
||||
const { keycloakifySrcDirPath, themeSrcDirPath, themeType } = params;
|
||||
|
||||
const resourcesCommonFilePaths = 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;
|
||||
}
|
||||
|
||||
const wrap = readPaths({ rawSourceFile });
|
||||
|
||||
wrap.resourcesCommonFilePaths.forEach(filePath => resourcesCommonFilePaths.add(filePath));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths)
|
||||
};
|
||||
}
|
||||
|
||||
/** Exported for testing purpose */
|
||||
export function readPaths(params: { rawSourceFile: string }): {
|
||||
resourcesCommonFilePaths: string[];
|
||||
} {
|
||||
const { rawSourceFile } = params;
|
||||
|
||||
const resourcesCommonFilePaths = new Set<string>();
|
||||
|
||||
{
|
||||
const regexp = new RegExp(`resourcesCommonPath\\s*}([^\`]+)\``, "g");
|
||||
|
||||
const matches = [...rawSourceFile.matchAll(regexp)];
|
||||
|
||||
for (const match of matches) {
|
||||
const filePath = match[1];
|
||||
|
||||
resourcesCommonFilePaths.add(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const regexp = new RegExp(`resourcesCommonPath\\s*[+,]\\s*["']([^"'\`]+)["'\`]`, "g");
|
||||
|
||||
const matches = [...rawSourceFile.matchAll(regexp)];
|
||||
|
||||
for (const match of matches) {
|
||||
const filePath = match[1];
|
||||
|
||||
resourcesCommonFilePaths.add(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
const normalizePath = (filePath: string) => {
|
||||
filePath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
||||
filePath = filePath.replace(/\//g, pathSep);
|
||||
return filePath;
|
||||
};
|
||||
|
||||
return {
|
||||
"resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths).map(normalizePath)
|
||||
};
|
||||
}
|
@ -30,7 +30,6 @@ export async function main() {
|
||||
|
||||
for (const themeName of [buildOptions.themeName, ...buildOptions.extraThemeNames]) {
|
||||
await generateTheme({
|
||||
projectDirPath,
|
||||
"keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath,
|
||||
themeSrcDirPath,
|
||||
"keycloakifySrcDirPath": pathJoin(keycloakifyDirPath, "src"),
|
||||
|
@ -1,6 +1,31 @@
|
||||
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 function replaceImportsFromStaticInJsCode(params: { jsCode: string }): { fixedJsCode: string } {
|
||||
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||
|
||||
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:
|
||||
|
||||
@ -13,38 +38,48 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string }): {
|
||||
will always run in keycloak context.
|
||||
*/
|
||||
|
||||
const { jsCode } = params;
|
||||
const { jsCode, buildOptions } = params;
|
||||
|
||||
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
|
||||
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"static\\/${language}\\/"`, "g"),
|
||||
(...[, n, u, matchedFunction, eForFunction]) => {
|
||||
const isArrowFunction = matchedFunction.includes("=>");
|
||||
const e = isArrowFunction ? matchedFunction.replace("=>", "").trim() : eForFunction;
|
||||
|
||||
return `
|
||||
${n}[(function(){
|
||||
var pd = Object.getOwnPropertyDescriptor(${n}, "p");
|
||||
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
|
||||
(...[, n, u, e]) => `
|
||||
${n}[(function(){
|
||||
var pd= Object.getOwnPropertyDescriptor(${n}, "p");
|
||||
if( pd === undefined || pd.configurable ){
|
||||
${
|
||||
buildOptions.isStandalone
|
||||
? `
|
||||
Object.defineProperty(${n}, "p", {
|
||||
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
|
||||
set: function (){}
|
||||
});
|
||||
`
|
||||
: `
|
||||
var p= "";
|
||||
Object.defineProperty(${n}, "p", {
|
||||
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
|
||||
set: function() {}
|
||||
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
|
||||
set: function (value){ p = value;}
|
||||
});
|
||||
`
|
||||
}
|
||||
return "${u}";
|
||||
})()] = ${isArrowFunction ? `${e} =>` : `function(${e}) { return `} "/build/static/${language}/"`
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
return "${u}";
|
||||
})()] = function(${e}) { return "${buildOptions.isStandalone ? "/build/" : ""}static/${language}/"`
|
||||
];
|
||||
|
||||
const fixedJsCode = jsCode
|
||||
.replace(...getReplaceArgs("js"))
|
||||
.replace(...getReplaceArgs("css"))
|
||||
.replace(/[a-zA-Z]+\.[a-zA-Z]+\+"static\//g, `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`)
|
||||
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
|
||||
buildOptions.isStandalone
|
||||
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
|
||||
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group}) + "static/`
|
||||
)
|
||||
//TODO: Write a test case for this
|
||||
.replace(
|
||||
/".chunk.css",([a-zA-Z])+=[a-zA-Z]+\.[a-zA-Z]+\+([a-zA-Z]+),/,
|
||||
(...[, group1, group2]) => `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group2},`
|
||||
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
|
||||
buildOptions.isStandalone
|
||||
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
|
||||
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group2}) + ${group3},`
|
||||
);
|
||||
|
||||
return { fixedJsCode };
|
||||
|
@ -1,12 +1,20 @@
|
||||
import * as crypto from "crypto";
|
||||
import type { BuildOptions } from "../BuildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
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 }): {
|
||||
fixedCssCode: string;
|
||||
|
@ -1,11 +1,32 @@
|
||||
import type { BuildOptions } from "../BuildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
export namespace BuildOptionsLike {
|
||||
export type Common = {
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
|
||||
export type Standalone = Common & {
|
||||
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 }): {
|
||||
fixedCssCode: string;
|
||||
@ -16,7 +37,10 @@ export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOp
|
||||
buildOptions.urlPathname === undefined
|
||||
? /url\(["']?\/([^/][^)"']+)["']?\)/g
|
||||
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
|
||||
(...[, group]) => `url(\${url.resourcesPath}/build/${group})`
|
||||
(...[, group]) =>
|
||||
`url(${
|
||||
buildOptions.isStandalone ? "${url.resourcesPath}/build/" + group : buildOptions.urlOrigin + (buildOptions.urlPathname ?? "/") + group
|
||||
})`
|
||||
);
|
||||
|
||||
return { fixedCssCode };
|
||||
|
@ -17,7 +17,7 @@ export async function promptKeycloakVersion() {
|
||||
return { getLatestsSemVersionedTag };
|
||||
})();
|
||||
|
||||
console.log("Select Keycloak version?");
|
||||
console.log("Initialize the directory with email template from which keycloak version?");
|
||||
|
||||
const tags = [
|
||||
...(await getLatestsSemVersionedTag({
|
||||
|
@ -1,55 +1,18 @@
|
||||
import { exec as execCallback } from "child_process";
|
||||
import { createHash } from "crypto";
|
||||
import { mkdir, readFile, stat, writeFile, unlink, rm } from "fs/promises";
|
||||
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
||||
import fetch, { type FetchOptions } from "make-fetch-happen";
|
||||
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { promisify } from "util";
|
||||
import { getProjectRoot } from "./getProjectRoot";
|
||||
import { transformCodebase } from "./transformCodebase";
|
||||
import { unzip, zip } from "./unzip";
|
||||
import { unzip } from "./unzip";
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
|
||||
function generateFileNameFromURL(params: {
|
||||
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;
|
||||
function hash(s: string) {
|
||||
return createHash("sha256").update(s).digest("hex");
|
||||
}
|
||||
|
||||
async function exists(path: string) {
|
||||
@ -150,43 +113,14 @@ async function getFetchOptions(): Promise<Pick<FetchOptions, "proxy" | "noProxy"
|
||||
return { proxy, noProxy, strictSSL, cert, ca: ca.length === 0 ? undefined : ca };
|
||||
}
|
||||
|
||||
export async function downloadAndUnzip(
|
||||
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;
|
||||
export async function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string }) {
|
||||
const { url, destDirPath, pathOfDirToExtractInArchive } = params;
|
||||
|
||||
const zipFileBasename = generateFileNameFromURL({
|
||||
url,
|
||||
"preCacheTransform":
|
||||
preCacheTransform === undefined
|
||||
? 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}`);
|
||||
const downloadHash = hash(JSON.stringify({ url })).substring(0, 15);
|
||||
const projectRoot = getProjectRoot();
|
||||
const cacheRoot = process.env.XDG_CACHE_HOME ?? pathJoin(projectRoot, "node_modules", ".cache");
|
||||
const zipFilePath = pathJoin(cacheRoot, "keycloakify", "zip", `_${downloadHash}.zip`);
|
||||
const extractDirPath = pathJoin(cacheRoot, "keycloakify", "unzip", `_${downloadHash}`);
|
||||
|
||||
if (!(await exists(zipFilePath))) {
|
||||
const opts = await getFetchOptions();
|
||||
@ -202,32 +136,12 @@ export async function downloadAndUnzip(
|
||||
response.body?.setMaxListeners(Number.MAX_VALUE);
|
||||
assert(typeof response.body !== "undefined" && response.body != null);
|
||||
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);
|
||||
await unzip(zipFilePath, extractDirPath, pathOfDirToExtractInArchive);
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": extractDirPath,
|
||||
"destDirPath": destDirPath
|
||||
});
|
||||
|
||||
if (!rest.doUseCache) {
|
||||
await rm(cacheRoot, { "recursive": true });
|
||||
} else {
|
||||
await rm(extractDirPath, { "recursive": true });
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import * as path from "path";
|
||||
import { crawl } from "./crawl";
|
||||
import { id } from "tsafe/id";
|
||||
|
||||
type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; fileRelativePath: string }) =>
|
||||
type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string }) =>
|
||||
| {
|
||||
modifiedSourceCode: Buffer;
|
||||
newFileName?: string;
|
||||
@ -20,27 +20,26 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
|
||||
}))
|
||||
} = params;
|
||||
|
||||
for (const fileRelativePath of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) {
|
||||
const filePath = path.join(srcDirPath, fileRelativePath);
|
||||
for (const file_relative_path of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) {
|
||||
const filePath = path.join(srcDirPath, file_relative_path);
|
||||
|
||||
const transformSourceCodeResult = transformSourceCode({
|
||||
"sourceCode": fs.readFileSync(filePath),
|
||||
filePath,
|
||||
fileRelativePath
|
||||
filePath
|
||||
});
|
||||
|
||||
if (transformSourceCodeResult === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(path.join(destDirPath, fileRelativePath)), {
|
||||
fs.mkdirSync(path.dirname(path.join(destDirPath, file_relative_path)), {
|
||||
"recursive": true
|
||||
});
|
||||
|
||||
const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(path.dirname(path.join(destDirPath, fileRelativePath)), newFileName ?? path.basename(fileRelativePath)),
|
||||
path.join(path.dirname(path.join(destDirPath, file_relative_path)), newFileName ?? path.basename(file_relative_path)),
|
||||
modifiedSourceCode
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import fsp from "node:fs/promises";
|
||||
import fs from "fs";
|
||||
import path from "node:path";
|
||||
import yauzl from "yauzl";
|
||||
import yazl from "yazl";
|
||||
import stream from "node:stream";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
@ -20,16 +19,11 @@ async function pathExists(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handlings of non posix path is not implemented correctly
|
||||
// it work by coincidence. Don't have the time to fix but it should be fixed.
|
||||
export async function unzip(file: string, targetFolder: string, specificDirsToExtract?: string[]) {
|
||||
specificDirsToExtract = specificDirsToExtract?.map(dirPath => {
|
||||
if (!dirPath.endsWith("/") || !dirPath.endsWith("\\")) {
|
||||
dirPath += "/";
|
||||
}
|
||||
|
||||
return dirPath;
|
||||
});
|
||||
export async function unzip(file: string, targetFolder: string, unzipSubPath?: string) {
|
||||
// add trailing slash to unzipSubPath and targetFolder
|
||||
if (unzipSubPath && (!unzipSubPath.endsWith("/") || !unzipSubPath.endsWith("\\"))) {
|
||||
unzipSubPath += "/";
|
||||
}
|
||||
|
||||
if (!targetFolder.endsWith("/") || !targetFolder.endsWith("\\")) {
|
||||
targetFolder += "/";
|
||||
@ -48,17 +42,15 @@ export async function unzip(file: string, targetFolder: string, specificDirsToEx
|
||||
zipfile.readEntry();
|
||||
|
||||
zipfile.on("entry", async entry => {
|
||||
if (specificDirsToExtract !== undefined) {
|
||||
const dirPath = specificDirsToExtract.find(dirPath => entry.fileName.startsWith(dirPath));
|
||||
|
||||
if (unzipSubPath) {
|
||||
// Skip files outside of the unzipSubPath
|
||||
if (dirPath === undefined) {
|
||||
if (!entry.fileName.startsWith(unzipSubPath)) {
|
||||
zipfile.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the unzipSubPath from the file name
|
||||
entry.fileName = entry.fileName.substring(dirPath.length);
|
||||
entry.fileName = entry.fileName.substring(unzipSubPath.length);
|
||||
}
|
||||
|
||||
const target = path.join(targetFolder, entry.fileName);
|
||||
@ -85,8 +77,6 @@ export async function unzip(file: string, targetFolder: string, specificDirsToEx
|
||||
return;
|
||||
}
|
||||
|
||||
await fsp.mkdir(path.dirname(target), { "recursive": true });
|
||||
|
||||
await pipeline(readStream, fs.createWriteStream(target));
|
||||
|
||||
zipfile.readEntry();
|
||||
@ -100,42 +90,3 @@ export async function unzip(file: string, targetFolder: string, specificDirsToEx
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
|
@ -1,15 +1,21 @@
|
||||
import { useReducer, useEffect } from "react";
|
||||
import { headInsert } from "keycloakify/tools/headInsert";
|
||||
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
|
||||
export function usePrepareTemplate(params: {
|
||||
doFetchDefaultThemeResources: boolean;
|
||||
stylesCommon?: string[];
|
||||
styles?: string[];
|
||||
scripts?: string[];
|
||||
url: {
|
||||
resourcesCommonPath: string;
|
||||
resourcesPath: string;
|
||||
};
|
||||
htmlClassName: string | undefined;
|
||||
bodyClassName: string | undefined;
|
||||
}) {
|
||||
const { doFetchDefaultThemeResources, styles = [], scripts = [], htmlClassName, bodyClassName } = params;
|
||||
const { doFetchDefaultThemeResources, stylesCommon = [], styles = [], url, scripts = [], htmlClassName, bodyClassName } = params;
|
||||
|
||||
const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources);
|
||||
|
||||
@ -23,30 +29,38 @@ export function usePrepareTemplate(params: {
|
||||
const removeArray: (() => void)[] = [];
|
||||
|
||||
(async () => {
|
||||
for (const style of [...styles].reverse()) {
|
||||
const { prLoaded, remove } = headInsert({
|
||||
"type": "css",
|
||||
"position": "prepend",
|
||||
"href": style
|
||||
const prLoadedArray: Promise<void>[] = [];
|
||||
|
||||
[
|
||||
...stylesCommon.map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
|
||||
...styles.map(relativePath => pathJoin(url.resourcesPath, relativePath))
|
||||
]
|
||||
.reverse()
|
||||
.forEach(href => {
|
||||
const { prLoaded, remove } = headInsert({
|
||||
"type": "css",
|
||||
"position": "prepend",
|
||||
href
|
||||
});
|
||||
|
||||
removeArray.push(remove);
|
||||
|
||||
prLoadedArray.push(prLoaded);
|
||||
});
|
||||
|
||||
removeArray.push(remove);
|
||||
await Promise.all(prLoadedArray);
|
||||
|
||||
// TODO: Find a way to do that in parallel (without breaking the order)
|
||||
await prLoaded;
|
||||
|
||||
if (isUnmounted) {
|
||||
return;
|
||||
}
|
||||
if (isUnmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReady();
|
||||
})();
|
||||
|
||||
scripts.forEach(src => {
|
||||
scripts.forEach(relativePath => {
|
||||
const { remove } = headInsert({
|
||||
"type": "javascript",
|
||||
src
|
||||
"src": pathJoin(url.resourcesPath, relativePath)
|
||||
});
|
||||
|
||||
removeArray.push(remove);
|
||||
|
@ -12,8 +12,6 @@ const Error = lazy(() => import("keycloakify/login/pages/Error"));
|
||||
const LoginResetPassword = lazy(() => import("keycloakify/login/pages/LoginResetPassword"));
|
||||
const LoginVerifyEmail = lazy(() => import("keycloakify/login/pages/LoginVerifyEmail"));
|
||||
const Terms = lazy(() => import("keycloakify/login/pages/Terms"));
|
||||
const LoginDeviceVerifyUserCode = lazy(() => import("keycloakify/login/pages/LoginDeviceVerifyUserCode"));
|
||||
const LoginOauthGrant = lazy(() => import("keycloakify/login/pages/LoginOauthGrant"));
|
||||
const LoginOtp = lazy(() => import("keycloakify/login/pages/LoginOtp"));
|
||||
const LoginPassword = lazy(() => import("keycloakify/login/pages/LoginPassword"));
|
||||
const LoginUsername = lazy(() => import("keycloakify/login/pages/LoginUsername"));
|
||||
@ -54,10 +52,6 @@ export default function Fallback(props: PageProps<KcContext, I18n>) {
|
||||
return <LoginVerifyEmail kcContext={kcContext} {...rest} />;
|
||||
case "terms.ftl":
|
||||
return <Terms kcContext={kcContext} {...rest} />;
|
||||
case "login-oauth2-device-verify-user-code.ftl":
|
||||
return <LoginDeviceVerifyUserCode kcContext={kcContext} {...rest} />;
|
||||
case "login-oauth-grant.ftl":
|
||||
return <LoginOauthGrant kcContext={kcContext} {...rest} />;
|
||||
case "login-otp.ftl":
|
||||
return <LoginOtp kcContext={kcContext} {...rest} />;
|
||||
case "login-username.ftl":
|
||||
|
@ -31,14 +31,15 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
|
||||
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`
|
||||
url,
|
||||
"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"),
|
||||
"bodyClassName": getClassName("kcBodyClass")
|
||||
"bodyClassName": undefined
|
||||
});
|
||||
|
||||
if (!isReady) {
|
||||
|
@ -21,7 +21,6 @@ export type TemplateProps<KcContext extends KcContext.Common, I18nExtended exten
|
||||
};
|
||||
|
||||
export type ClassKey =
|
||||
| "kcBodyClass"
|
||||
| "kcHtmlClass"
|
||||
| "kcLoginClass"
|
||||
| "kcHeaderClass"
|
||||
@ -94,5 +93,4 @@ export type ClassKey =
|
||||
| "kcSelectOTPListItemClass"
|
||||
| "kcAuthenticatorOtpCircleClass"
|
||||
| "kcSelectOTPItemHeadingClass"
|
||||
| "kcFormOptionsWrapperClass"
|
||||
| "kcFormButtonsWrapperClass";
|
||||
| "kcFormOptionsWrapperClass";
|
||||
|
@ -18,8 +18,6 @@ export type KcContext =
|
||||
| KcContext.LoginResetPassword
|
||||
| KcContext.LoginVerifyEmail
|
||||
| KcContext.Terms
|
||||
| KcContext.LoginDeviceVerifyUserCode
|
||||
| KcContext.LoginOauthGrant
|
||||
| KcContext.LoginOtp
|
||||
| KcContext.LoginUsername
|
||||
| KcContext.WebauthnAuthenticate
|
||||
@ -39,7 +37,6 @@ export type KcContext =
|
||||
|
||||
export declare namespace KcContext {
|
||||
export type Common = {
|
||||
themeVersion: string;
|
||||
keycloakifyVersion: string;
|
||||
themeType: "login";
|
||||
themeName: string;
|
||||
@ -244,27 +241,6 @@ export declare namespace KcContext {
|
||||
pageId: "terms.ftl";
|
||||
};
|
||||
|
||||
export type LoginDeviceVerifyUserCode = Common & {
|
||||
pageId: "login-oauth2-device-verify-user-code.ftl";
|
||||
url: {
|
||||
oauth2DeviceVerificationAction: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type LoginOauthGrant = Common & {
|
||||
pageId: "login-oauth-grant.ftl";
|
||||
oauth: {
|
||||
code: string;
|
||||
client: string;
|
||||
clientScopesRequested: {
|
||||
consentScreenText: string;
|
||||
}[];
|
||||
};
|
||||
url: {
|
||||
oauthAction: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type LoginOtp = Common & {
|
||||
pageId: "login-otp.ftl";
|
||||
otpLogin: {
|
||||
|
@ -103,7 +103,6 @@ const attributes: Attribute[] = [
|
||||
const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any;
|
||||
|
||||
export const kcContextCommonMock: KcContext.Common = {
|
||||
"themeVersion": "0.0.0",
|
||||
"keycloakifyVersion": "0.0.0",
|
||||
"themeType": "login",
|
||||
"themeName": "my-theme-name",
|
||||
@ -241,9 +240,7 @@ export const kcContextCommonMock: KcContext.Common = {
|
||||
const loginUrl = {
|
||||
...kcContextCommonMock.url,
|
||||
"loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
|
||||
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg",
|
||||
"oauth2DeviceVerificationAction": "/auth/realms/myrealm/device",
|
||||
"oauthAction": "/auth/realms/myrealm/login-actions/consent?client_id=account&tab_id=HoAx28ja4xg"
|
||||
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg"
|
||||
};
|
||||
|
||||
export const kcContextMocks = [
|
||||
@ -347,25 +344,6 @@ export const kcContextMocks = [
|
||||
...kcContextCommonMock,
|
||||
"pageId": "terms.ftl"
|
||||
}),
|
||||
id<KcContext.LoginDeviceVerifyUserCode>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-oauth2-device-verify-user-code.ftl",
|
||||
url: loginUrl
|
||||
}),
|
||||
id<KcContext.LoginOauthGrant>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-oauth-grant.ftl",
|
||||
oauth: {
|
||||
code: "5-1N4CIzfi1aprIQjmylI-9e3spLCWW9i5d-GDcs-Sw",
|
||||
clientScopesRequested: [
|
||||
{ consentScreenText: "${profileScopeConsentText}" },
|
||||
{ consentScreenText: "${rolesScopeConsentText}" },
|
||||
{ consentScreenText: "${emailScopeConsentText}" }
|
||||
],
|
||||
client: "account"
|
||||
},
|
||||
url: loginUrl
|
||||
}),
|
||||
id<KcContext.LoginOtp>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-otp.ftl",
|
||||
|
@ -3,7 +3,6 @@ import type { ClassKey } from "keycloakify/login/TemplateProps";
|
||||
|
||||
export const { useGetClassName } = createUseClassName<ClassKey>({
|
||||
"defaultClasses": {
|
||||
"kcBodyClass": undefined,
|
||||
"kcHtmlClass": "login-pf",
|
||||
"kcLoginClass": "login-pf-page",
|
||||
"kcContentWrapperClass": "row",
|
||||
@ -45,7 +44,6 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
|
||||
"kcInputClass": "form-control",
|
||||
"kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text",
|
||||
"kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
"kcFormButtonsWrapperClass": undefined,
|
||||
"kcFormOptionsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
"kcFormButtonsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
"kcFormSettingClass": "login-pf-settings",
|
||||
|
@ -8,10 +8,7 @@ export default function Info(props: PageProps<Extract<KcContext, { pageId: "info
|
||||
|
||||
const { msgStr, msg } = i18n;
|
||||
|
||||
assert(
|
||||
kcContext.message !== undefined,
|
||||
"No message in kcContext.message, there will always be a message in production context, add it in your mock"
|
||||
);
|
||||
assert(kcContext.message !== undefined);
|
||||
|
||||
const { messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client } = kcContext;
|
||||
|
||||
|
@ -32,7 +32,7 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
|
||||
|
||||
<ul id="kc-totp-supported-apps">
|
||||
{totp.supportedApplications.map(app => (
|
||||
<li>{msg(app as MessageKey)}</li>
|
||||
<li>{msgStr(app as MessageKey, app)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
|
@ -1,68 +0,0 @@
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import Template from "../Template";
|
||||
import { I18n } from "../i18n";
|
||||
import { KcContext } from "../kcContext";
|
||||
import { useGetClassName } from "../lib/useGetClassName";
|
||||
import { PageProps } from "./PageProps";
|
||||
|
||||
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth2-device-verify-user-code.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, classes } = props;
|
||||
const { url } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("oauth2DeviceVerificationTitle")}>
|
||||
<form
|
||||
id="kc-user-verify-device-user-code-form"
|
||||
className={getClassName("kcFormClass")}
|
||||
action={url.oauth2DeviceVerificationAction}
|
||||
method="post"
|
||||
>
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="device-user-code" className={getClassName("kcLabelClass")}>
|
||||
{msg("verifyOAuth2DeviceUserCode")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
id="device-user-code"
|
||||
name="device_user_code"
|
||||
autoComplete="off"
|
||||
type="text"
|
||||
className={getClassName("kcInputClass")}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
|
||||
<div className={getClassName("kcFormOptionsWrapperClass")}></div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
|
||||
<div className={getClassName("kcFormButtonsWrapperClass")}>
|
||||
<input
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
type="submit"
|
||||
value={msgStr("doSubmit")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { PageProps } from "./PageProps";
|
||||
import { KcContext } from "../kcContext";
|
||||
import { I18n } from "../i18n";
|
||||
import Template from "../Template";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
|
||||
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth-grant.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, classes } = props;
|
||||
const { url, oauth, client } = kcContext;
|
||||
|
||||
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
||||
headerNode={msg("oauthGrantTitle", client.name ? advancedMsgStr(client.name) : client.clientId)}
|
||||
>
|
||||
<div id="kc-oauth" className="content-area">
|
||||
<h3>{msg("oauthGrantRequest")}</h3>
|
||||
<ul>
|
||||
{oauth.clientScopesRequested.map(clientScope => (
|
||||
<li key={clientScope.consentScreenText}>
|
||||
<span>{advancedMsg(clientScope.consentScreenText)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<form className="form-actions" action={url.oauthAction} method="POST">
|
||||
<input type="hidden" name="code" value={oauth.code} />
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div id="kc-form-options">
|
||||
<div className={getClassName("kcFormOptionsWrapperClass")}></div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons">
|
||||
<div className={getClassName("kcFormButtonsWrapperClass")}>
|
||||
<input
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
name="accept"
|
||||
id="kc-login"
|
||||
type="submit"
|
||||
value={msgStr("doYes")}
|
||||
/>
|
||||
<input
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonDefaultClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
name="cancel"
|
||||
id="kc-cancel"
|
||||
type="submit"
|
||||
value={msgStr("doNo")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="clearfix"></div>
|
||||
</div>
|
||||
</Template>
|
||||
);
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { headInsert } from "keycloakify/tools/headInsert";
|
||||
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
@ -23,7 +24,7 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
|
||||
|
||||
const { prLoaded, remove } = headInsert({
|
||||
"type": "javascript",
|
||||
"src": `${kcContext.url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js`
|
||||
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js")
|
||||
});
|
||||
|
||||
(async () => {
|
||||
|
@ -21,13 +21,4 @@ const meta: ComponentMeta<any> = {
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default = () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "This is the server message",
|
||||
type: "info"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
export const Default = () => <PageStory />;
|
||||
|
@ -1,24 +0,0 @@
|
||||
import React from "react";
|
||||
import type { ComponentMeta } from "@storybook/react";
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const pageId = "login-oauth2-device-verify-user-code.ftl";
|
||||
|
||||
const { PageStory } = createPageStory({ pageId });
|
||||
|
||||
const meta: ComponentMeta<any> = {
|
||||
title: `login/${pageId}`,
|
||||
component: PageStory,
|
||||
parameters: {
|
||||
viewMode: "story",
|
||||
previewTabs: {
|
||||
"storybook/docs/panel": {
|
||||
"hidden": true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default = () => <PageStory />;
|
@ -1,24 +0,0 @@
|
||||
import React from "react";
|
||||
import type { ComponentMeta } from "@storybook/react";
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const pageId = "login-oauth-grant.ftl";
|
||||
|
||||
const { PageStory } = createPageStory({ pageId });
|
||||
|
||||
const meta: ComponentMeta<any> = {
|
||||
title: `login/${pageId}`,
|
||||
component: PageStory,
|
||||
parameters: {
|
||||
viewMode: "story",
|
||||
previewTabs: {
|
||||
"storybook/docs/panel": {
|
||||
"hidden": true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default = () => <PageStory />;
|
@ -1,107 +0,0 @@
|
||||
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"
|
||||
]
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
@ -32,14 +32,13 @@ describe("bin/js-transforms", () => {
|
||||
908:"67c9ed2c"
|
||||
}[e]+".chunk.css"
|
||||
}
|
||||
|
||||
n.u=e=>"static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js"
|
||||
|
||||
t.miniCssF=e=>"static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
|
||||
`;
|
||||
it("transforms standalone code properly", () => {
|
||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||
"jsCode": jsCodeUntransformed
|
||||
"jsCode": jsCodeUntransformed,
|
||||
"buildOptions": {
|
||||
"isStandalone": true
|
||||
}
|
||||
});
|
||||
|
||||
const fixedJsCodeExpected = `
|
||||
@ -56,11 +55,11 @@ describe("bin/js-transforms", () => {
|
||||
}
|
||||
|
||||
__webpack_require__[(function (){
|
||||
var pd = Object.getOwnPropertyDescriptor(__webpack_require__, "p");
|
||||
var pd= Object.getOwnPropertyDescriptor(__webpack_require__, "p");
|
||||
if( pd === undefined || pd.configurable ){
|
||||
Object.defineProperty(__webpack_require__, "p", {
|
||||
get: function() { return window.kcContext.url.resourcesPath; },
|
||||
set: function() {}
|
||||
set: function (){}
|
||||
});
|
||||
}
|
||||
return "u";
|
||||
@ -73,11 +72,11 @@ describe("bin/js-transforms", () => {
|
||||
}
|
||||
|
||||
t[(function (){
|
||||
var pd = Object.getOwnPropertyDescriptor(t, "p");
|
||||
var pd= Object.getOwnPropertyDescriptor(t, "p");
|
||||
if( pd === undefined || pd.configurable ){
|
||||
Object.defineProperty(t, "p", {
|
||||
get: function() { return window.kcContext.url.resourcesPath; },
|
||||
set: function() {}
|
||||
set: function (){}
|
||||
});
|
||||
}
|
||||
return "miniCssF";
|
||||
@ -87,28 +86,67 @@ describe("bin/js-transforms", () => {
|
||||
908:"67c9ed2c"
|
||||
} [e] + ".chunk.css"
|
||||
}
|
||||
|
||||
n[(function(){
|
||||
var pd = Object.getOwnPropertyDescriptor(n, "p");
|
||||
|
||||
`;
|
||||
|
||||
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 ){
|
||||
Object.defineProperty(n, "p", {
|
||||
get: function() { return window.kcContext.url.resourcesPath; },
|
||||
set: function() {}
|
||||
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";
|
||||
})()] = e => "/build/static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js"
|
||||
|
||||
t[(function(){
|
||||
var pd = Object.getOwnPropertyDescriptor(t, "p");
|
||||
})()] = 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 window.kcContext.url.resourcesPath; },
|
||||
set: function() {}
|
||||
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev/" : p; },
|
||||
set: function (value){ p = value; }
|
||||
});
|
||||
}
|
||||
return "miniCssF";
|
||||
})()] = e => "/build/static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
|
||||
})()] = function(e) {
|
||||
return "static/css/" + e + "." + {
|
||||
164:"dcfd7749",
|
||||
908:"67c9ed2c"
|
||||
} [e] + ".chunk.css"
|
||||
}
|
||||
`;
|
||||
|
||||
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
|
||||
@ -266,6 +304,7 @@ describe("bin/css-inline-transforms", () => {
|
||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||
cssCode,
|
||||
"buildOptions": {
|
||||
"isStandalone": true,
|
||||
"urlPathname": undefined
|
||||
}
|
||||
});
|
||||
@ -305,6 +344,53 @@ 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);
|
||||
});
|
||||
});
|
||||
@ -344,6 +430,7 @@ describe("bin/css-inline-transforms", () => {
|
||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||
cssCode,
|
||||
"buildOptions": {
|
||||
"isStandalone": true,
|
||||
"urlPathname": "/x/y/z/"
|
||||
}
|
||||
});
|
||||
@ -383,6 +470,53 @@ 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);
|
||||
});
|
||||
});
|
||||
|
@ -12,8 +12,7 @@ export const sampleReactProjectDirPath = pathJoin(getProjectRoot(), "sample_reac
|
||||
async function setupSampleReactProject(destDir: string) {
|
||||
await downloadAndUnzip({
|
||||
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
|
||||
"destDirPath": destDir,
|
||||
"doUseCache": false
|
||||
"destDirPath": destDir
|
||||
});
|
||||
}
|
||||
let parsedPackageJson: Record<string, unknown> = {};
|
||||
@ -52,19 +51,17 @@ describe("Sample Project", () => {
|
||||
await setupSampleReactProject(sampleReactProjectDirPath);
|
||||
await initializeEmailTheme();
|
||||
|
||||
const projectDirPath = process.cwd();
|
||||
|
||||
const destDirPath = pathJoin(
|
||||
readBuildOptions({
|
||||
"processArgv": ["--silent"],
|
||||
projectDirPath
|
||||
"projectDirPath": process.cwd()
|
||||
}).keycloakifyBuildDirPath,
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"theme"
|
||||
);
|
||||
await downloadBuiltinKeycloakTheme({ destDirPath, "keycloakVersion": "11.0.3", projectDirPath });
|
||||
await downloadBuiltinKeycloakTheme({ destDirPath, keycloakVersion: "11.0.3", "isSilent": false });
|
||||
},
|
||||
{ timeout: 90000 }
|
||||
);
|
||||
@ -80,19 +77,17 @@ describe("Sample Project", () => {
|
||||
await setupSampleReactProject(pathJoin(sampleReactProjectDirPath, "custom_input"));
|
||||
await initializeEmailTheme();
|
||||
|
||||
const projectDirPath = process.cwd();
|
||||
|
||||
const destDirPath = pathJoin(
|
||||
readBuildOptions({
|
||||
"processArgv": ["--silent"],
|
||||
projectDirPath
|
||||
"projectDirPath": process.cwd()
|
||||
}).keycloakifyBuildDirPath,
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"theme"
|
||||
);
|
||||
await downloadBuiltinKeycloakTheme({ destDirPath, "keycloakVersion": "11.0.3", projectDirPath });
|
||||
await downloadBuiltinKeycloakTheme({ destDirPath, "keycloakVersion": "11.0.3", "isSilent": false });
|
||||
},
|
||||
{ timeout: 90000 }
|
||||
);
|
||||
|
@ -3,7 +3,6 @@ import { downloadAndUnzip } from "keycloakify/bin/tools/downloadAndUnzip";
|
||||
export async function setupSampleReactProject(destDirPath: string) {
|
||||
await downloadAndUnzip({
|
||||
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
|
||||
"destDirPath": destDirPath,
|
||||
"doUseCache": false
|
||||
"destDirPath": destDirPath
|
||||
});
|
||||
}
|
||||
|
112
yarn.lock
112
yarn.lock
@ -4559,30 +4559,29 @@ check-error@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
|
||||
integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==
|
||||
|
||||
cheerio-select@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4"
|
||||
integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==
|
||||
cheerio-select-tmp@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646"
|
||||
integrity sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
css-select "^5.1.0"
|
||||
css-what "^6.1.0"
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.0.1"
|
||||
css-select "^3.1.2"
|
||||
css-what "^4.0.0"
|
||||
domelementtype "^2.1.0"
|
||||
domhandler "^4.0.0"
|
||||
domutils "^2.4.4"
|
||||
|
||||
cheerio@^1.0.0-rc.5:
|
||||
version "1.0.0-rc.12"
|
||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683"
|
||||
integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==
|
||||
cheerio@1.0.0-rc.5:
|
||||
version "1.0.0-rc.5"
|
||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.5.tgz#88907e1828674e8f9fee375188b27dadd4f0fa2f"
|
||||
integrity sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw==
|
||||
dependencies:
|
||||
cheerio-select "^2.1.0"
|
||||
dom-serializer "^2.0.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.0.1"
|
||||
htmlparser2 "^8.0.1"
|
||||
parse5 "^7.0.0"
|
||||
parse5-htmlparser2-tree-adapter "^7.0.0"
|
||||
cheerio-select-tmp "^0.1.0"
|
||||
dom-serializer "~1.2.0"
|
||||
domhandler "^4.0.0"
|
||||
entities "~2.1.0"
|
||||
htmlparser2 "^6.0.0"
|
||||
parse5 "^6.0.0"
|
||||
parse5-htmlparser2-tree-adapter "^6.0.0"
|
||||
|
||||
chokidar@^2.1.8:
|
||||
version "2.1.8"
|
||||
@ -5164,6 +5163,17 @@ css-loader@^5.0.1:
|
||||
schema-utils "^3.0.0"
|
||||
semver "^7.3.5"
|
||||
|
||||
css-select@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8"
|
||||
integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
css-what "^4.0.0"
|
||||
domhandler "^4.0.0"
|
||||
domutils "^2.4.3"
|
||||
nth-check "^2.0.0"
|
||||
|
||||
css-select@^4.1.3:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
|
||||
@ -5175,18 +5185,12 @@ css-select@^4.1.3:
|
||||
domutils "^2.8.0"
|
||||
nth-check "^2.0.1"
|
||||
|
||||
css-select@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"
|
||||
integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
css-what "^6.1.0"
|
||||
domhandler "^5.0.2"
|
||||
domutils "^3.0.1"
|
||||
nth-check "^2.0.1"
|
||||
css-what@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233"
|
||||
integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==
|
||||
|
||||
css-what@^6.0.1, css-what@^6.1.0:
|
||||
css-what@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
|
||||
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
|
||||
@ -5471,6 +5475,15 @@ dom-serializer@^2.0.0:
|
||||
domhandler "^5.0.2"
|
||||
entities "^4.2.0"
|
||||
|
||||
dom-serializer@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1"
|
||||
integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==
|
||||
dependencies:
|
||||
domelementtype "^2.0.1"
|
||||
domhandler "^4.0.0"
|
||||
entities "^2.0.0"
|
||||
|
||||
dom-walk@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
|
||||
@ -5481,7 +5494,7 @@ domain-browser@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
|
||||
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
|
||||
|
||||
domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0:
|
||||
domelementtype@^2.0.1, domelementtype@^2.1.0, domelementtype@^2.2.0, domelementtype@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
|
||||
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
|
||||
@ -5500,7 +5513,7 @@ domhandler@^5.0, domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
domutils@^2.5.2, domutils@^2.8.0:
|
||||
domutils@^2.4.3, domutils@^2.4.4, domutils@^2.5.2, domutils@^2.8.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
|
||||
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
|
||||
@ -5658,6 +5671,11 @@ entities@^4.2.0, entities@^4.4.0:
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||
|
||||
entities@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
|
||||
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
|
||||
|
||||
err-code@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
|
||||
@ -6987,7 +7005,7 @@ html-webpack-plugin@^5.0.0:
|
||||
pretty-error "^4.0.0"
|
||||
tapable "^2.0.0"
|
||||
|
||||
htmlparser2@^6.1.0:
|
||||
htmlparser2@^6.0.0, htmlparser2@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
|
||||
integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
|
||||
@ -6997,7 +7015,7 @@ htmlparser2@^6.1.0:
|
||||
domutils "^2.5.2"
|
||||
entities "^2.0.0"
|
||||
|
||||
htmlparser2@^8.0, htmlparser2@^8.0.1:
|
||||
htmlparser2@^8.0:
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
|
||||
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
|
||||
@ -8823,7 +8841,7 @@ npmlog@^5.0.1:
|
||||
gauge "^3.0.0"
|
||||
set-blocking "^2.0.0"
|
||||
|
||||
nth-check@^2.0.1:
|
||||
nth-check@^2.0.0, nth-check@^2.0.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
|
||||
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
|
||||
@ -9176,26 +9194,18 @@ parse-json@^5.0.0:
|
||||
json-parse-even-better-errors "^2.3.0"
|
||||
lines-and-columns "^1.1.6"
|
||||
|
||||
parse5-htmlparser2-tree-adapter@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1"
|
||||
integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==
|
||||
parse5-htmlparser2-tree-adapter@^6.0.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
|
||||
integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
|
||||
dependencies:
|
||||
domhandler "^5.0.2"
|
||||
parse5 "^7.0.0"
|
||||
parse5 "^6.0.1"
|
||||
|
||||
parse5@^6.0.0:
|
||||
parse5@^6.0.0, parse5@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
|
||||
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
|
||||
|
||||
parse5@^7.0.0:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
|
||||
integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==
|
||||
dependencies:
|
||||
entities "^4.4.0"
|
||||
|
||||
parseurl@~1.3.2, parseurl@~1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
|
Reference in New Issue
Block a user