Compare commits
121 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d24a8e99cc | ||
|
59d2d56091 | ||
|
8515b7060a | ||
|
a076b3f4d0 | ||
|
1e3240ef35 | ||
|
d767080dfe | ||
|
b228eda488 | ||
|
35f54964ce | ||
|
759b834ccc | ||
|
137e12cbbb | ||
|
57b08d9dea | ||
|
1dd7b673a1 | ||
|
b00ffc50c3 | ||
|
f9db40d33d | ||
|
4bc6a843d8 | ||
|
da3e7514f0 | ||
|
bc396bc41b | ||
|
947efe8d63 | ||
|
64189bf8fe | ||
|
400c630418 | ||
|
402360b436 | ||
|
9f001f1521 | ||
|
368e3a32c5 | ||
|
002e3d4b3d | ||
|
f94f9b51c9 | ||
|
055b15bd46 | ||
|
0e70b0b0de | ||
|
8faf9a3eed | ||
|
075d9f9de5 | ||
|
840079be32 | ||
|
50ae962f09 | ||
|
61aa1f9896 | ||
|
d88e0e4dd5 | ||
|
18c36eb4de | ||
|
80aeabad51 | ||
|
419e1f473a | ||
|
80988125e8 | ||
|
271ad2da71 | ||
|
b2732f2595 | ||
|
53820e1e34 | ||
|
09dd45e437 | ||
|
1f654a7820 | ||
|
0690f40bad | ||
|
2285883149 | ||
|
af87e41bb8 | ||
|
9ba884483d | ||
|
f5a300953a | ||
|
ab9a962f58 | ||
|
484adb607f | ||
|
e1f38d4196 | ||
|
5de629acf2 | ||
|
8b4b24a036 | ||
|
75ab130249 | ||
|
981ca7e9a4 | ||
|
acb4e260a7 | ||
|
ff20b0a844 | ||
|
1b77c69a01 | ||
|
158275f5c2 | ||
|
a085c8093e | ||
|
cb358bd745 | ||
|
e788c46601 | ||
|
d551b4bffb | ||
|
c168c7b156 | ||
|
7a46115042 | ||
|
249a7bde89 | ||
|
813740a002 | ||
|
7840c2a6f5 | ||
|
8f6c0d36d9 | ||
|
12690b892b | ||
|
d01b4b71c9 | ||
|
c29e600786 | ||
|
6309b7c45d | ||
|
7e7996e40c | ||
|
deaeab0f61 | ||
|
6bd5451230 | ||
|
fb2d651a6f | ||
|
4845d7c32d | ||
|
c33c315120 | ||
|
99b8f1e789 | ||
|
6af13e1405 | ||
|
f59fa4238c | ||
|
248effc57d | ||
|
9e540b2c1f | ||
|
ab7b5ff490 | ||
|
486f944e0f | ||
|
6cc3d4c442 | ||
|
083290c6d4 | ||
|
cd1b55b850 | ||
|
482ba6c639 | ||
|
e2921b7e37 | ||
|
c87b6153bb | ||
|
488dd2c6b9 | ||
|
1ac678a368 | ||
|
5866c802e5 | ||
|
fe892c840b | ||
|
9685dfb55a | ||
|
c1dc899bc1 | ||
|
d2da43c617 | ||
|
6de5fd4f96 | ||
|
cc3d0d61dd | ||
|
4403f00274 | ||
|
eddfb8e634 | ||
|
4f2790f6d3 | ||
|
96690e1354 | ||
|
982f216a01 | ||
|
13c21e8910 | ||
|
94b7d2b85b | ||
|
9a4f89e69d | ||
|
a5ba03cca0 | ||
|
5203813e7b | ||
|
0e461fd072 | ||
|
326411ca5d | ||
|
c39c450e90 | ||
|
3191954dda | ||
|
20c6d2ea86 | ||
|
f43544e134 | ||
|
474a863708 | ||
|
0bacdca8fe | ||
|
f023d6bca7 | ||
|
150b01f1f3 | ||
|
2b2bb20658 |
@ -327,6 +327,42 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"doc"
|
"doc"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "EternalSide",
|
||||||
|
"name": "Lesha",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/118743608?v=4",
|
||||||
|
"profile": "http://t.me/AAT_L",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "bacongobbler",
|
||||||
|
"name": "Matthew Fisher",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1360539?v=4",
|
||||||
|
"profile": "https://blog.bacongobbler.com",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "kodebach",
|
||||||
|
"name": "Klemens Böswirth",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/23529132?v=4",
|
||||||
|
"profile": "https://github.com/kodebach",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "wnmzzzz",
|
||||||
|
"name": "wnmzzzz",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/117174301?v=4",
|
||||||
|
"profile": "https://github.com/wnmzzzz",
|
||||||
|
"contributions": [
|
||||||
|
"test"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
1
.github/FUNDING.yaml
vendored
1
.github/FUNDING.yaml
vendored
@ -1,4 +1,3 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: [garronej]
|
github: [garronej]
|
||||||
custom: ['https://www.ringerhq.com/experts/garronej']
|
|
||||||
|
10
README.md
10
README.md
@ -6,7 +6,7 @@
|
|||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<a href="https://github.com/garronej/keycloakify/actions">
|
<a href="https://github.com/garronej/keycloakify/actions">
|
||||||
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
|
<img src="https://github.com/keycloakify/keycloakify/actions/workflows/ci.yaml/badge.svg">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.npmjs.com/package/keycloakify">
|
<a href="https://www.npmjs.com/package/keycloakify">
|
||||||
<img src="https://img.shields.io/npm/dm/keycloakify">
|
<img src="https://img.shields.io/npm/dm/keycloakify">
|
||||||
@ -46,7 +46,7 @@ Keycloakify is fully compatible with Keycloak from version 11 to 26...[and beyon
|
|||||||
> 📣 **Keycloakify 26 Released**
|
> 📣 **Keycloakify 26 Released**
|
||||||
> Themes built with Keycloakify versions **prior** to Keycloak 26 are **incompatible** with Keycloak 26.
|
> Themes built with Keycloakify versions **prior** to Keycloak 26 are **incompatible** with Keycloak 26.
|
||||||
> To ensure compatibility, simply upgrade to the latest Keycloakify version for your major release (v10 or v11) and rebuild your theme.
|
> To ensure compatibility, simply upgrade to the latest Keycloakify version for your major release (v10 or v11) and rebuild your theme.
|
||||||
> No breaking changes have been introduced, but the target version ranges have been updated. For more details, see [this guide](https://docs.keycloakify.dev/targeting-specific-keycloak-versions).
|
> No breaking changes have been introduced, but the target version ranges have been updated. For more details, see [this guide](https://docs.keycloakify.dev/features/compiler-options/keycloakversiontargets).
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
@ -168,6 +168,12 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zvn2060"><img src="https://avatars.githubusercontent.com/u/45450852?v=4?s=100" width="100px;" alt="HI_OuO"/><br /><sub><b>HI_OuO</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zvn2060" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zvn2060"><img src="https://avatars.githubusercontent.com/u/45450852?v=4?s=100" width="100px;" alt="HI_OuO"/><br /><sub><b>HI_OuO</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zvn2060" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tripheo0412"><img src="https://avatars.githubusercontent.com/u/25382052?v=4?s=100" width="100px;" alt="Tri Hoang"/><br /><sub><b>Tri Hoang</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=tripheo0412" title="Documentation">📖</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tripheo0412"><img src="https://avatars.githubusercontent.com/u/25382052?v=4?s=100" width="100px;" alt="Tri Hoang"/><br /><sub><b>Tri Hoang</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=tripheo0412" title="Documentation">📖</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://t.me/AAT_L"><img src="https://avatars.githubusercontent.com/u/118743608?v=4?s=100" width="100px;" alt="Lesha"/><br /><sub><b>Lesha</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=EternalSide" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://blog.bacongobbler.com"><img src="https://avatars.githubusercontent.com/u/1360539?v=4?s=100" width="100px;" alt="Matthew Fisher"/><br /><sub><b>Matthew Fisher</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=bacongobbler" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kodebach"><img src="https://avatars.githubusercontent.com/u/23529132?v=4?s=100" width="100px;" alt="Klemens Böswirth"/><br /><sub><b>Klemens Böswirth</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kodebach" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wnmzzzz"><img src="https://avatars.githubusercontent.com/u/117174301?v=4?s=100" width="100px;" alt="wnmzzzz"/><br /><sub><b>wnmzzzz</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=wnmzzzz" title="Tests">⚠️</a></td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "11.5.1",
|
"version": "11.8.23",
|
||||||
"description": "Framework to create custom Keycloak UIs",
|
"description": "Framework to create custom Keycloak UIs",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -67,7 +67,9 @@ export function vendorFrontendDependencies(params: { distDirPath: string }) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
run(`npx webpack --config ${webpackConfigJsFilePath}`);
|
run(`npx webpack --config ${pathBasename(webpackConfigJsFilePath)}`, {
|
||||||
|
cwd: pathDirname(webpackConfigJsFilePath)
|
||||||
|
});
|
||||||
|
|
||||||
fs.readdirSync(webpackOutputDirPath)
|
fs.readdirSync(webpackOutputDirPath)
|
||||||
.filter(fileBasename => !fileBasename.endsWith(".txt"))
|
.filter(fileBasename => !fileBasename.endsWith(".txt"))
|
||||||
|
@ -3,10 +3,9 @@ import child_process from "child_process";
|
|||||||
import { SemVer } from "../src/bin/tools/SemVer";
|
import { SemVer } from "../src/bin/tools/SemVer";
|
||||||
import { dumpContainerConfig } from "../src/bin/start-keycloak/realmConfig/dumpContainerConfig";
|
import { dumpContainerConfig } from "../src/bin/start-keycloak/realmConfig/dumpContainerConfig";
|
||||||
import { cacheDirPath } from "./shared/cacheDirPath";
|
import { cacheDirPath } from "./shared/cacheDirPath";
|
||||||
import { runPrettier } from "../src/bin/tools/runPrettier";
|
|
||||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||||
|
import { writeRealmJsonFile } from "../src/bin/start-keycloak/realmConfig/ParsedRealmJson";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import * as fs from "fs";
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -26,9 +25,7 @@ import chalk from "chalk";
|
|||||||
realmName: "myrealm"
|
realmName: "myrealm"
|
||||||
});
|
});
|
||||||
|
|
||||||
let sourceCode = JSON.stringify(parsedRealmJson, null, 2);
|
const realmJsonFilePath = pathJoin(
|
||||||
|
|
||||||
const filePath = pathJoin(
|
|
||||||
getThisCodebaseRootDirPath(),
|
getThisCodebaseRootDirPath(),
|
||||||
"src",
|
"src",
|
||||||
"bin",
|
"bin",
|
||||||
@ -38,12 +35,11 @@ import chalk from "chalk";
|
|||||||
`realm-kc-${keycloakMajorVersionNumber}.json`
|
`realm-kc-${keycloakMajorVersionNumber}.json`
|
||||||
);
|
);
|
||||||
|
|
||||||
sourceCode = await runPrettier({
|
await writeRealmJsonFile({
|
||||||
sourceCode,
|
parsedRealmJson,
|
||||||
filePath
|
realmJsonFilePath,
|
||||||
|
keycloakMajorVersionNumber
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.writeFileSync(filePath, Buffer.from(sourceCode, "utf8"));
|
console.log(chalk.green(`Realm config dumped to ${realmJsonFilePath}`));
|
||||||
|
|
||||||
console.log(chalk.green(`Realm config dumped to ${filePath}`));
|
|
||||||
})();
|
})();
|
||||||
|
@ -45,7 +45,10 @@ const commonThirdPartyDeps = [
|
|||||||
.replace(/"!\.\/dist\//g, '"!./');
|
.replace(/"!\.\/dist\//g, '"!./');
|
||||||
|
|
||||||
modifiedPackageJsonContent = JSON.stringify(
|
modifiedPackageJsonContent = JSON.stringify(
|
||||||
{ ...JSON.parse(modifiedPackageJsonContent), version: "0.0.0" },
|
{
|
||||||
|
...JSON.parse(modifiedPackageJsonContent),
|
||||||
|
version: `0.0.0-rc.${~~(Math.random() * 1000000)}`
|
||||||
|
},
|
||||||
null,
|
null,
|
||||||
4
|
4
|
||||||
);
|
);
|
||||||
|
@ -280,6 +280,24 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
"fonts",
|
"fonts",
|
||||||
"OpenSans-Semibold-webfont.woff2"
|
"OpenSans-Semibold-webfont.woff2"
|
||||||
),
|
),
|
||||||
|
pathJoin(
|
||||||
|
"patternfly",
|
||||||
|
"dist",
|
||||||
|
"fonts",
|
||||||
|
"OpenSans-SemiboldItalic-webfont.woff2"
|
||||||
|
),
|
||||||
|
pathJoin(
|
||||||
|
"patternfly",
|
||||||
|
"dist",
|
||||||
|
"fonts",
|
||||||
|
"OpenSans-SemiboldItalic-webfont.woff"
|
||||||
|
),
|
||||||
|
pathJoin(
|
||||||
|
"patternfly",
|
||||||
|
"dist",
|
||||||
|
"fonts",
|
||||||
|
"OpenSans-SemiboldItalic-webfont.ttf"
|
||||||
|
),
|
||||||
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
|
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
|
||||||
pathJoin("jquery", "dist", "jquery.min.js"),
|
pathJoin("jquery", "dist", "jquery.min.js"),
|
||||||
pathJoin("rfc4648", "lib", "rfc4648.js")
|
pathJoin("rfc4648", "lib", "rfc4648.js")
|
||||||
|
@ -20,7 +20,7 @@ import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_dele
|
|||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
commandName: "add-story",
|
commandName: "add-story",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
@ -74,7 +74,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
|
|
||||||
if (themeType === "admin") {
|
if (themeType === "admin") {
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.red("✗")} Sorry, there is no Storybook support for the Account UI.`
|
`${chalk.red("✗")} Sorry, there is no Storybook support for the Admin UI.`
|
||||||
);
|
);
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
@ -11,7 +11,7 @@ import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
|||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
commandName: "copy-keycloak-resources-to-public",
|
commandName: "copy-keycloak-resources-to-public",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
import type { BuildContext } from "./shared/buildContext";
|
|
||||||
import { getUiModuleFileSourceCodeReadyToBeCopied } from "./postinstall/getUiModuleFileSourceCodeReadyToBeCopied";
|
|
||||||
import { getAbsoluteAndInOsFormatPath } from "./tools/getAbsoluteAndInOsFormatPath";
|
|
||||||
import { relative as pathRelative, dirname as pathDirname, join as pathJoin } from "path";
|
|
||||||
import { getUiModuleMetas } from "./postinstall/uiModuleMeta";
|
|
||||||
import { getInstalledModuleDirPath } from "./tools/getInstalledModuleDirPath";
|
|
||||||
import * as fsPr from "fs/promises";
|
|
||||||
import {
|
|
||||||
readManagedGitignoreFile,
|
|
||||||
writeManagedGitignoreFile
|
|
||||||
} from "./postinstall/managedGitignoreFile";
|
|
||||||
|
|
||||||
export async function command(params: {
|
|
||||||
buildContext: BuildContext;
|
|
||||||
cliCommandOptions: {
|
|
||||||
file: string;
|
|
||||||
};
|
|
||||||
}) {
|
|
||||||
const { buildContext, cliCommandOptions } = params;
|
|
||||||
|
|
||||||
const fileRelativePath = pathRelative(
|
|
||||||
buildContext.themeSrcDirPath,
|
|
||||||
getAbsoluteAndInOsFormatPath({
|
|
||||||
cwd: buildContext.themeSrcDirPath,
|
|
||||||
pathIsh: cliCommandOptions.file
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const uiModuleMetas = await getUiModuleMetas({ buildContext });
|
|
||||||
|
|
||||||
const uiModuleMeta = uiModuleMetas.find(({ files }) =>
|
|
||||||
files.map(({ fileRelativePath }) => fileRelativePath).includes(fileRelativePath)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!uiModuleMeta) {
|
|
||||||
throw new Error(`No UI module found for the file ${fileRelativePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uiModuleDirPath = await getInstalledModuleDirPath({
|
|
||||||
moduleName: uiModuleMeta.moduleName,
|
|
||||||
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath),
|
|
||||||
projectDirPath: buildContext.projectDirPath
|
|
||||||
});
|
|
||||||
|
|
||||||
const sourceCode = await getUiModuleFileSourceCodeReadyToBeCopied({
|
|
||||||
buildContext,
|
|
||||||
fileRelativePath,
|
|
||||||
isForEjection: true,
|
|
||||||
uiModuleName: uiModuleMeta.moduleName,
|
|
||||||
uiModuleDirPath,
|
|
||||||
uiModuleVersion: uiModuleMeta.version
|
|
||||||
});
|
|
||||||
|
|
||||||
await fsPr.writeFile(
|
|
||||||
pathJoin(buildContext.themeSrcDirPath, fileRelativePath),
|
|
||||||
sourceCode
|
|
||||||
);
|
|
||||||
|
|
||||||
const { ejectedFilesRelativePaths } = await readManagedGitignoreFile({
|
|
||||||
buildContext
|
|
||||||
});
|
|
||||||
|
|
||||||
await writeManagedGitignoreFile({
|
|
||||||
buildContext,
|
|
||||||
uiModuleMetas,
|
|
||||||
ejectedFilesRelativePaths: [...ejectedFilesRelativePaths, fileRelativePath]
|
|
||||||
});
|
|
||||||
}
|
|
@ -11,12 +11,7 @@ import {
|
|||||||
} from "./shared/constants";
|
} from "./shared/constants";
|
||||||
import { capitalize } from "tsafe/capitalize";
|
import { capitalize } from "tsafe/capitalize";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import {
|
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||||
join as pathJoin,
|
|
||||||
relative as pathRelative,
|
|
||||||
dirname as pathDirname,
|
|
||||||
basename as pathBasename
|
|
||||||
} from "path";
|
|
||||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||||
import { assert, Equals } from "tsafe/assert";
|
import { assert, Equals } from "tsafe/assert";
|
||||||
import type { BuildContext } from "./shared/buildContext";
|
import type { BuildContext } from "./shared/buildContext";
|
||||||
@ -27,7 +22,7 @@ import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
|||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
commandName: "eject-page",
|
commandName: "eject-page",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
@ -67,9 +62,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
if (themeType === "admin") {
|
if (themeType === "admin") {
|
||||||
console.log(
|
console.log("Use `npx keycloakify own` command instead, see documentation");
|
||||||
"Use `npx keycloakify eject-file` command instead, see documentation"
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
@ -79,85 +72,16 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
||||||
buildContext.implementedThemeTypes.account.type === "Single-Page")
|
buildContext.implementedThemeTypes.account.type === "Single-Page")
|
||||||
) {
|
) {
|
||||||
const srcDirPath = pathJoin(
|
|
||||||
pathDirname(buildContext.packageJsonFilePath),
|
|
||||||
"node_modules",
|
|
||||||
"@keycloakify",
|
|
||||||
`keycloak-account-ui`,
|
|
||||||
"src"
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
[
|
chalk.yellow(
|
||||||
`There isn't an interactive CLI to eject components of the Account SPA UI.`,
|
[
|
||||||
`You can however copy paste into your codebase the any file or directory from the following source directory:`,
|
"You are implementing a Single-Page Account theme.",
|
||||||
``,
|
"The eject-page command isn't applicable in this context"
|
||||||
`${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`,
|
].join("\n")
|
||||||
``
|
)
|
||||||
].join("\n")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
eject_entrypoint: {
|
process.exit(1);
|
||||||
const kcUiTsxFileRelativePath = `KcAccountUi.tsx` as const;
|
|
||||||
|
|
||||||
const themeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
|
|
||||||
|
|
||||||
const targetFilePath = pathJoin(themeSrcDirPath, kcUiTsxFileRelativePath);
|
|
||||||
|
|
||||||
if (fs.existsSync(targetFilePath)) {
|
|
||||||
break eject_entrypoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.cpSync(pathJoin(srcDirPath, kcUiTsxFileRelativePath), targetFilePath);
|
|
||||||
|
|
||||||
{
|
|
||||||
const kcPageTsxFilePath = pathJoin(themeSrcDirPath, "KcPage.tsx");
|
|
||||||
|
|
||||||
const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8");
|
|
||||||
|
|
||||||
const componentName = pathBasename(kcUiTsxFileRelativePath).replace(
|
|
||||||
/.tsx$/,
|
|
||||||
""
|
|
||||||
);
|
|
||||||
|
|
||||||
let modifiedKcPageTsxCode = kcPageTsxCode.replace(
|
|
||||||
`@keycloakify/keycloak-account-ui/${componentName}`,
|
|
||||||
`./${componentName}`
|
|
||||||
);
|
|
||||||
|
|
||||||
run_prettier: {
|
|
||||||
if (!(await getIsPrettierAvailable())) {
|
|
||||||
break run_prettier;
|
|
||||||
}
|
|
||||||
|
|
||||||
modifiedKcPageTsxCode = await runPrettier({
|
|
||||||
filePath: kcPageTsxFilePath,
|
|
||||||
sourceCode: modifiedKcPageTsxCode
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
kcPageTsxFilePath,
|
|
||||||
Buffer.from(modifiedKcPageTsxCode, "utf8")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const routesTsxFilePath = pathRelative(
|
|
||||||
process.cwd(),
|
|
||||||
pathJoin(srcDirPath, "routes.tsx")
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
`To help you get started ${chalk.bold(pathRelative(process.cwd(), targetFilePath))} has been copied into your project.`,
|
|
||||||
`The next step is usually to eject ${chalk.bold(routesTsxFilePath)}`,
|
|
||||||
`with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), themeSrcDirPath)}\``,
|
|
||||||
`then update the import of routes in ${kcUiTsxFileRelativePath}.`
|
|
||||||
].join("\n")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(0);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,12 +92,14 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
const templateValue = "Template.tsx (Layout common to every page)";
|
const templateValue = "Template.tsx (Layout common to every page)";
|
||||||
const userProfileFormFieldsValue =
|
const userProfileFormFieldsValue =
|
||||||
"UserProfileFormFields.tsx (Renders the form of the register.ftl, login-update-profile.ftl, update-email.ftl and idp-review-user-profile.ftl)";
|
"UserProfileFormFields.tsx (Renders the form of the register.ftl, login-update-profile.ftl, update-email.ftl and idp-review-user-profile.ftl)";
|
||||||
|
const otherPageValue = "The page you're looking for isn't listed here";
|
||||||
|
|
||||||
const { value: pageIdOrComponent } = await cliSelect<
|
const { value: pageIdOrComponent } = await cliSelect<
|
||||||
| LoginThemePageId
|
| LoginThemePageId
|
||||||
| AccountThemePageId
|
| AccountThemePageId
|
||||||
| typeof templateValue
|
| typeof templateValue
|
||||||
| typeof userProfileFormFieldsValue
|
| typeof userProfileFormFieldsValue
|
||||||
|
| typeof otherPageValue
|
||||||
>({
|
>({
|
||||||
values: (() => {
|
values: (() => {
|
||||||
switch (themeType) {
|
switch (themeType) {
|
||||||
@ -181,10 +107,11 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return [
|
return [
|
||||||
templateValue,
|
templateValue,
|
||||||
userProfileFormFieldsValue,
|
userProfileFormFieldsValue,
|
||||||
...LOGIN_THEME_PAGE_IDS
|
...LOGIN_THEME_PAGE_IDS,
|
||||||
|
otherPageValue
|
||||||
];
|
];
|
||||||
case "account":
|
case "account":
|
||||||
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS];
|
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS, otherPageValue];
|
||||||
}
|
}
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
})()
|
})()
|
||||||
@ -192,6 +119,17 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (pageIdOrComponent === otherPageValue) {
|
||||||
|
console.log(
|
||||||
|
[
|
||||||
|
"To style a page not included in the base Keycloak, such as one added by a third-party Keycloak extension,",
|
||||||
|
"refer to the documentation: https://docs.keycloakify.dev/features/styling-a-custom-page-not-included-in-base-keycloak"
|
||||||
|
].join(" ")
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`→ ${pageIdOrComponent}`);
|
console.log(`→ ${pageIdOrComponent}`);
|
||||||
|
|
||||||
const componentBasename = (() => {
|
const componentBasename = (() => {
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import { join as pathJoin } from "path";
|
|
||||||
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
|
||||||
|
|
||||||
export function copyBoilerplate(params: {
|
|
||||||
accountThemeType: "Single-Page" | "Multi-Page";
|
|
||||||
accountThemeSrcDirPath: string;
|
|
||||||
}) {
|
|
||||||
const { accountThemeType, accountThemeSrcDirPath } = params;
|
|
||||||
|
|
||||||
fs.cpSync(
|
|
||||||
pathJoin(
|
|
||||||
getThisCodebaseRootDirPath(),
|
|
||||||
"src",
|
|
||||||
"bin",
|
|
||||||
"initialize-account-theme",
|
|
||||||
"src",
|
|
||||||
(() => {
|
|
||||||
switch (accountThemeType) {
|
|
||||||
case "Single-Page":
|
|
||||||
return "single-page";
|
|
||||||
case "Multi-Page":
|
|
||||||
return "multi-page";
|
|
||||||
}
|
|
||||||
assert<Equals<typeof accountThemeType, never>>(false);
|
|
||||||
})()
|
|
||||||
),
|
|
||||||
accountThemeSrcDirPath,
|
|
||||||
{ recursive: true }
|
|
||||||
);
|
|
||||||
}
|
|
@ -7,11 +7,12 @@ import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeIm
|
|||||||
import { command as updateKcGenCommand } from "../update-kc-gen";
|
import { command as updateKcGenCommand } from "../update-kc-gen";
|
||||||
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
|
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
|
||||||
import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges";
|
import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges";
|
||||||
|
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
||||||
|
|
||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
commandName: "initialize-account-theme",
|
commandName: "initialize-account-theme",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
@ -22,22 +23,6 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
|
|
||||||
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
|
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
|
||||||
|
|
||||||
if (
|
|
||||||
fs.existsSync(accountThemeSrcDirPath) &&
|
|
||||||
fs.readdirSync(accountThemeSrcDirPath).length > 0
|
|
||||||
) {
|
|
||||||
console.warn(
|
|
||||||
chalk.red(
|
|
||||||
`There is already a ${pathRelative(
|
|
||||||
process.cwd(),
|
|
||||||
accountThemeSrcDirPath
|
|
||||||
)} directory in your project. Aborting.`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
exitIfUncommittedChanges({
|
exitIfUncommittedChanges({
|
||||||
projectDirPath: buildContext.projectDirPath
|
projectDirPath: buildContext.projectDirPath
|
||||||
});
|
});
|
||||||
@ -51,23 +36,41 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
switch (accountThemeType) {
|
switch (accountThemeType) {
|
||||||
case "Multi-Page":
|
case "Multi-Page":
|
||||||
{
|
{
|
||||||
const { initializeAccountTheme_multiPage } = await import(
|
if (
|
||||||
"./initializeAccountTheme_multiPage"
|
fs.existsSync(accountThemeSrcDirPath) &&
|
||||||
);
|
fs.readdirSync(accountThemeSrcDirPath).length > 0
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
chalk.red(
|
||||||
|
`There is already a ${pathRelative(
|
||||||
|
process.cwd(),
|
||||||
|
accountThemeSrcDirPath
|
||||||
|
)} directory in your project. Aborting.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
await initializeAccountTheme_multiPage({
|
process.exit(-1);
|
||||||
accountThemeSrcDirPath
|
}
|
||||||
});
|
|
||||||
|
fs.cpSync(
|
||||||
|
pathJoin(
|
||||||
|
getThisCodebaseRootDirPath(),
|
||||||
|
"src",
|
||||||
|
"bin",
|
||||||
|
"initialize-account-theme",
|
||||||
|
"multi-page-boilerplate"
|
||||||
|
),
|
||||||
|
accountThemeSrcDirPath,
|
||||||
|
{ recursive: true }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "Single-Page":
|
case "Single-Page":
|
||||||
{
|
{
|
||||||
const { initializeAccountTheme_singlePage } = await import(
|
const { initializeSpa } = await import("../shared/initializeSpa");
|
||||||
"./initializeAccountTheme_singlePage"
|
|
||||||
);
|
|
||||||
|
|
||||||
await initializeAccountTheme_singlePage({
|
await initializeSpa({
|
||||||
accountThemeSrcDirPath,
|
themeType: "account",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import { relative as pathRelative } from "path";
|
|
||||||
import chalk from "chalk";
|
|
||||||
import { copyBoilerplate } from "./copyBoilerplate";
|
|
||||||
|
|
||||||
export async function initializeAccountTheme_multiPage(params: {
|
|
||||||
accountThemeSrcDirPath: string;
|
|
||||||
}) {
|
|
||||||
const { accountThemeSrcDirPath } = params;
|
|
||||||
|
|
||||||
copyBoilerplate({
|
|
||||||
accountThemeType: "Multi-Page",
|
|
||||||
accountThemeSrcDirPath
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
chalk.green("The Multi-Page account theme has been initialized."),
|
|
||||||
`Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}`
|
|
||||||
].join("\n")
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,140 +0,0 @@
|
|||||||
import { relative as pathRelative, dirname as pathDirname } from "path";
|
|
||||||
import type { BuildContext } from "../shared/buildContext";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import chalk from "chalk";
|
|
||||||
import {
|
|
||||||
getLatestsSemVersionedTag,
|
|
||||||
type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag
|
|
||||||
} from "../shared/getLatestsSemVersionedTag";
|
|
||||||
import { SemVer } from "../tools/SemVer";
|
|
||||||
import fetch from "make-fetch-happen";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { assert, type Equals, is } from "tsafe/assert";
|
|
||||||
import { id } from "tsafe/id";
|
|
||||||
import { npmInstall } from "../tools/npmInstall";
|
|
||||||
import { copyBoilerplate } from "./copyBoilerplate";
|
|
||||||
|
|
||||||
type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {
|
|
||||||
fetchOptions: BuildContext["fetchOptions"];
|
|
||||||
packageJsonFilePath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
|
||||||
|
|
||||||
export async function initializeAccountTheme_singlePage(params: {
|
|
||||||
accountThemeSrcDirPath: string;
|
|
||||||
buildContext: BuildContextLike;
|
|
||||||
}) {
|
|
||||||
const { accountThemeSrcDirPath, buildContext } = params;
|
|
||||||
|
|
||||||
const OWNER = "keycloakify";
|
|
||||||
const REPO = "keycloak-account-ui";
|
|
||||||
|
|
||||||
const [semVersionedTag] = await getLatestsSemVersionedTag({
|
|
||||||
owner: OWNER,
|
|
||||||
repo: REPO,
|
|
||||||
count: 1,
|
|
||||||
doIgnoreReleaseCandidates: false,
|
|
||||||
buildContext
|
|
||||||
});
|
|
||||||
|
|
||||||
const dependencies = await fetch(
|
|
||||||
`https://raw.githubusercontent.com/${OWNER}/${REPO}/${semVersionedTag.tag}/dependencies.gen.json`,
|
|
||||||
buildContext.fetchOptions
|
|
||||||
)
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(
|
|
||||||
(() => {
|
|
||||||
type Dependencies = {
|
|
||||||
dependencies: Record<string, string>;
|
|
||||||
devDependencies?: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const zDependencies = (() => {
|
|
||||||
type TargetType = Dependencies;
|
|
||||||
|
|
||||||
const zTargetType = z.object({
|
|
||||||
dependencies: z.record(z.string()),
|
|
||||||
devDependencies: z.record(z.string()).optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
|
||||||
|
|
||||||
return id<z.ZodType<TargetType>>(zTargetType);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return o => zDependencies.parse(o);
|
|
||||||
})()
|
|
||||||
);
|
|
||||||
|
|
||||||
dependencies.dependencies["@keycloakify/keycloak-account-ui"] = SemVer.stringify(
|
|
||||||
semVersionedTag.version
|
|
||||||
);
|
|
||||||
|
|
||||||
const parsedPackageJson = (() => {
|
|
||||||
type ParsedPackageJson = {
|
|
||||||
dependencies?: Record<string, string>;
|
|
||||||
devDependencies?: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const zParsedPackageJson = (() => {
|
|
||||||
type TargetType = ParsedPackageJson;
|
|
||||||
|
|
||||||
const zTargetType = z.object({
|
|
||||||
dependencies: z.record(z.string()).optional(),
|
|
||||||
devDependencies: z.record(z.string()).optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
|
||||||
|
|
||||||
return id<z.ZodType<TargetType>>(zTargetType);
|
|
||||||
})();
|
|
||||||
const parsedPackageJson = JSON.parse(
|
|
||||||
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
zParsedPackageJson.parse(parsedPackageJson);
|
|
||||||
|
|
||||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
|
||||||
|
|
||||||
return parsedPackageJson;
|
|
||||||
})();
|
|
||||||
|
|
||||||
parsedPackageJson.dependencies = {
|
|
||||||
...parsedPackageJson.dependencies,
|
|
||||||
...dependencies.dependencies
|
|
||||||
};
|
|
||||||
|
|
||||||
parsedPackageJson.devDependencies = {
|
|
||||||
...parsedPackageJson.devDependencies,
|
|
||||||
...dependencies.devDependencies
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Object.keys(parsedPackageJson.devDependencies).length === 0) {
|
|
||||||
delete parsedPackageJson.devDependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
buildContext.packageJsonFilePath,
|
|
||||||
JSON.stringify(parsedPackageJson, undefined, 4)
|
|
||||||
);
|
|
||||||
|
|
||||||
npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) });
|
|
||||||
|
|
||||||
copyBoilerplate({
|
|
||||||
accountThemeType: "Single-Page",
|
|
||||||
accountThemeSrcDirPath
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
chalk.green(
|
|
||||||
"The Single-Page account theme has been successfully initialized."
|
|
||||||
),
|
|
||||||
`Using Account UI of Keycloak version: ${chalk.bold(semVersionedTag.tag.split("-")[0])}`,
|
|
||||||
`Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}`,
|
|
||||||
`Dependencies added to your project's package.json: `,
|
|
||||||
chalk.bold(JSON.stringify(dependencies, null, 2))
|
|
||||||
].join("\n")
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,8 +1,8 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { i18nBuilder } from "keycloakify/account";
|
import { i18nBuilder } from "keycloakify/account";
|
||||||
import type { ThemeName } from "../kc.gen";
|
import type { ThemeName } from "../kc.gen";
|
||||||
|
|
||||||
/** @see: https://docs.keycloakify.dev/i18n */
|
/** @see: https://docs.keycloakify.dev/features/i18n */
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { useI18n, ofTypeI18n } = i18nBuilder.withThemeName<ThemeName>().build();
|
const { useI18n, ofTypeI18n } = i18nBuilder.withThemeName<ThemeName>().build();
|
||||||
|
|
||||||
type I18n = typeof ofTypeI18n;
|
type I18n = typeof ofTypeI18n;
|
@ -1,7 +0,0 @@
|
|||||||
import type { KcContextLike } from "@keycloakify/keycloak-account-ui";
|
|
||||||
import type { KcEnvName } from "../kc.gen";
|
|
||||||
|
|
||||||
export type KcContext = KcContextLike & {
|
|
||||||
themeType: "account";
|
|
||||||
properties: Record<KcEnvName, string>;
|
|
||||||
};
|
|
@ -1,11 +0,0 @@
|
|||||||
import { lazy } from "react";
|
|
||||||
import { KcAccountUiLoader } from "@keycloakify/keycloak-account-ui";
|
|
||||||
import type { KcContext } from "./KcContext";
|
|
||||||
|
|
||||||
const KcAccountUi = lazy(() => import("@keycloakify/keycloak-account-ui/KcAccountUi"));
|
|
||||||
|
|
||||||
export default function KcPage(props: { kcContext: KcContext }) {
|
|
||||||
const { kcContext } = props;
|
|
||||||
|
|
||||||
return <KcAccountUiLoader kcContext={kcContext} KcAccountUi={KcAccountUi} />;
|
|
||||||
}
|
|
39
src/bin/initialize-admin-theme.ts
Normal file
39
src/bin/initialize-admin-theme.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import type { BuildContext } from "./shared/buildContext";
|
||||||
|
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||||
|
import { initializeSpa } from "./shared/initializeSpa";
|
||||||
|
import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges";
|
||||||
|
import { command as updateKcGenCommand } from "./update-kc-gen";
|
||||||
|
|
||||||
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
|
const { buildContext } = params;
|
||||||
|
|
||||||
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
|
commandName: "initialize-admin-theme",
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasBeenHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
exitIfUncommittedChanges({
|
||||||
|
projectDirPath: buildContext.projectDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
await initializeSpa({
|
||||||
|
themeType: "admin",
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateKcGenCommand({
|
||||||
|
buildContext: {
|
||||||
|
...buildContext,
|
||||||
|
implementedThemeTypes: {
|
||||||
|
...buildContext.implementedThemeTypes,
|
||||||
|
admin: {
|
||||||
|
isImplemented: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -1,18 +1,23 @@
|
|||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
|
||||||
import { transformCodebase } from "./tools/transformCodebase";
|
|
||||||
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
|
||||||
import type { BuildContext } from "./shared/buildContext";
|
import type { BuildContext } from "./shared/buildContext";
|
||||||
import * as fs from "fs";
|
import cliSelect from "cli-select";
|
||||||
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
|
|
||||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||||
import fetch from "make-fetch-happen";
|
import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges";
|
||||||
import { SemVer } from "./tools/SemVer";
|
|
||||||
import { assert } from "tsafe/assert";
|
import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { assert, is, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { addSyncExtensionsToPostinstallScript } from "./shared/addSyncExtensionsToPostinstallScript";
|
||||||
|
import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier";
|
||||||
|
import { npmInstall } from "./tools/npmInstall";
|
||||||
|
import * as child_process from "child_process";
|
||||||
|
import { z } from "zod";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
commandName: "initialize-email-theme",
|
commandName: "initialize-email-theme",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
@ -21,6 +26,10 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exitIfUncommittedChanges({
|
||||||
|
projectDirPath: buildContext.projectDirPath
|
||||||
|
});
|
||||||
|
|
||||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -28,93 +37,120 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
fs.readdirSync(emailThemeSrcDirPath).length > 0
|
fs.readdirSync(emailThemeSrcDirPath).length > 0
|
||||||
) {
|
) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`There is already a non empty ${pathRelative(
|
chalk.red(
|
||||||
process.cwd(),
|
`There is already a ${pathRelative(
|
||||||
emailThemeSrcDirPath
|
process.cwd(),
|
||||||
)} directory in your project. Aborting.`
|
emailThemeSrcDirPath
|
||||||
|
)} directory in your project. Aborting.`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Initialize with the base email theme from which version of Keycloak?");
|
const { value: emailThemeType } = await cliSelect({
|
||||||
|
values: [
|
||||||
|
"native (FreeMarker)" as const,
|
||||||
|
"Another email templating solution" as const
|
||||||
|
]
|
||||||
|
}).catch(() => {
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
let { keycloakVersion } = await promptKeycloakVersion({
|
if (emailThemeType === "Another email templating solution") {
|
||||||
// NOTE: This is arbitrary
|
console.log(
|
||||||
startingFromMajor: 17,
|
[
|
||||||
excludeMajorVersions: [],
|
"There is currently no automated support for keycloakify-email, it has to be done manually, see documentation:",
|
||||||
doOmitPatch: false,
|
"https://docs.keycloakify.dev/theme-types/email-theme"
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPackageJson = (() => {
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
scripts?: Record<string, string | undefined>;
|
||||||
|
dependencies?: Record<string, string | undefined>;
|
||||||
|
devDependencies?: Record<string, string | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = (() => {
|
||||||
|
type TargetType = ParsedPackageJson;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
scripts: z.record(z.union([z.string(), z.undefined()])).optional(),
|
||||||
|
dependencies: z.record(z.union([z.string(), z.undefined()])).optional(),
|
||||||
|
devDependencies: z.record(z.union([z.string(), z.undefined()])).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
const parsedPackageJson = JSON.parse(
|
||||||
|
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
|
|
||||||
|
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||||
|
|
||||||
|
return parsedPackageJson;
|
||||||
|
})();
|
||||||
|
|
||||||
|
addSyncExtensionsToPostinstallScript({
|
||||||
|
parsedPackageJson,
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
const getUrl = (keycloakVersion: string) => {
|
const moduleName = `@keycloakify/email-native`;
|
||||||
return `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`;
|
|
||||||
};
|
|
||||||
|
|
||||||
keycloakVersion = await (async () => {
|
const [version] = ((): string[] => {
|
||||||
const keycloakVersionParsed = SemVer.parse(keycloakVersion);
|
const cmdOutput = child_process
|
||||||
|
.execSync(`npm show ${moduleName} versions --json`)
|
||||||
|
.toString("utf8")
|
||||||
|
.trim();
|
||||||
|
|
||||||
while (true) {
|
const versions = JSON.parse(cmdOutput) as string | string[];
|
||||||
const url = getUrl(SemVer.stringify(keycloakVersionParsed));
|
|
||||||
|
|
||||||
const response = await fetch(url, buildContext.fetchOptions);
|
// NOTE: Bug in some older npm versions
|
||||||
|
if (typeof versions === "string") {
|
||||||
if (response.ok) {
|
return [versions];
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(keycloakVersionParsed.patch !== 0);
|
|
||||||
|
|
||||||
keycloakVersionParsed.patch--;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return SemVer.stringify(keycloakVersionParsed);
|
return versions;
|
||||||
})();
|
})()
|
||||||
|
.reverse()
|
||||||
|
.filter(version => !version.includes("-"));
|
||||||
|
|
||||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
assert(version !== undefined);
|
||||||
url: getUrl(keycloakVersion),
|
|
||||||
cacheDirPath: buildContext.cacheDirPath,
|
|
||||||
fetchOptions: buildContext.fetchOptions,
|
|
||||||
uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme",
|
|
||||||
onArchiveFile: async ({ fileRelativePath, writeFile }) => {
|
|
||||||
const fileRelativePath_target = pathRelative(
|
|
||||||
pathJoin("theme", "base", "email"),
|
|
||||||
fileRelativePath
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fileRelativePath_target.startsWith("..")) {
|
(parsedPackageJson.dependencies ??= {})[moduleName] = `~${version}`;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeFile({ fileRelativePath: fileRelativePath_target });
|
if (parsedPackageJson.devDependencies !== undefined) {
|
||||||
}
|
delete parsedPackageJson.devDependencies[moduleName];
|
||||||
});
|
}
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: extractedDirPath,
|
|
||||||
destDirPath: emailThemeSrcDirPath
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties");
|
let sourceCode = JSON.stringify(parsedPackageJson, undefined, 2);
|
||||||
|
|
||||||
|
if (await getIsPrettierAvailable()) {
|
||||||
|
sourceCode = await runPrettier({
|
||||||
|
sourceCode,
|
||||||
|
filePath: buildContext.packageJsonFilePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
themePropertyFilePath,
|
buildContext.packageJsonFilePath,
|
||||||
Buffer.from(
|
Buffer.from(sourceCode, "utf8")
|
||||||
[
|
|
||||||
`parent=base`,
|
|
||||||
fs.readFileSync(themePropertyFilePath).toString("utf8")
|
|
||||||
].join("\n"),
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
await npmInstall({
|
||||||
`The \`${pathJoin(
|
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||||
".",
|
});
|
||||||
pathRelative(process.cwd(), emailThemeSrcDirPath)
|
|
||||||
)}\` directory have been created.`
|
console.log(chalk.green("Email theme initialized."));
|
||||||
);
|
|
||||||
console.log("You can delete any file you don't modify.");
|
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ import { readFileSync } from "fs";
|
|||||||
import { isInside } from "../../tools/isInside";
|
import { isInside } from "../../tools/isInside";
|
||||||
import child_process from "child_process";
|
import child_process from "child_process";
|
||||||
import { rmSync } from "../../tools/fs.rmSync";
|
import { rmSync } from "../../tools/fs.rmSync";
|
||||||
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
|
||||||
import { existsAsync } from "../../tools/fs.existsAsync";
|
import { existsAsync } from "../../tools/fs.existsAsync";
|
||||||
|
|
||||||
export type BuildContextLike = BuildContextLike_generatePom & {
|
export type BuildContextLike = BuildContextLike_generatePom & {
|
||||||
@ -106,29 +105,55 @@ export async function buildJar(params: {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
remove_account_v1_in_meta_inf: {
|
{
|
||||||
if (!doesImplementAccountV1Theme) {
|
const filePath = pathJoin(
|
||||||
// NOTE: We do not have account v1 anyway
|
tmpResourcesDirPath,
|
||||||
break remove_account_v1_in_meta_inf;
|
"META-INF",
|
||||||
}
|
"keycloak-themes.json"
|
||||||
|
);
|
||||||
|
|
||||||
if (keycloakAccountV1Version !== null) {
|
await fs.mkdir(pathDirname(filePath));
|
||||||
// NOTE: No, we need to keep account-v1 in meta-inf
|
|
||||||
break remove_account_v1_in_meta_inf;
|
|
||||||
}
|
|
||||||
|
|
||||||
writeMetaInfKeycloakThemes({
|
await fs.writeFile(
|
||||||
resourcesDirPath: tmpResourcesDirPath,
|
filePath,
|
||||||
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
|
Buffer.from(
|
||||||
assert(metaInfKeycloakTheme !== undefined);
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
themes: await (async () => {
|
||||||
|
const dirPath = pathJoin(tmpResourcesDirPath, "theme");
|
||||||
|
|
||||||
metaInfKeycloakTheme.themes = metaInfKeycloakTheme.themes.filter(
|
const themeNames = (await fs.readdir(dirPath)).sort(
|
||||||
({ name }) => name !== "account-v1"
|
(a, b) => {
|
||||||
);
|
const indexA = buildContext.themeNames.indexOf(a);
|
||||||
|
const indexB = buildContext.themeNames.indexOf(b);
|
||||||
|
|
||||||
return metaInfKeycloakTheme;
|
const orderA = indexA === -1 ? Infinity : indexA;
|
||||||
}
|
const orderB = indexB === -1 ? Infinity : indexB;
|
||||||
});
|
|
||||||
|
return orderA - orderB;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
themeNames.map(async themeName => {
|
||||||
|
const types = await fs.readdir(
|
||||||
|
pathJoin(dirPath, themeName)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: themeName,
|
||||||
|
types
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
route_legacy_pages: {
|
route_legacy_pages: {
|
||||||
@ -195,31 +220,39 @@ export async function buildJar(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) =>
|
{
|
||||||
child_process.exec(
|
const mvnBuildCmd = `mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`;
|
||||||
`mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`,
|
|
||||||
{ cwd: keycloakifyBuildCacheDirPath },
|
|
||||||
error => {
|
|
||||||
if (error !== null) {
|
|
||||||
console.error(
|
|
||||||
`Build jar failed: ${JSON.stringify(
|
|
||||||
{
|
|
||||||
jarFileBasename,
|
|
||||||
keycloakAccountV1Version,
|
|
||||||
keycloakThemeAdditionalInfoExtensionVersion
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
reject(error);
|
await new Promise<void>((resolve, reject) =>
|
||||||
return;
|
child_process.exec(
|
||||||
|
mvnBuildCmd,
|
||||||
|
{ cwd: keycloakifyBuildCacheDirPath },
|
||||||
|
error => {
|
||||||
|
if (error !== null) {
|
||||||
|
console.error(
|
||||||
|
[
|
||||||
|
`Build jar failed: ${JSON.stringify(
|
||||||
|
{
|
||||||
|
jarFileBasename,
|
||||||
|
keycloakAccountV1Version,
|
||||||
|
keycloakThemeAdditionalInfoExtensionVersion
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}`,
|
||||||
|
"Try running the following command to debug the issue (you are probably under a restricted network and you need to configure your proxy):",
|
||||||
|
`cd ${keycloakifyBuildCacheDirPath} && ${mvnBuildCmd}`
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
}
|
}
|
||||||
resolve();
|
)
|
||||||
}
|
);
|
||||||
)
|
}
|
||||||
);
|
|
||||||
|
|
||||||
await fs.rename(
|
await fs.rename(
|
||||||
pathJoin(
|
pathJoin(
|
||||||
|
@ -190,7 +190,7 @@ function decodeHtmlEntities(htmlStr){
|
|||||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
|
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
|
||||||
key == "loginAction" &&
|
key == "loginAction" &&
|
||||||
areSamePath(path, ["url"]) &&
|
areSamePath(path, ["url"]) &&
|
||||||
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(xKeycloakify.pageId) &&
|
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl", "frontchannel-logout.ftl"]?seq_contains(xKeycloakify.pageId) &&
|
||||||
!(auth?has_content && auth.showTryAnotherWayLink())
|
!(auth?has_content && auth.showTryAnotherWayLink())
|
||||||
) || (
|
) || (
|
||||||
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
|
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
|
||||||
|
@ -6,8 +6,7 @@ import {
|
|||||||
join as pathJoin,
|
join as pathJoin,
|
||||||
relative as pathRelative,
|
relative as pathRelative,
|
||||||
dirname as pathDirname,
|
dirname as pathDirname,
|
||||||
extname as pathExtname,
|
basename as pathBasename
|
||||||
sep as pathSep
|
|
||||||
} from "path";
|
} from "path";
|
||||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||||
@ -31,15 +30,13 @@ import {
|
|||||||
type BuildContextLike as BuildContextLike_generateMessageProperties
|
type BuildContextLike as BuildContextLike_generateMessageProperties
|
||||||
} from "./generateMessageProperties";
|
} from "./generateMessageProperties";
|
||||||
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
||||||
import {
|
|
||||||
writeMetaInfKeycloakThemes,
|
|
||||||
type MetaInfKeycloakTheme
|
|
||||||
} from "../../shared/metaInfKeycloakThemes";
|
|
||||||
import { objectEntries } from "tsafe/objectEntries";
|
|
||||||
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
||||||
import * as child_process from "child_process";
|
|
||||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||||
import propertiesParser from "properties-parser";
|
import propertiesParser from "properties-parser";
|
||||||
|
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
|
||||||
|
import { listInstalledModules } from "../../tools/listInstalledModules";
|
||||||
|
import { isInside } from "../../tools/isInside";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||||
BuildContextLike_generateMessageProperties & {
|
BuildContextLike_generateMessageProperties & {
|
||||||
@ -60,6 +57,8 @@ export async function generateResources(params: {
|
|||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
resourcesDirPath: string;
|
resourcesDirPath: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
const { resourcesDirPath, buildContext } = params;
|
const { resourcesDirPath, buildContext } = params;
|
||||||
|
|
||||||
const [themeName] = buildContext.themeNames;
|
const [themeName] = buildContext.themeNames;
|
||||||
@ -77,12 +76,23 @@ export async function generateResources(params: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const writeMessagePropertiesFilesByThemeType: Partial<
|
const writeMessagePropertiesFilesByThemeType: Partial<
|
||||||
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
|
Record<
|
||||||
|
ThemeType | "email",
|
||||||
|
(params: { messageDirPath: string; themeName: string }) => void
|
||||||
|
>
|
||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
for (const themeType of THEME_TYPES) {
|
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||||
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
|
let isNative: boolean;
|
||||||
continue;
|
|
||||||
|
{
|
||||||
|
const v = buildContext.implementedThemeTypes[themeType];
|
||||||
|
|
||||||
|
if (!v.isImplemented && !v.isImplemented_native) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNative = !v.isImplemented && v.isImplemented_native;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAccountThemeType = () => {
|
const getAccountThemeType = () => {
|
||||||
@ -101,12 +111,18 @@ export async function generateResources(params: {
|
|||||||
return getAccountThemeType() === "Single-Page";
|
return getAccountThemeType() === "Single-Page";
|
||||||
case "admin":
|
case "admin":
|
||||||
return true;
|
return true;
|
||||||
|
case "email":
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
|
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
|
||||||
|
|
||||||
apply_replacers_and_move_to_theme_resources: {
|
apply_replacers_and_move_to_theme_resources: {
|
||||||
|
if (isNative) {
|
||||||
|
break apply_replacers_and_move_to_theme_resources;
|
||||||
|
}
|
||||||
|
|
||||||
const destDirPath = pathJoin(
|
const destDirPath = pathJoin(
|
||||||
themeTypeDirPath,
|
themeTypeDirPath,
|
||||||
"resources",
|
"resources",
|
||||||
@ -190,59 +206,93 @@ export async function generateResources(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
generate_ftl_files: {
|
||||||
themeName,
|
if (isNative) {
|
||||||
indexHtmlCode: fs
|
break generate_ftl_files;
|
||||||
.readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html"))
|
}
|
||||||
.toString("utf8"),
|
|
||||||
buildContext,
|
|
||||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
|
||||||
themeType,
|
|
||||||
fieldNames: isSpa
|
|
||||||
? []
|
|
||||||
: (assert(themeType !== "admin"),
|
|
||||||
readFieldNameUsage({
|
|
||||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
|
||||||
themeType
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
[
|
assert(themeType !== "email");
|
||||||
...(() => {
|
|
||||||
switch (themeType) {
|
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||||
case "login":
|
themeName,
|
||||||
return LOGIN_THEME_PAGE_IDS;
|
indexHtmlCode: fs
|
||||||
case "account":
|
.readFileSync(
|
||||||
return getAccountThemeType() === "Single-Page"
|
pathJoin(buildContext.projectBuildDirPath, "index.html")
|
||||||
? ["index.ftl"]
|
)
|
||||||
: ACCOUNT_THEME_PAGE_IDS;
|
.toString("utf8"),
|
||||||
case "admin":
|
buildContext,
|
||||||
return ["index.ftl"];
|
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||||
|
themeType,
|
||||||
|
fieldNames: isSpa
|
||||||
|
? []
|
||||||
|
: (assert(themeType !== "admin"),
|
||||||
|
readFieldNameUsage({
|
||||||
|
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||||
|
themeType
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
...(() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return LOGIN_THEME_PAGE_IDS;
|
||||||
|
case "account":
|
||||||
|
return getAccountThemeType() === "Single-Page"
|
||||||
|
? ["index.ftl"]
|
||||||
|
: ACCOUNT_THEME_PAGE_IDS;
|
||||||
|
case "admin":
|
||||||
|
return ["index.ftl"];
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
...(isSpa
|
||||||
|
? []
|
||||||
|
: readExtraPagesNames({
|
||||||
|
themeType,
|
||||||
|
themeSrcDirPath: buildContext.themeSrcDirPath
|
||||||
|
}))
|
||||||
|
].forEach(pageId => {
|
||||||
|
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(themeTypeDirPath, pageId),
|
||||||
|
Buffer.from(ftlCode, "utf8")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_native_theme: {
|
||||||
|
if (!isNative) {
|
||||||
|
break copy_native_theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirPath = pathJoin(buildContext.themeSrcDirPath, themeType);
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
srcDirPath: dirPath,
|
||||||
|
destDirPath: getThemeTypeDirPath({ themeName, themeType }),
|
||||||
|
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||||
|
if (isInside({ dirPath: "messages", filePath: fileRelativePath })) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modifiedSourceCode: sourceCode };
|
||||||
}
|
}
|
||||||
})(),
|
});
|
||||||
...(isSpa
|
}
|
||||||
? []
|
|
||||||
: readExtraPagesNames({
|
|
||||||
themeType,
|
|
||||||
themeSrcDirPath: buildContext.themeSrcDirPath
|
|
||||||
}))
|
|
||||||
].forEach(pageId => {
|
|
||||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(themeTypeDirPath, pageId),
|
|
||||||
Buffer.from(ftlCode, "utf8")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let languageTags: string[] | undefined = undefined;
|
let languageTags: string[] | undefined = undefined;
|
||||||
|
|
||||||
i18n_messages_generation: {
|
i18n_multi_page: {
|
||||||
if (isSpa) {
|
if (isNative) {
|
||||||
break i18n_messages_generation;
|
break i18n_multi_page;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(themeType !== "admin");
|
if (isSpa) {
|
||||||
|
break i18n_multi_page;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(themeType !== "admin" && themeType !== "email");
|
||||||
|
|
||||||
const wrap = generateMessageProperties({
|
const wrap = generateMessageProperties({
|
||||||
buildContext,
|
buildContext,
|
||||||
@ -256,21 +306,43 @@ export async function generateResources(params: {
|
|||||||
writeMessagePropertiesFiles;
|
writeMessagePropertiesFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
bring_in_spas_messages: {
|
let isLegacyAccountSpa = false;
|
||||||
|
|
||||||
|
// NOTE: Eventually remove this block.
|
||||||
|
i18n_single_page_account_legacy: {
|
||||||
if (!isSpa) {
|
if (!isSpa) {
|
||||||
break bring_in_spas_messages;
|
break i18n_single_page_account_legacy;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(themeType !== "login");
|
if (themeType !== "account") {
|
||||||
|
break i18n_single_page_account_legacy;
|
||||||
|
}
|
||||||
|
|
||||||
const accountUiDirPath = child_process
|
const [moduleMeta] = await listInstalledModules({
|
||||||
.execSync(`npm list @keycloakify/keycloak-${themeType}-ui --parseable`, {
|
packageJsonFilePath: buildContext.packageJsonFilePath,
|
||||||
cwd: pathDirname(buildContext.packageJsonFilePath)
|
filter: ({ moduleName }) =>
|
||||||
})
|
moduleName === "@keycloakify/keycloak-account-ui"
|
||||||
.toString("utf8")
|
});
|
||||||
.trim();
|
|
||||||
|
|
||||||
const messageDirPath_defaults = pathJoin(accountUiDirPath, "messages");
|
assert(
|
||||||
|
moduleMeta !== undefined,
|
||||||
|
`@keycloakify/keycloak-account-ui is supposed to be installed`
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const [majorStr] = moduleMeta.version.split(".");
|
||||||
|
|
||||||
|
if (majorStr.length === 6) {
|
||||||
|
// NOTE: Now we use the format MMmmpp (Major, minor, patch) for example for
|
||||||
|
// 26.0.7 it would be 260007.
|
||||||
|
break i18n_single_page_account_legacy;
|
||||||
|
} else {
|
||||||
|
// 25.0.4-rc.5 or later
|
||||||
|
isLegacyAccountSpa = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageDirPath_defaults = pathJoin(moduleMeta.dirPath, "messages");
|
||||||
|
|
||||||
if (!fs.existsSync(messageDirPath_defaults)) {
|
if (!fs.existsSync(messageDirPath_defaults)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -278,8 +350,10 @@ export async function generateResources(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLegacyAccountSpa = true;
|
||||||
|
|
||||||
const messagesDirPath_dest = pathJoin(
|
const messagesDirPath_dest = pathJoin(
|
||||||
getThemeTypeDirPath({ themeName, themeType }),
|
getThemeTypeDirPath({ themeName, themeType: "account" }),
|
||||||
"messages"
|
"messages"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -291,7 +365,7 @@ export async function generateResources(params: {
|
|||||||
apply_theme_changes: {
|
apply_theme_changes: {
|
||||||
const messagesDirPath_theme = pathJoin(
|
const messagesDirPath_theme = pathJoin(
|
||||||
buildContext.themeSrcDirPath,
|
buildContext.themeSrcDirPath,
|
||||||
themeType,
|
"account",
|
||||||
"messages"
|
"messages"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -339,7 +413,167 @@ export async function generateResources(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i18n_for_spas_and_native: {
|
||||||
|
if (!isSpa && !isNative) {
|
||||||
|
break i18n_for_spas_and_native;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLegacyAccountSpa) {
|
||||||
|
break i18n_for_spas_and_native;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesDirPath_theme = pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
themeType,
|
||||||
|
isNative ? "messages" : "i18n"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(messagesDirPath_theme)) {
|
||||||
|
break i18n_for_spas_and_native;
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertiesByLang: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
base: Buffer;
|
||||||
|
override: Buffer | undefined;
|
||||||
|
overrideByThemeName: Record<string, Buffer>;
|
||||||
|
}
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
fs.readdirSync(messagesDirPath_theme).forEach(basename => {
|
||||||
|
type ParsedBasename = { lang: string } & (
|
||||||
|
| {
|
||||||
|
isOverride: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
isOverride: true;
|
||||||
|
themeName: string | undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsedBasename = ((): ParsedBasename | undefined => {
|
||||||
|
const match = basename.match(/^messages_([^.]+)\.properties$/);
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const discriminator = match[1];
|
||||||
|
|
||||||
|
const split = discriminator.split("_override");
|
||||||
|
|
||||||
|
if (split.length === 1) {
|
||||||
|
return {
|
||||||
|
lang: discriminator,
|
||||||
|
isOverride: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(split.length === 2);
|
||||||
|
|
||||||
|
if (split[1] === "") {
|
||||||
|
return {
|
||||||
|
lang: split[0],
|
||||||
|
isOverride: true,
|
||||||
|
themeName: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const match2 = split[1].match(/^_(.+)$/);
|
||||||
|
|
||||||
|
assert(match2 !== null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
lang: split[0],
|
||||||
|
isOverride: true,
|
||||||
|
themeName: match2[1]
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (parsedBasename === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
propertiesByLang[parsedBasename.lang] ??= {
|
||||||
|
base: createObjectThatThrowsIfAccessed<Buffer>({
|
||||||
|
debugMessage: `No base ${parsedBasename.lang} translation for ${themeType} theme`
|
||||||
|
}),
|
||||||
|
override: undefined,
|
||||||
|
overrideByThemeName: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buffer = fs.readFileSync(pathJoin(messagesDirPath_theme, basename));
|
||||||
|
|
||||||
|
if (parsedBasename.isOverride === false) {
|
||||||
|
propertiesByLang[parsedBasename.lang].base = buffer;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedBasename.themeName === undefined) {
|
||||||
|
propertiesByLang[parsedBasename.lang].override = buffer;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
propertiesByLang[parsedBasename.lang].overrideByThemeName[
|
||||||
|
parsedBasename.themeName
|
||||||
|
] = buffer;
|
||||||
|
});
|
||||||
|
|
||||||
|
languageTags = Object.keys(propertiesByLang);
|
||||||
|
|
||||||
|
writeMessagePropertiesFilesByThemeType[themeType] = ({
|
||||||
|
messageDirPath,
|
||||||
|
themeName
|
||||||
|
}) => {
|
||||||
|
if (!fs.existsSync(messageDirPath)) {
|
||||||
|
fs.mkdirSync(messageDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(propertiesByLang).forEach(
|
||||||
|
([lang, { base, override, overrideByThemeName }]) => {
|
||||||
|
const messages = propertiesParser.parse(base.toString("utf8"));
|
||||||
|
|
||||||
|
if (override !== undefined) {
|
||||||
|
const overrideMessages = propertiesParser.parse(
|
||||||
|
override.toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.entries(overrideMessages).forEach(
|
||||||
|
([key, value]) => (messages[key] = value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeName in overrideByThemeName) {
|
||||||
|
const overrideMessages = propertiesParser.parse(
|
||||||
|
overrideByThemeName[themeName].toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.entries(overrideMessages).forEach(
|
||||||
|
([key, value]) => (messages[key] = value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = propertiesParser.createEditor();
|
||||||
|
|
||||||
|
Object.entries(messages).forEach(([key, value]) => {
|
||||||
|
editor.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(messageDirPath, `messages_${lang}.properties`),
|
||||||
|
Buffer.from(editor.toString(), "utf8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
keycloak_static_resources: {
|
keycloak_static_resources: {
|
||||||
|
if (isNative) {
|
||||||
|
break keycloak_static_resources;
|
||||||
|
}
|
||||||
|
|
||||||
if (isSpa) {
|
if (isSpa) {
|
||||||
break keycloak_static_resources;
|
break keycloak_static_resources;
|
||||||
}
|
}
|
||||||
@ -356,183 +590,167 @@ export async function generateResources(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(
|
bring_in_account_v1: {
|
||||||
pathJoin(themeTypeDirPath, "theme.properties"),
|
if (isNative) {
|
||||||
Buffer.from(
|
break bring_in_account_v1;
|
||||||
[
|
|
||||||
`parent=${(() => {
|
|
||||||
switch (themeType) {
|
|
||||||
case "account":
|
|
||||||
switch (getAccountThemeType()) {
|
|
||||||
case "Multi-Page":
|
|
||||||
return "account-v1";
|
|
||||||
case "Single-Page":
|
|
||||||
return "base";
|
|
||||||
}
|
|
||||||
case "login":
|
|
||||||
return "keycloak";
|
|
||||||
case "admin":
|
|
||||||
return "base";
|
|
||||||
}
|
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
|
||||||
})()}`,
|
|
||||||
...(themeType === "account" && getAccountThemeType() === "Single-Page"
|
|
||||||
? ["deprecatedMode=false"]
|
|
||||||
: []),
|
|
||||||
...(buildContext.extraThemeProperties ?? []),
|
|
||||||
...[
|
|
||||||
...buildContext.environmentVariables,
|
|
||||||
{ name: KEYCLOAKIFY_SPA_DEV_SERVER_PORT, default: "" }
|
|
||||||
].map(
|
|
||||||
({ name, default: defaultValue }) =>
|
|
||||||
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
|
|
||||||
),
|
|
||||||
...(languageTags === undefined
|
|
||||||
? []
|
|
||||||
: [`locales=${languageTags.join(",")}`])
|
|
||||||
].join("\n\n"),
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
email: {
|
|
||||||
if (!buildContext.implementedThemeTypes.email.isImplemented) {
|
|
||||||
break email;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: emailThemeSrcDirPath,
|
|
||||||
destDirPath: getThemeTypeDirPath({ themeName, themeType: "email" })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bring_in_account_v1: {
|
|
||||||
if (!buildContext.implementedThemeTypes.account.isImplemented) {
|
|
||||||
break bring_in_account_v1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
|
|
||||||
break bring_in_account_v1;
|
|
||||||
}
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
|
|
||||||
destDirPath: getThemeTypeDirPath({
|
|
||||||
themeName: "account-v1",
|
|
||||||
themeType: "account"
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
|
|
||||||
|
|
||||||
for (const themeName of buildContext.themeNames) {
|
|
||||||
metaInfKeycloakThemes.themes.push({
|
|
||||||
name: themeName,
|
|
||||||
types: objectEntries(buildContext.implementedThemeTypes)
|
|
||||||
.filter(([, { isImplemented }]) => isImplemented)
|
|
||||||
.map(([themeType]) => themeType)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buildContext.implementedThemeTypes.account.isImplemented) {
|
|
||||||
metaInfKeycloakThemes.themes.push({
|
|
||||||
name: "account-v1",
|
|
||||||
types: ["account"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
writeMetaInfKeycloakThemes({
|
|
||||||
resourcesDirPath,
|
|
||||||
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const themeVariantName of buildContext.themeNames) {
|
|
||||||
if (themeVariantName === themeName) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: pathJoin(resourcesDirPath, "theme", themeName),
|
|
||||||
destDirPath: pathJoin(resourcesDirPath, "theme", themeVariantName),
|
|
||||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
|
||||||
if (
|
|
||||||
pathExtname(fileRelativePath) === ".ftl" &&
|
|
||||||
fileRelativePath.split(pathSep).length === 2
|
|
||||||
) {
|
|
||||||
const modifiedSourceCode = Buffer.from(
|
|
||||||
Buffer.from(sourceCode)
|
|
||||||
.toString("utf-8")
|
|
||||||
.replace(
|
|
||||||
`"themeName": "${themeName}"`,
|
|
||||||
`"themeName": "${themeVariantName}"`
|
|
||||||
),
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0);
|
|
||||||
|
|
||||||
return { modifiedSourceCode };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { modifiedSourceCode: sourceCode };
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const themeName of buildContext.themeNames) {
|
if (themeType !== "account") {
|
||||||
for (const [themeType, writeMessagePropertiesFiles] of objectEntries(
|
break bring_in_account_v1;
|
||||||
writeMessagePropertiesFilesByThemeType
|
|
||||||
)) {
|
|
||||||
// NOTE: This is just a quirk of the type system: We can't really differentiate in a record
|
|
||||||
// between the case where the key isn't present and the case where the value is `undefined`.
|
|
||||||
if (writeMessagePropertiesFiles === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
writeMessagePropertiesFiles({
|
|
||||||
messageDirPath: pathJoin(
|
|
||||||
getThemeTypeDirPath({ themeName, themeType }),
|
|
||||||
"messages"
|
|
||||||
),
|
|
||||||
themeName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modify_email_theme_per_variant: {
|
assert(buildContext.implementedThemeTypes.account.isImplemented);
|
||||||
if (!buildContext.implementedThemeTypes.email.isImplemented) {
|
|
||||||
break modify_email_theme_per_variant;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const themeName of buildContext.themeNames) {
|
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
|
||||||
const emailThemeDirPath = getThemeTypeDirPath({
|
break bring_in_account_v1;
|
||||||
themeName,
|
}
|
||||||
themeType: "email"
|
|
||||||
});
|
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
srcDirPath: emailThemeDirPath,
|
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
|
||||||
destDirPath: emailThemeDirPath,
|
destDirPath: getThemeTypeDirPath({
|
||||||
transformSourceCode: ({ filePath, sourceCode }) => {
|
themeName: "account-v1",
|
||||||
if (!filePath.endsWith(".ftl")) {
|
themeType: "account"
|
||||||
return { modifiedSourceCode: sourceCode };
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
modifiedSourceCode: Buffer.from(
|
|
||||||
sourceCode
|
|
||||||
.toString("utf8")
|
|
||||||
.replace(/xKeycloakify\.themeName/g, `"${themeName}"`),
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generate_theme_properties: {
|
||||||
|
if (isNative) {
|
||||||
|
break generate_theme_properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(themeType !== "email");
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(themeTypeDirPath, "theme.properties"),
|
||||||
|
Buffer.from(
|
||||||
|
[
|
||||||
|
`parent=${(() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "account":
|
||||||
|
switch (getAccountThemeType()) {
|
||||||
|
case "Multi-Page":
|
||||||
|
return "account-v1";
|
||||||
|
case "Single-Page":
|
||||||
|
return "base";
|
||||||
|
}
|
||||||
|
case "login":
|
||||||
|
return "keycloak";
|
||||||
|
case "admin":
|
||||||
|
return "base";
|
||||||
|
}
|
||||||
|
assert<Equals<typeof themeType, never>>;
|
||||||
|
})()}`,
|
||||||
|
...(themeType === "account" &&
|
||||||
|
getAccountThemeType() === "Single-Page"
|
||||||
|
? ["deprecatedMode=false"]
|
||||||
|
: []),
|
||||||
|
...(buildContext.extraThemeProperties ?? []),
|
||||||
|
...[
|
||||||
|
...buildContext.environmentVariables,
|
||||||
|
{ name: KEYCLOAKIFY_SPA_DEV_SERVER_PORT, default: "" }
|
||||||
|
].map(
|
||||||
|
({ name, default: defaultValue }) =>
|
||||||
|
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
|
||||||
|
),
|
||||||
|
...(languageTags === undefined
|
||||||
|
? []
|
||||||
|
: [`locales=${languageTags.join(",")}`])
|
||||||
|
].join("\n\n"),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const themeVariantName of [...buildContext.themeNames].reverse()) {
|
||||||
|
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||||
|
copy_main_theme_to_theme_variant_theme: {
|
||||||
|
let isNative: boolean;
|
||||||
|
|
||||||
|
{
|
||||||
|
const v = buildContext.implementedThemeTypes[themeType];
|
||||||
|
|
||||||
|
if (!v.isImplemented && !v.isImplemented_native) {
|
||||||
|
break copy_main_theme_to_theme_variant_theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNative = !v.isImplemented && v.isImplemented_native;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNative && themeVariantName === themeName) {
|
||||||
|
break copy_main_theme_to_theme_variant_theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
srcDirPath: getThemeTypeDirPath({ themeName, themeType }),
|
||||||
|
destDirPath: getThemeTypeDirPath({
|
||||||
|
themeName: themeVariantName,
|
||||||
|
themeType
|
||||||
|
}),
|
||||||
|
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||||
|
patch_xKeycloakify_themeName: {
|
||||||
|
if (!fileRelativePath.endsWith(".ftl")) {
|
||||||
|
break patch_xKeycloakify_themeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isNative &&
|
||||||
|
pathBasename(fileRelativePath) !== fileRelativePath
|
||||||
|
) {
|
||||||
|
break patch_xKeycloakify_themeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiedSourceCode = Buffer.from(
|
||||||
|
Buffer.from(sourceCode)
|
||||||
|
.toString("utf-8")
|
||||||
|
.replace(
|
||||||
|
...id<[string | RegExp, string]>(
|
||||||
|
isNative
|
||||||
|
? [
|
||||||
|
/xKeycloakify\.themeName/g,
|
||||||
|
`"${themeVariantName}"`
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
`"themeName": "${themeName}"`,
|
||||||
|
`"themeName": "${themeVariantName}"`
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isNative) {
|
||||||
|
assert(
|
||||||
|
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modifiedSourceCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modifiedSourceCode: sourceCode };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
run_writeMessagePropertiesFiles: {
|
||||||
|
const writeMessagePropertiesFiles =
|
||||||
|
writeMessagePropertiesFilesByThemeType[themeType];
|
||||||
|
|
||||||
|
if (writeMessagePropertiesFiles === undefined) {
|
||||||
|
break run_writeMessagePropertiesFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeMessagePropertiesFiles({
|
||||||
|
messageDirPath: pathJoin(
|
||||||
|
getThemeTypeDirPath({ themeName: themeVariantName, themeType }),
|
||||||
|
"messages"
|
||||||
|
),
|
||||||
|
themeName: themeVariantName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Generated resources in ${Date.now() - start}ms`);
|
||||||
}
|
}
|
||||||
|
140
src/bin/main.ts
140
src/bin/main.ts
@ -5,9 +5,6 @@ import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
|
|||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
|
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
|
||||||
import { getBuildContext } from "./shared/buildContext";
|
import { getBuildContext } from "./shared/buildContext";
|
||||||
import { SemVer } from "./tools/SemVer";
|
|
||||||
import { assert, is } from "tsafe/assert";
|
|
||||||
import chalk from "chalk";
|
|
||||||
|
|
||||||
type CliCommandOptions = {
|
type CliCommandOptions = {
|
||||||
projectDirPath: string | undefined;
|
projectDirPath: string | undefined;
|
||||||
@ -137,47 +134,11 @@ program
|
|||||||
handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => {
|
handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => {
|
||||||
const { command } = await import("./start-keycloak");
|
const { command } = await import("./start-keycloak");
|
||||||
|
|
||||||
validate_keycloak_version: {
|
|
||||||
if (keycloakVersion === undefined) {
|
|
||||||
break validate_keycloak_version;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValidVersion = (() => {
|
|
||||||
if (typeof keycloakVersion === "number") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
SemVer.parse(keycloakVersion);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (isValidVersion) {
|
|
||||||
break validate_keycloak_version;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
chalk.red(
|
|
||||||
[
|
|
||||||
`Invalid Keycloak version: ${keycloakVersion}`,
|
|
||||||
"It should be a valid semver version example: 26.0.4"
|
|
||||||
].join(" ")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(is<string | undefined>(keycloakVersion));
|
|
||||||
|
|
||||||
await command({
|
await command({
|
||||||
buildContext: getBuildContext({ projectDirPath }),
|
buildContext: getBuildContext({ projectDirPath }),
|
||||||
cliCommandOptions: {
|
cliCommandOptions: {
|
||||||
keycloakVersion,
|
keycloakVersion:
|
||||||
|
keycloakVersion === undefined ? undefined : `${keycloakVersion}`,
|
||||||
port,
|
port,
|
||||||
realmJsonFilePath
|
realmJsonFilePath
|
||||||
}
|
}
|
||||||
@ -230,7 +191,7 @@ program
|
|||||||
program
|
program
|
||||||
.command({
|
.command({
|
||||||
name: "initialize-account-theme",
|
name: "initialize-account-theme",
|
||||||
description: "Initialize the account theme."
|
description: "Initialize an Account Single-Page or Multi-Page custom Account UI."
|
||||||
})
|
})
|
||||||
.task({
|
.task({
|
||||||
skip,
|
skip,
|
||||||
@ -241,6 +202,20 @@ program
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command({
|
||||||
|
name: "initialize-admin-theme",
|
||||||
|
description: "Initialize an Admin Console custom UI."
|
||||||
|
})
|
||||||
|
.task({
|
||||||
|
skip,
|
||||||
|
handler: async ({ projectDirPath }) => {
|
||||||
|
const { command } = await import("./initialize-admin-theme");
|
||||||
|
|
||||||
|
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
.command({
|
.command({
|
||||||
name: "copy-keycloak-resources-to-public",
|
name: "copy-keycloak-resources-to-public",
|
||||||
@ -273,13 +248,30 @@ program
|
|||||||
|
|
||||||
program
|
program
|
||||||
.command({
|
.command({
|
||||||
name: "postinstall",
|
name: "sync-extensions",
|
||||||
description: "Initialize all the Keycloakify UI modules installed in the project."
|
description: [
|
||||||
|
"Synchronizes all installed Keycloakify extension modules with your project.",
|
||||||
|
"",
|
||||||
|
"Example of extension modules: '@keycloakify/keycloak-account-ui', '@keycloakify/keycloak-admin-ui', '@keycloakify/keycloak-ui-shared'",
|
||||||
|
"",
|
||||||
|
"This command ensures that:",
|
||||||
|
"- All required files from installed extensions are copied into your project.",
|
||||||
|
"- The copied files are correctly ignored by Git to help you distinguish between your custom source files",
|
||||||
|
" and those provided by the extensions.",
|
||||||
|
"- Peer dependencies declared by the extensions are automatically added to your package.json.",
|
||||||
|
"",
|
||||||
|
"You can safely run this command multiple times. It will only update the files and dependencies if needed,",
|
||||||
|
"ensuring your project stays in sync with the installed extensions.",
|
||||||
|
"",
|
||||||
|
"Typical usage:",
|
||||||
|
"- Should be run as a postinstall script of your project.",
|
||||||
|
""
|
||||||
|
].join("\n")
|
||||||
})
|
})
|
||||||
.task({
|
.task({
|
||||||
skip,
|
skip,
|
||||||
handler: async ({ projectDirPath }) => {
|
handler: async ({ projectDirPath }) => {
|
||||||
const { command } = await import("./postinstall");
|
const { command } = await import("./sync-extensions");
|
||||||
|
|
||||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||||
}
|
}
|
||||||
@ -287,36 +279,66 @@ program
|
|||||||
|
|
||||||
program
|
program
|
||||||
.command<{
|
.command<{
|
||||||
file: string;
|
path: string;
|
||||||
|
revert: boolean;
|
||||||
}>({
|
}>({
|
||||||
name: "eject-file",
|
name: "own",
|
||||||
description: [
|
description: [
|
||||||
"WARNING: Not usable yet, will be used for future features",
|
"Manages ownership of auto-generated files provided by Keycloakify extensions.",
|
||||||
"Take ownership over a given file"
|
"",
|
||||||
|
"This command allows you to take ownership of a specific file or directory generated",
|
||||||
|
"by an extension. Once owned, you can freely modify and version-control the file.",
|
||||||
|
"",
|
||||||
|
"You can also use the --revert flag to relinquish ownership and restore the file",
|
||||||
|
"or directory to its original auto-generated state.",
|
||||||
|
"",
|
||||||
|
"For convenience, the exact command to take ownership of any file is included as a comment",
|
||||||
|
"in the header of each extension-generated file.",
|
||||||
|
"",
|
||||||
|
"Examples:",
|
||||||
|
"$ npx keycloakify own --path admin/KcPage.tsx"
|
||||||
|
].join("\n")
|
||||||
|
})
|
||||||
|
.option({
|
||||||
|
key: "path",
|
||||||
|
name: (() => {
|
||||||
|
const long = "path";
|
||||||
|
const short = "t";
|
||||||
|
|
||||||
|
optionsKeys.push(long, short);
|
||||||
|
|
||||||
|
return { long, short };
|
||||||
|
})(),
|
||||||
|
description: [
|
||||||
|
"Specifies the relative path of the file or directory to take ownership of.",
|
||||||
|
"This path should be relative to your theme directory.",
|
||||||
|
"Example: `--path 'admin/KcPage.tsx'`"
|
||||||
].join(" ")
|
].join(" ")
|
||||||
})
|
})
|
||||||
.option({
|
.option({
|
||||||
key: "file",
|
key: "revert",
|
||||||
name: (() => {
|
name: (() => {
|
||||||
const name = "file";
|
const long = "revert";
|
||||||
|
const short = "r";
|
||||||
|
|
||||||
optionsKeys.push(name);
|
optionsKeys.push(long, short);
|
||||||
|
|
||||||
return name;
|
return { long, short };
|
||||||
})(),
|
})(),
|
||||||
description: [
|
description: [
|
||||||
"Relative path of the file relative to the directory of your keycloak theme source",
|
"Restores a file or directory to its original auto-generated state,",
|
||||||
"Example `--file src/login/page/Login.tsx`"
|
"removing your ownership claim and reverting any modifications."
|
||||||
].join(" ")
|
].join(" "),
|
||||||
|
defaultValue: false
|
||||||
})
|
})
|
||||||
.task({
|
.task({
|
||||||
skip,
|
skip,
|
||||||
handler: async ({ projectDirPath, file }) => {
|
handler: async ({ projectDirPath, path, revert }) => {
|
||||||
const { command } = await import("./eject-file");
|
const { command } = await import("./own");
|
||||||
|
|
||||||
await command({
|
await command({
|
||||||
buildContext: getBuildContext({ projectDirPath }),
|
buildContext: getBuildContext({ projectDirPath }),
|
||||||
cliCommandOptions: { file }
|
cliCommandOptions: { path, isRevert: revert }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
208
src/bin/own.ts
Normal file
208
src/bin/own.ts
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import type { BuildContext } from "./shared/buildContext";
|
||||||
|
import { getExtensionModuleFileSourceCodeReadyToBeCopied } from "./sync-extensions/getExtensionModuleFileSourceCodeReadyToBeCopied";
|
||||||
|
import type { ExtensionModuleMeta } from "./sync-extensions/extensionModuleMeta";
|
||||||
|
import { command as command_syncExtensions } from "./sync-extensions/sync-extension";
|
||||||
|
import {
|
||||||
|
readManagedGitignoreFile,
|
||||||
|
writeManagedGitignoreFile
|
||||||
|
} from "./sync-extensions/managedGitignoreFile";
|
||||||
|
import { getExtensionModuleMetas } from "./sync-extensions/extensionModuleMeta";
|
||||||
|
import { getAbsoluteAndInOsFormatPath } from "./tools/getAbsoluteAndInOsFormatPath";
|
||||||
|
import { relative as pathRelative, dirname as pathDirname, join as pathJoin } from "path";
|
||||||
|
import { getInstalledModuleDirPath } from "./tools/getInstalledModuleDirPath";
|
||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import { isInside } from "./tools/isInside";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
export async function command(params: {
|
||||||
|
buildContext: BuildContext;
|
||||||
|
cliCommandOptions: {
|
||||||
|
path: string;
|
||||||
|
isRevert: boolean;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
const { buildContext, cliCommandOptions } = params;
|
||||||
|
|
||||||
|
const extensionModuleMetas = await getExtensionModuleMetas({ buildContext });
|
||||||
|
|
||||||
|
const { targetFileRelativePathsByExtensionModuleMeta } = await (async () => {
|
||||||
|
const fileOrDirectoryRelativePath = pathRelative(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
getAbsoluteAndInOsFormatPath({
|
||||||
|
cwd: buildContext.themeSrcDirPath,
|
||||||
|
pathIsh: cliCommandOptions.path
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const arr = extensionModuleMetas
|
||||||
|
.map(extensionModuleMeta => ({
|
||||||
|
extensionModuleMeta,
|
||||||
|
fileRelativePaths: extensionModuleMeta.files
|
||||||
|
.map(({ fileRelativePath }) => fileRelativePath)
|
||||||
|
.filter(
|
||||||
|
fileRelativePath =>
|
||||||
|
fileRelativePath === fileOrDirectoryRelativePath ||
|
||||||
|
isInside({
|
||||||
|
dirPath: fileOrDirectoryRelativePath,
|
||||||
|
filePath: fileRelativePath
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
.filter(({ fileRelativePaths }) => fileRelativePaths.length !== 0);
|
||||||
|
|
||||||
|
const targetFileRelativePathsByExtensionModuleMeta = new Map<
|
||||||
|
ExtensionModuleMeta,
|
||||||
|
string[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
for (const { extensionModuleMeta, fileRelativePaths } of arr) {
|
||||||
|
targetFileRelativePathsByExtensionModuleMeta.set(
|
||||||
|
extensionModuleMeta,
|
||||||
|
fileRelativePaths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { targetFileRelativePathsByExtensionModuleMeta };
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (targetFileRelativePathsByExtensionModuleMeta.size === 0) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
"There is no Keycloakify extension modules files matching the provided path."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ownedFilesRelativePaths: ownedFilesRelativePaths_current } =
|
||||||
|
await readManagedGitignoreFile({
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
await (cliCommandOptions.isRevert ? command_revert : command_own)({
|
||||||
|
extensionModuleMetas,
|
||||||
|
targetFileRelativePathsByExtensionModuleMeta,
|
||||||
|
ownedFilesRelativePaths_current,
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type Params_subcommands = {
|
||||||
|
extensionModuleMetas: ExtensionModuleMeta[];
|
||||||
|
targetFileRelativePathsByExtensionModuleMeta: Map<ExtensionModuleMeta, string[]>;
|
||||||
|
ownedFilesRelativePaths_current: string[];
|
||||||
|
buildContext: BuildContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function command_own(params: Params_subcommands) {
|
||||||
|
const {
|
||||||
|
extensionModuleMetas,
|
||||||
|
targetFileRelativePathsByExtensionModuleMeta,
|
||||||
|
ownedFilesRelativePaths_current,
|
||||||
|
buildContext
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
await writeManagedGitignoreFile({
|
||||||
|
buildContext,
|
||||||
|
extensionModuleMetas,
|
||||||
|
ownedFilesRelativePaths: [
|
||||||
|
...ownedFilesRelativePaths_current,
|
||||||
|
...Array.from(targetFileRelativePathsByExtensionModuleMeta.values())
|
||||||
|
.flat()
|
||||||
|
.filter(
|
||||||
|
fileRelativePath =>
|
||||||
|
!ownedFilesRelativePaths_current.includes(fileRelativePath)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const writeActions: (() => Promise<void>)[] = [];
|
||||||
|
|
||||||
|
for (const [
|
||||||
|
extensionModuleMeta,
|
||||||
|
fileRelativePaths
|
||||||
|
] of targetFileRelativePathsByExtensionModuleMeta.entries()) {
|
||||||
|
const extensionModuleDirPath = await getInstalledModuleDirPath({
|
||||||
|
moduleName: extensionModuleMeta.moduleName,
|
||||||
|
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const fileRelativePath of fileRelativePaths) {
|
||||||
|
if (ownedFilesRelativePaths_current.includes(fileRelativePath)) {
|
||||||
|
console.log(
|
||||||
|
chalk.grey(`You already have ownership over '${fileRelativePath}'.`)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeActions.push(async () => {
|
||||||
|
const sourceCode = await getExtensionModuleFileSourceCodeReadyToBeCopied({
|
||||||
|
buildContext,
|
||||||
|
fileRelativePath,
|
||||||
|
isOwnershipAction: true,
|
||||||
|
extensionModuleName: extensionModuleMeta.moduleName,
|
||||||
|
extensionModuleDirPath,
|
||||||
|
extensionModuleVersion: extensionModuleMeta.version
|
||||||
|
});
|
||||||
|
|
||||||
|
await fsPr.writeFile(
|
||||||
|
pathJoin(buildContext.themeSrcDirPath, fileRelativePath),
|
||||||
|
sourceCode
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(chalk.green(`Ownership over '${fileRelativePath}' claimed.`));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeActions.length === 0) {
|
||||||
|
console.log(chalk.yellow("No new file claimed."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(writeActions.map(action => action()));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function command_revert(params: Params_subcommands) {
|
||||||
|
const {
|
||||||
|
extensionModuleMetas,
|
||||||
|
targetFileRelativePathsByExtensionModuleMeta,
|
||||||
|
ownedFilesRelativePaths_current,
|
||||||
|
buildContext
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const ownedFilesRelativePaths_toRemove = Array.from(
|
||||||
|
targetFileRelativePathsByExtensionModuleMeta.values()
|
||||||
|
)
|
||||||
|
.flat()
|
||||||
|
.filter(fileRelativePath => {
|
||||||
|
if (!ownedFilesRelativePaths_current.includes(fileRelativePath)) {
|
||||||
|
console.log(
|
||||||
|
chalk.grey(`Ownership over '${fileRelativePath}' wasn't claimed.`)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
chalk.green(`Ownership over '${fileRelativePath}' relinquished.`)
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ownedFilesRelativePaths_toRemove.length === 0) {
|
||||||
|
console.log(chalk.yellow("No file relinquished."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeManagedGitignoreFile({
|
||||||
|
buildContext,
|
||||||
|
extensionModuleMetas,
|
||||||
|
ownedFilesRelativePaths: ownedFilesRelativePaths_current.filter(
|
||||||
|
fileRelativePath =>
|
||||||
|
!ownedFilesRelativePaths_toRemove.includes(fileRelativePath)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
await command_syncExtensions({ buildContext });
|
||||||
|
}
|
@ -1,82 +0,0 @@
|
|||||||
import { getIsPrettierAvailable, runPrettier } from "../tools/runPrettier";
|
|
||||||
import * as fsPr from "fs/promises";
|
|
||||||
import { join as pathJoin, sep as pathSep } from "path";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import type { BuildContext } from "../shared/buildContext";
|
|
||||||
import { KEYCLOAK_THEME } from "../shared/constants";
|
|
||||||
|
|
||||||
export type BuildContextLike = {
|
|
||||||
themeSrcDirPath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
|
||||||
|
|
||||||
export async function getUiModuleFileSourceCodeReadyToBeCopied(params: {
|
|
||||||
buildContext: BuildContextLike;
|
|
||||||
fileRelativePath: string;
|
|
||||||
isForEjection: boolean;
|
|
||||||
uiModuleDirPath: string;
|
|
||||||
uiModuleName: string;
|
|
||||||
uiModuleVersion: string;
|
|
||||||
}): Promise<Buffer> {
|
|
||||||
const {
|
|
||||||
buildContext,
|
|
||||||
uiModuleDirPath,
|
|
||||||
fileRelativePath,
|
|
||||||
isForEjection,
|
|
||||||
uiModuleName,
|
|
||||||
uiModuleVersion
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
let sourceCode = (
|
|
||||||
await fsPr.readFile(pathJoin(uiModuleDirPath, KEYCLOAK_THEME, fileRelativePath))
|
|
||||||
).toString("utf8");
|
|
||||||
|
|
||||||
const toComment = (lines: string[]) => {
|
|
||||||
for (const ext of [".ts", ".tsx", ".css", ".less", ".sass", ".js", ".jsx"]) {
|
|
||||||
if (!fileRelativePath.endsWith(ext)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [`/**`, ...lines.map(line => ` * ${line}`), ` */`].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileRelativePath.endsWith(".html")) {
|
|
||||||
return [`<!--`, ...lines.map(line => ` ${line}`), `-->`].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const comment = toComment(
|
|
||||||
isForEjection
|
|
||||||
? [`This file was ejected from ${uiModuleName} version ${uiModuleVersion}.`]
|
|
||||||
: [
|
|
||||||
`WARNING: Before modifying this file run the following command:`,
|
|
||||||
``,
|
|
||||||
`$ npx keycloakify eject-file --file ${fileRelativePath.split(pathSep).join("/")}`,
|
|
||||||
``,
|
|
||||||
`This file comes from ${uiModuleName} version ${uiModuleVersion}.`,
|
|
||||||
`This file has been copied over to your repo by your postinstall script: \`npx keycloakify postinstall\``
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (comment !== undefined) {
|
|
||||||
sourceCode = [comment, ``, sourceCode].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
const destFilePath = pathJoin(buildContext.themeSrcDirPath, fileRelativePath);
|
|
||||||
|
|
||||||
format: {
|
|
||||||
if (!(await getIsPrettierAvailable())) {
|
|
||||||
break format;
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceCode = await runPrettier({
|
|
||||||
filePath: destFilePath,
|
|
||||||
sourceCode
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Buffer.from(sourceCode, "utf8");
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export * from "./postinstall";
|
|
70
src/bin/shared/addSyncExtensionsToPostinstallScript.ts
Normal file
70
src/bin/shared/addSyncExtensionsToPostinstallScript.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { dirname as pathDirname, relative as pathRelative, sep as pathSep } from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { BuildContext } from "./buildContext";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
projectDirPath: string;
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export function addSyncExtensionsToPostinstallScript(params: {
|
||||||
|
parsedPackageJson: { scripts?: Record<string, string | undefined> };
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}) {
|
||||||
|
const { parsedPackageJson, buildContext } = params;
|
||||||
|
|
||||||
|
const cmd_base = "keycloakify sync-extensions";
|
||||||
|
|
||||||
|
const projectCliOptionValue = (() => {
|
||||||
|
const packageJsonDirPath = pathDirname(buildContext.packageJsonFilePath);
|
||||||
|
|
||||||
|
const relativePath = pathRelative(
|
||||||
|
packageJsonDirPath,
|
||||||
|
buildContext.projectDirPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (relativePath === "") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return relativePath.split(pathSep).join("/");
|
||||||
|
})();
|
||||||
|
|
||||||
|
const generateCmd = (params: { cmd_preexisting: string | undefined }) => {
|
||||||
|
const { cmd_preexisting } = params;
|
||||||
|
|
||||||
|
let cmd = cmd_preexisting === undefined ? "" : `${cmd_preexisting} && `;
|
||||||
|
|
||||||
|
cmd += cmd_base;
|
||||||
|
|
||||||
|
if (projectCliOptionValue !== undefined) {
|
||||||
|
cmd += ` -p ${projectCliOptionValue}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const scripts = (parsedPackageJson.scripts ??= {});
|
||||||
|
|
||||||
|
for (const scriptName of ["postinstall", "prepare"]) {
|
||||||
|
const cmd_preexisting = scripts[scriptName];
|
||||||
|
|
||||||
|
if (cmd_preexisting === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cmd_preexisting.includes(cmd_base)) {
|
||||||
|
scripts[scriptName] = generateCmd({ cmd_preexisting });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedPackageJson.scripts = {
|
||||||
|
postinstall: generateCmd({ cmd_preexisting: undefined }),
|
||||||
|
...parsedPackageJson.scripts
|
||||||
|
};
|
||||||
|
}
|
@ -45,12 +45,16 @@ export type BuildContext = {
|
|||||||
environmentVariables: { name: string; default: string }[];
|
environmentVariables: { name: string; default: string }[];
|
||||||
themeSrcDirPath: string;
|
themeSrcDirPath: string;
|
||||||
implementedThemeTypes: {
|
implementedThemeTypes: {
|
||||||
login: { isImplemented: boolean };
|
login:
|
||||||
email: { isImplemented: boolean };
|
| { isImplemented: true }
|
||||||
|
| { isImplemented: false; isImplemented_native: boolean };
|
||||||
|
email: { isImplemented: false; isImplemented_native: boolean };
|
||||||
account:
|
account:
|
||||||
| { isImplemented: false }
|
| { isImplemented: false; isImplemented_native: boolean }
|
||||||
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
|
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
|
||||||
admin: { isImplemented: boolean };
|
admin:
|
||||||
|
| { isImplemented: true }
|
||||||
|
| { isImplemented: false; isImplemented_native: boolean };
|
||||||
};
|
};
|
||||||
packageJsonFilePath: string;
|
packageJsonFilePath: string;
|
||||||
bundler: "vite" | "webpack";
|
bundler: "vite" | "webpack";
|
||||||
@ -434,27 +438,68 @@ export function getBuildContext(params: {
|
|||||||
assert<Equals<typeof bundler, never>>(false);
|
assert<Equals<typeof bundler, never>>(false);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const implementedThemeTypes: BuildContext["implementedThemeTypes"] = {
|
const implementedThemeTypes: BuildContext["implementedThemeTypes"] = (() => {
|
||||||
login: {
|
const getIsNative = (dirPath: string) =>
|
||||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "login"))
|
fs.existsSync(pathJoin(dirPath, "theme.properties"));
|
||||||
},
|
|
||||||
email: {
|
|
||||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "email"))
|
|
||||||
},
|
|
||||||
account: (() => {
|
|
||||||
if (buildOptions.accountThemeImplementation === "none") {
|
|
||||||
return { isImplemented: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isImplemented: true,
|
login: (() => {
|
||||||
type: buildOptions.accountThemeImplementation
|
const dirPath = pathJoin(themeSrcDirPath, "login");
|
||||||
};
|
|
||||||
})(),
|
if (!fs.existsSync(dirPath)) {
|
||||||
admin: {
|
return { isImplemented: false, isImplemented_native: false };
|
||||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "admin"))
|
}
|
||||||
}
|
|
||||||
};
|
if (getIsNative(dirPath)) {
|
||||||
|
return { isImplemented: false, isImplemented_native: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isImplemented: true };
|
||||||
|
})(),
|
||||||
|
email: (() => {
|
||||||
|
const dirPath = pathJoin(themeSrcDirPath, "email");
|
||||||
|
|
||||||
|
if (!fs.existsSync(dirPath) || !getIsNative(dirPath)) {
|
||||||
|
return { isImplemented: false, isImplemented_native: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isImplemented: false, isImplemented_native: true };
|
||||||
|
})(),
|
||||||
|
account: (() => {
|
||||||
|
const dirPath = pathJoin(themeSrcDirPath, "account");
|
||||||
|
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
return { isImplemented: false, isImplemented_native: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getIsNative(dirPath)) {
|
||||||
|
return { isImplemented: false, isImplemented_native: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildOptions.accountThemeImplementation === "none") {
|
||||||
|
return { isImplemented: false, isImplemented_native: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isImplemented: true,
|
||||||
|
type: buildOptions.accountThemeImplementation
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
admin: (() => {
|
||||||
|
const dirPath = pathJoin(themeSrcDirPath, "admin");
|
||||||
|
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
return { isImplemented: false, isImplemented_native: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getIsNative(dirPath)) {
|
||||||
|
return { isImplemented: false, isImplemented_native: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isImplemented: true };
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
implementedThemeTypes.account.isImplemented &&
|
implementedThemeTypes.account.isImplemented &&
|
||||||
|
@ -12,6 +12,7 @@ export type CommandName =
|
|||||||
| "add-story"
|
| "add-story"
|
||||||
| "initialize-account-theme"
|
| "initialize-account-theme"
|
||||||
| "initialize-admin-theme"
|
| "initialize-admin-theme"
|
||||||
|
| "initialize-admin-theme"
|
||||||
| "initialize-email-theme"
|
| "initialize-email-theme"
|
||||||
| "copy-keycloak-resources-to-public";
|
| "copy-keycloak-resources-to-public";
|
||||||
|
|
||||||
|
@ -13,13 +13,15 @@ import * as fs from "fs";
|
|||||||
|
|
||||||
assert<Equals<ApiVersion, "v1">>();
|
assert<Equals<ApiVersion, "v1">>();
|
||||||
|
|
||||||
export function maybeDelegateCommandToCustomHandler(params: {
|
export async function maybeDelegateCommandToCustomHandler(params: {
|
||||||
commandName: CommandName;
|
commandName: CommandName;
|
||||||
buildContext: BuildContext;
|
buildContext: BuildContext;
|
||||||
}): { hasBeenHandled: boolean } {
|
}): Promise<{ hasBeenHandled: boolean }> {
|
||||||
const { commandName, buildContext } = params;
|
const { commandName, buildContext } = params;
|
||||||
|
|
||||||
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
|
const nodeModulesBinDirPath = await getNodeModulesBinDirPath({
|
||||||
|
packageJsonFilePath: buildContext.packageJsonFilePath
|
||||||
|
});
|
||||||
|
|
||||||
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
|
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
|
||||||
return { hasBeenHandled: false };
|
return { hasBeenHandled: false };
|
||||||
|
@ -1,201 +0,0 @@
|
|||||||
import { getLatestsSemVersionedTagFactory } from "../tools/octokit-addons/getLatestsSemVersionedTag";
|
|
||||||
import { Octokit } from "@octokit/rest";
|
|
||||||
import type { ReturnType } from "tsafe";
|
|
||||||
import type { Param0 } from "tsafe";
|
|
||||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
|
||||||
import { id } from "tsafe/id";
|
|
||||||
import type { SemVer } from "../tools/SemVer";
|
|
||||||
import { same } from "evt/tools/inDepth/same";
|
|
||||||
import type { BuildContext } from "./buildContext";
|
|
||||||
import fetch from "make-fetch-happen";
|
|
||||||
|
|
||||||
type GetLatestsSemVersionedTag = ReturnType<
|
|
||||||
typeof getLatestsSemVersionedTagFactory
|
|
||||||
>["getLatestsSemVersionedTag"];
|
|
||||||
|
|
||||||
type Params = Param0<GetLatestsSemVersionedTag>;
|
|
||||||
type R = ReturnType<GetLatestsSemVersionedTag>;
|
|
||||||
|
|
||||||
let getLatestsSemVersionedTag_stateless: GetLatestsSemVersionedTag | undefined =
|
|
||||||
undefined;
|
|
||||||
|
|
||||||
const CACHE_VERSION = 1;
|
|
||||||
|
|
||||||
type Cache = {
|
|
||||||
version: typeof CACHE_VERSION;
|
|
||||||
entries: {
|
|
||||||
time: number;
|
|
||||||
params: Params;
|
|
||||||
result: R;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BuildContextLike = {
|
|
||||||
cacheDirPath: string;
|
|
||||||
fetchOptions: BuildContext["fetchOptions"];
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
|
||||||
|
|
||||||
export async function getLatestsSemVersionedTag({
|
|
||||||
buildContext,
|
|
||||||
...params
|
|
||||||
}: Params & {
|
|
||||||
buildContext: BuildContextLike;
|
|
||||||
}): Promise<R> {
|
|
||||||
const cacheFilePath = pathJoin(
|
|
||||||
buildContext.cacheDirPath,
|
|
||||||
"latest-sem-versioned-tags.json"
|
|
||||||
);
|
|
||||||
|
|
||||||
const cacheLookupResult = (() => {
|
|
||||||
const getResult_currentCache = (currentCacheEntries: Cache["entries"]) => ({
|
|
||||||
hasCachedResult: false as const,
|
|
||||||
currentCache: {
|
|
||||||
version: CACHE_VERSION,
|
|
||||||
entries: currentCacheEntries
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!fs.existsSync(cacheFilePath)) {
|
|
||||||
return getResult_currentCache([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let cache_json;
|
|
||||||
|
|
||||||
try {
|
|
||||||
cache_json = fs.readFileSync(cacheFilePath).toString("utf8");
|
|
||||||
} catch {
|
|
||||||
return getResult_currentCache([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let cache_json_parsed: unknown;
|
|
||||||
|
|
||||||
try {
|
|
||||||
cache_json_parsed = JSON.parse(cache_json);
|
|
||||||
} catch {
|
|
||||||
return getResult_currentCache([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const zSemVer = (() => {
|
|
||||||
type TargetType = SemVer;
|
|
||||||
|
|
||||||
const zTargetType = z.object({
|
|
||||||
major: z.number(),
|
|
||||||
minor: z.number(),
|
|
||||||
patch: z.number(),
|
|
||||||
rc: z.number().optional(),
|
|
||||||
parsedFrom: z.string()
|
|
||||||
});
|
|
||||||
|
|
||||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
|
||||||
|
|
||||||
return id<z.ZodType<TargetType>>(zTargetType);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const zCache = (() => {
|
|
||||||
type TargetType = Cache;
|
|
||||||
|
|
||||||
const zTargetType = z.object({
|
|
||||||
version: z.literal(CACHE_VERSION),
|
|
||||||
entries: z.array(
|
|
||||||
z.object({
|
|
||||||
time: z.number(),
|
|
||||||
params: z.object({
|
|
||||||
owner: z.string(),
|
|
||||||
repo: z.string(),
|
|
||||||
count: z.number(),
|
|
||||||
doIgnoreReleaseCandidates: z.boolean()
|
|
||||||
}),
|
|
||||||
result: z.array(
|
|
||||||
z.object({
|
|
||||||
tag: z.string(),
|
|
||||||
version: zSemVer
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
|
||||||
|
|
||||||
return id<z.ZodType<TargetType>>(zTargetType);
|
|
||||||
})();
|
|
||||||
|
|
||||||
let cache: Cache;
|
|
||||||
|
|
||||||
try {
|
|
||||||
cache = zCache.parse(cache_json_parsed);
|
|
||||||
} catch {
|
|
||||||
return getResult_currentCache([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheEntry = cache.entries.find(e => same(e.params, params));
|
|
||||||
|
|
||||||
if (cacheEntry === undefined) {
|
|
||||||
return getResult_currentCache(cache.entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Date.now() - cacheEntry.time > 3_600_000) {
|
|
||||||
return getResult_currentCache(cache.entries.filter(e => e !== cacheEntry));
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
hasCachedResult: true as const,
|
|
||||||
cachedResult: cacheEntry.result
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (cacheLookupResult.hasCachedResult) {
|
|
||||||
return cacheLookupResult.cachedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { currentCache } = cacheLookupResult;
|
|
||||||
|
|
||||||
getLatestsSemVersionedTag_stateless ??= (() => {
|
|
||||||
const octokit = (() => {
|
|
||||||
const githubToken = process.env.GITHUB_TOKEN;
|
|
||||||
|
|
||||||
const octokit = new Octokit({
|
|
||||||
...(githubToken === undefined ? {} : { auth: githubToken }),
|
|
||||||
request: {
|
|
||||||
fetch: (url: string, options?: any) =>
|
|
||||||
fetch(url, {
|
|
||||||
...options,
|
|
||||||
...buildContext.fetchOptions
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return octokit;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({
|
|
||||||
octokit
|
|
||||||
});
|
|
||||||
|
|
||||||
return getLatestsSemVersionedTag;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const result = await getLatestsSemVersionedTag_stateless(params);
|
|
||||||
|
|
||||||
currentCache.entries.push({
|
|
||||||
time: Date.now(),
|
|
||||||
params,
|
|
||||||
result
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
const dirPath = pathDirname(cacheFilePath);
|
|
||||||
|
|
||||||
if (!fs.existsSync(dirPath)) {
|
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(cacheFilePath, JSON.stringify(currentCache, null, 2));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
156
src/bin/shared/initializeSpa.ts
Normal file
156
src/bin/shared/initializeSpa.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path";
|
||||||
|
import type { BuildContext } from "./buildContext";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { assert, is, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import {
|
||||||
|
addSyncExtensionsToPostinstallScript,
|
||||||
|
type BuildContextLike as BuildContextLike_addSyncExtensionsToPostinstallScript
|
||||||
|
} from "./addSyncExtensionsToPostinstallScript";
|
||||||
|
import { getIsPrettierAvailable, runPrettier } from "../tools/runPrettier";
|
||||||
|
import { npmInstall } from "../tools/npmInstall";
|
||||||
|
import * as child_process from "child_process";
|
||||||
|
import { z } from "zod";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
export type BuildContextLike = BuildContextLike_addSyncExtensionsToPostinstallScript & {
|
||||||
|
themeSrcDirPath: string;
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export async function initializeSpa(params: {
|
||||||
|
themeType: "account" | "admin";
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}) {
|
||||||
|
const { themeType, buildContext } = params;
|
||||||
|
|
||||||
|
{
|
||||||
|
const themeTypeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, themeType);
|
||||||
|
|
||||||
|
if (
|
||||||
|
fs.existsSync(themeTypeSrcDirPath) &&
|
||||||
|
fs.readdirSync(themeTypeSrcDirPath).length > 0
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
chalk.red(
|
||||||
|
`There is already a ${pathRelative(
|
||||||
|
process.cwd(),
|
||||||
|
themeTypeSrcDirPath
|
||||||
|
)} directory in your project. Aborting.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPackageJson = (() => {
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
scripts?: Record<string, string | undefined>;
|
||||||
|
dependencies?: Record<string, string | undefined>;
|
||||||
|
devDependencies?: Record<string, string | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = (() => {
|
||||||
|
type TargetType = ParsedPackageJson;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
scripts: z.record(z.union([z.string(), z.undefined()])).optional(),
|
||||||
|
dependencies: z.record(z.union([z.string(), z.undefined()])).optional(),
|
||||||
|
devDependencies: z.record(z.union([z.string(), z.undefined()])).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
const parsedPackageJson = JSON.parse(
|
||||||
|
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
|
|
||||||
|
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||||
|
|
||||||
|
return parsedPackageJson;
|
||||||
|
})();
|
||||||
|
|
||||||
|
addSyncExtensionsToPostinstallScript({
|
||||||
|
parsedPackageJson,
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
const uiSharedMajor = (() => {
|
||||||
|
const dependencies = {
|
||||||
|
...parsedPackageJson.devDependencies,
|
||||||
|
...parsedPackageJson.dependencies
|
||||||
|
};
|
||||||
|
|
||||||
|
const version = dependencies["@keycloakify/keycloak-ui-shared"];
|
||||||
|
|
||||||
|
if (version === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = version.match(/^[^~]?(\d+)\./);
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match[1];
|
||||||
|
})();
|
||||||
|
|
||||||
|
const moduleName = `@keycloakify/keycloak-${themeType}-ui`;
|
||||||
|
|
||||||
|
const version = ((): string[] => {
|
||||||
|
const cmdOutput = child_process
|
||||||
|
.execSync(`npm show ${moduleName} versions --json`)
|
||||||
|
.toString("utf8")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
const versions = JSON.parse(cmdOutput) as string | string[];
|
||||||
|
|
||||||
|
// NOTE: Bug in some older npm versions
|
||||||
|
if (typeof versions === "string") {
|
||||||
|
return [versions];
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
})()
|
||||||
|
.reverse()
|
||||||
|
.filter(version => !version.includes("-"))
|
||||||
|
.find(version =>
|
||||||
|
uiSharedMajor === undefined ? true : version.startsWith(`${uiSharedMajor}.`)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(version !== undefined);
|
||||||
|
|
||||||
|
(parsedPackageJson.dependencies ??= {})[moduleName] = `~${version}`;
|
||||||
|
|
||||||
|
if (parsedPackageJson.devDependencies !== undefined) {
|
||||||
|
delete parsedPackageJson.devDependencies[moduleName];
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let sourceCode = JSON.stringify(parsedPackageJson, undefined, 2);
|
||||||
|
|
||||||
|
if (await getIsPrettierAvailable()) {
|
||||||
|
sourceCode = await runPrettier({
|
||||||
|
sourceCode,
|
||||||
|
filePath: buildContext.packageJsonFilePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
buildContext.packageJsonFilePath,
|
||||||
|
Buffer.from(sourceCode, "utf8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await npmInstall({
|
||||||
|
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||||
|
});
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
|
||||||
import type { ThemeType } from "./constants";
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
export type MetaInfKeycloakTheme = {
|
|
||||||
themes: { name: string; types: (ThemeType | "email")[] }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function writeMetaInfKeycloakThemes(params: {
|
|
||||||
resourcesDirPath: string;
|
|
||||||
getNewMetaInfKeycloakTheme: (params: {
|
|
||||||
metaInfKeycloakTheme: MetaInfKeycloakTheme | undefined;
|
|
||||||
}) => MetaInfKeycloakTheme;
|
|
||||||
}) {
|
|
||||||
const { resourcesDirPath, getNewMetaInfKeycloakTheme } = params;
|
|
||||||
|
|
||||||
const filePath = pathJoin(resourcesDirPath, "META-INF", "keycloak-themes.json");
|
|
||||||
|
|
||||||
const currentMetaInfKeycloakTheme = !fs.existsSync(filePath)
|
|
||||||
? undefined
|
|
||||||
: (JSON.parse(
|
|
||||||
fs.readFileSync(filePath).toString("utf8")
|
|
||||||
) as MetaInfKeycloakTheme);
|
|
||||||
|
|
||||||
const newMetaInfKeycloakThemes = getNewMetaInfKeycloakTheme({
|
|
||||||
metaInfKeycloakTheme: currentMetaInfKeycloakTheme
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
const dirPath = pathDirname(filePath);
|
|
||||||
if (!fs.existsSync(dirPath)) {
|
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
filePath,
|
|
||||||
Buffer.from(JSON.stringify(newMetaInfKeycloakThemes, null, 2), "utf8")
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
import {
|
|
||||||
getLatestsSemVersionedTag,
|
|
||||||
type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag
|
|
||||||
} from "./getLatestsSemVersionedTag";
|
|
||||||
import cliSelect from "cli-select";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import { SemVer } from "../tools/SemVer";
|
|
||||||
import type { BuildContext } from "./buildContext";
|
|
||||||
|
|
||||||
export type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {};
|
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
|
||||||
|
|
||||||
export async function promptKeycloakVersion(params: {
|
|
||||||
startingFromMajor: number | undefined;
|
|
||||||
excludeMajorVersions: number[];
|
|
||||||
doOmitPatch: boolean;
|
|
||||||
buildContext: BuildContextLike;
|
|
||||||
}) {
|
|
||||||
const { startingFromMajor, excludeMajorVersions, doOmitPatch, buildContext } = params;
|
|
||||||
|
|
||||||
const semVersionedTagByMajor = new Map<number, { tag: string; version: SemVer }>();
|
|
||||||
|
|
||||||
const semVersionedTags = await getLatestsSemVersionedTag({
|
|
||||||
count: 50,
|
|
||||||
owner: "keycloak",
|
|
||||||
repo: "keycloak",
|
|
||||||
doIgnoreReleaseCandidates: true,
|
|
||||||
buildContext
|
|
||||||
});
|
|
||||||
|
|
||||||
semVersionedTags.forEach(semVersionedTag => {
|
|
||||||
if (
|
|
||||||
startingFromMajor !== undefined &&
|
|
||||||
semVersionedTag.version.major < startingFromMajor
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludeMajorVersions.includes(semVersionedTag.version.major)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentSemVersionedTag = semVersionedTagByMajor.get(
|
|
||||||
semVersionedTag.version.major
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentSemVersionedTag !== undefined &&
|
|
||||||
SemVer.compare(semVersionedTag.version, currentSemVersionedTag.version) === -1
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
semVersionedTagByMajor.set(semVersionedTag.version.major, semVersionedTag);
|
|
||||||
});
|
|
||||||
|
|
||||||
const lastMajorVersions = Array.from(semVersionedTagByMajor.values()).map(
|
|
||||||
({ version }) =>
|
|
||||||
`${version.major}.${version.minor}${doOmitPatch ? "" : `.${version.patch}`}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const { value } = await cliSelect<string>({
|
|
||||||
values: lastMajorVersions
|
|
||||||
}).catch(() => {
|
|
||||||
process.exit(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
const keycloakVersion = value.split(" ")[0];
|
|
||||||
|
|
||||||
return { keycloakVersion };
|
|
||||||
}
|
|
@ -10,6 +10,7 @@ import { join as pathJoin, dirname as pathDirname } from "path";
|
|||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
import { existsAsync } from "../tools/fs.existsAsync";
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||||
|
import type { ReturnType } from "tsafe";
|
||||||
|
|
||||||
export type BuildContextLike = {
|
export type BuildContextLike = {
|
||||||
fetchOptions: BuildContext["fetchOptions"];
|
fetchOptions: BuildContext["fetchOptions"];
|
||||||
@ -20,7 +21,10 @@ assert<BuildContext extends BuildContextLike ? true : false>;
|
|||||||
|
|
||||||
export async function getSupportedDockerImageTags(params: {
|
export async function getSupportedDockerImageTags(params: {
|
||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
}) {
|
}): Promise<{
|
||||||
|
allSupportedTags: string[];
|
||||||
|
latestMajorTags: string[];
|
||||||
|
}> {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -31,14 +35,14 @@ export async function getSupportedDockerImageTags(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags: string[] = [];
|
const tags_queryResponse: string[] = [];
|
||||||
|
|
||||||
await (async function callee(url: string) {
|
await (async function callee(url: string) {
|
||||||
const r = await fetch(url, buildContext.fetchOptions);
|
const r = await fetch(url, buildContext.fetchOptions);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
(async () => {
|
(async () => {
|
||||||
tags.push(
|
tags_queryResponse.push(
|
||||||
...z
|
...z
|
||||||
.object({
|
.object({
|
||||||
tags: z.array(z.string())
|
tags: z.array(z.string())
|
||||||
@ -70,7 +74,9 @@ export async function getSupportedDockerImageTags(params: {
|
|||||||
]);
|
]);
|
||||||
})("https://quay.io/v2/keycloak/keycloak/tags/list");
|
})("https://quay.io/v2/keycloak/keycloak/tags/list");
|
||||||
|
|
||||||
const arr = tags
|
const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions();
|
||||||
|
|
||||||
|
const allSupportedTags_withVersion = tags_queryResponse
|
||||||
.map(tag => ({
|
.map(tag => ({
|
||||||
tag,
|
tag,
|
||||||
version: (() => {
|
version: (() => {
|
||||||
@ -86,28 +92,35 @@ export async function getSupportedDockerImageTags(params: {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tag.split(".").length !== 3) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportedKeycloakMajorVersions.includes(version.major)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return version;
|
return version;
|
||||||
})()
|
})()
|
||||||
}))
|
}))
|
||||||
.map(({ tag, version }) => (version === undefined ? undefined : { tag, version }))
|
.map(({ tag, version }) => (version === undefined ? undefined : { tag, version }))
|
||||||
.filter(exclude(undefined));
|
.filter(exclude(undefined))
|
||||||
|
.sort(({ version: a }, { version: b }) => SemVer.compare(b, a));
|
||||||
|
|
||||||
const versionByMajor: Record<number, SemVer | undefined> = {};
|
const latestTagByMajor: Record<number, SemVer | undefined> = {};
|
||||||
|
|
||||||
for (const { version } of arr) {
|
for (const { version } of allSupportedTags_withVersion) {
|
||||||
const version_current = versionByMajor[version.major];
|
const version_current = latestTagByMajor[version.major];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
version_current === undefined ||
|
version_current === undefined ||
|
||||||
SemVer.compare(version_current, version) === -1
|
SemVer.compare(version_current, version) === -1
|
||||||
) {
|
) {
|
||||||
versionByMajor[version.major] = version;
|
latestTagByMajor[version.major] = version;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions();
|
const latestMajorTags = Object.entries(latestTagByMajor)
|
||||||
|
|
||||||
const result = Object.entries(versionByMajor)
|
|
||||||
.sort(([a], [b]) => parseInt(b) - parseInt(a))
|
.sort(([a], [b]) => parseInt(b) - parseInt(a))
|
||||||
.map(([, version]) => version)
|
.map(([, version]) => version)
|
||||||
.map(version => {
|
.map(version => {
|
||||||
@ -121,16 +134,40 @@ export async function getSupportedDockerImageTags(params: {
|
|||||||
})
|
})
|
||||||
.filter(exclude(undefined));
|
.filter(exclude(undefined));
|
||||||
|
|
||||||
|
const allSupportedTags = allSupportedTags_withVersion.map(({ tag }) => tag);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
latestMajorTags,
|
||||||
|
allSupportedTags
|
||||||
|
};
|
||||||
|
|
||||||
await setCachedValue({ cacheDirPath: buildContext.cacheDirPath, result });
|
await setCachedValue({ cacheDirPath: buildContext.cacheDirPath, result });
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getCachedValue, setCachedValue } = (() => {
|
const { getCachedValue, setCachedValue } = (() => {
|
||||||
|
type Result = ReturnType<typeof getSupportedDockerImageTags>;
|
||||||
|
|
||||||
|
const zResult = (() => {
|
||||||
|
type TargetType = Result;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
allSupportedTags: z.array(z.string()),
|
||||||
|
latestMajorTags: z.array(z.string())
|
||||||
|
});
|
||||||
|
|
||||||
|
type InferredType = z.infer<typeof zTargetType>;
|
||||||
|
|
||||||
|
assert<Equals<TargetType, InferredType>>;
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
type Cache = {
|
type Cache = {
|
||||||
keycloakifyVersion: string;
|
keycloakifyVersion: string;
|
||||||
time: number;
|
time: number;
|
||||||
result: string[];
|
result: Result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const zCache = (() => {
|
const zCache = (() => {
|
||||||
@ -139,7 +176,7 @@ const { getCachedValue, setCachedValue } = (() => {
|
|||||||
const zTargetType = z.object({
|
const zTargetType = z.object({
|
||||||
keycloakifyVersion: z.string(),
|
keycloakifyVersion: z.string(),
|
||||||
time: z.number(),
|
time: z.number(),
|
||||||
result: z.array(z.string())
|
result: zResult
|
||||||
});
|
});
|
||||||
|
|
||||||
type InferredType = z.infer<typeof zTargetType>;
|
type InferredType = z.infer<typeof zTargetType>;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
import { is } from "tsafe/is";
|
|
||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
export type ParsedRealmJson = {
|
export type ParsedRealmJson = {
|
||||||
realm: string;
|
realm: string;
|
||||||
@ -50,7 +48,7 @@ export type ParsedRealmJson = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const zParsedRealmJson = (() => {
|
export const zParsedRealmJson = (() => {
|
||||||
type TargetType = ParsedRealmJson;
|
type TargetType = ParsedRealmJson;
|
||||||
|
|
||||||
const zTargetType = z.object({
|
const zTargetType = z.object({
|
||||||
@ -118,19 +116,3 @@ const zParsedRealmJson = (() => {
|
|||||||
|
|
||||||
return id<z.ZodType<TargetType>>(zTargetType);
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
export function readRealmJsonFile(params: {
|
|
||||||
realmJsonFilePath: string;
|
|
||||||
}): ParsedRealmJson {
|
|
||||||
const { realmJsonFilePath } = params;
|
|
||||||
|
|
||||||
const parsedRealmJson = JSON.parse(
|
|
||||||
fs.readFileSync(realmJsonFilePath).toString("utf8")
|
|
||||||
) as unknown;
|
|
||||||
|
|
||||||
zParsedRealmJson.parse(parsedRealmJson);
|
|
||||||
|
|
||||||
assert(is<ParsedRealmJson>(parsedRealmJson));
|
|
||||||
|
|
||||||
return parsedRealmJson;
|
|
||||||
}
|
|
@ -0,0 +1,3 @@
|
|||||||
|
export type { ParsedRealmJson } from "./ParsedRealmJson";
|
||||||
|
export { readRealmJsonFile } from "./readRealmJsonFile";
|
||||||
|
export { writeRealmJsonFile } from "./writeRealmJsonFile";
|
@ -0,0 +1,20 @@
|
|||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { type ParsedRealmJson, zParsedRealmJson } from "./ParsedRealmJson";
|
||||||
|
|
||||||
|
export function readRealmJsonFile(params: {
|
||||||
|
realmJsonFilePath: string;
|
||||||
|
}): ParsedRealmJson {
|
||||||
|
const { realmJsonFilePath } = params;
|
||||||
|
|
||||||
|
const parsedRealmJson = JSON.parse(
|
||||||
|
fs.readFileSync(realmJsonFilePath).toString("utf8")
|
||||||
|
) as unknown;
|
||||||
|
|
||||||
|
zParsedRealmJson.parse(parsedRealmJson);
|
||||||
|
|
||||||
|
assert(is<ParsedRealmJson>(parsedRealmJson));
|
||||||
|
|
||||||
|
return parsedRealmJson;
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import { getIsPrettierAvailable, runPrettier } from "../../../tools/runPrettier";
|
||||||
|
import { canonicalStringify } from "../../../tools/canonicalStringify";
|
||||||
|
import type { ParsedRealmJson } from "./ParsedRealmJson";
|
||||||
|
import { getDefaultConfig } from "../defaultConfig";
|
||||||
|
|
||||||
|
export async function writeRealmJsonFile(params: {
|
||||||
|
realmJsonFilePath: string;
|
||||||
|
parsedRealmJson: ParsedRealmJson;
|
||||||
|
keycloakMajorVersionNumber: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { realmJsonFilePath, parsedRealmJson, keycloakMajorVersionNumber } = params;
|
||||||
|
|
||||||
|
let sourceCode = canonicalStringify({
|
||||||
|
data: parsedRealmJson,
|
||||||
|
referenceData: getDefaultConfig({
|
||||||
|
keycloakMajorVersionNumber
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (await getIsPrettierAvailable()) {
|
||||||
|
sourceCode = await runPrettier({
|
||||||
|
sourceCode: sourceCode,
|
||||||
|
filePath: realmJsonFilePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsPr.writeFile(realmJsonFilePath, Buffer.from(sourceCode, "utf8"));
|
||||||
|
}
|
@ -3,11 +3,10 @@ import { getThisCodebaseRootDirPath } from "../../../tools/getThisCodebaseRootDi
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { exclude } from "tsafe/exclude";
|
import { exclude } from "tsafe/exclude";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { type ParsedRealmJson, readRealmJsonFile } from "../ParsedRealmJson";
|
import { readRealmJsonFile } from "../ParsedRealmJson/readRealmJsonFile";
|
||||||
|
import type { ParsedRealmJson } from "../ParsedRealmJson/ParsedRealmJson";
|
||||||
|
|
||||||
export function getDefaultRealmJsonFilePath(params: {
|
function getDefaultRealmJsonFilePath(params: { keycloakMajorVersionNumber: number }) {
|
||||||
keycloakMajorVersionNumber: number;
|
|
||||||
}) {
|
|
||||||
const { keycloakMajorVersionNumber } = params;
|
const { keycloakMajorVersionNumber } = params;
|
||||||
|
|
||||||
return pathJoin(
|
return pathJoin(
|
||||||
|
@ -756,6 +756,24 @@
|
|||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"protocolMappers": [
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
||||||
"name": "locale",
|
"name": "locale",
|
||||||
@ -1336,13 +1354,13 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
|
"saml-user-property-mapper",
|
||||||
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
|
"oidc-address-mapper",
|
||||||
|
"oidc-full-name-mapper",
|
||||||
|
"oidc-usermodel-attribute-mapper",
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"oidc-usermodel-property-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"saml-user-property-mapper",
|
|
||||||
"oidc-usermodel-attribute-mapper",
|
|
||||||
"oidc-address-mapper",
|
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
|
||||||
"saml-role-list-mapper"
|
"saml-role-list-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1393,13 +1411,13 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-full-name-mapper",
|
"oidc-full-name-mapper",
|
||||||
"oidc-usermodel-property-mapper",
|
|
||||||
"saml-user-property-mapper",
|
|
||||||
"oidc-usermodel-attribute-mapper",
|
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
|
||||||
"saml-role-list-mapper",
|
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
"saml-user-attribute-mapper"
|
"saml-role-list-mapper",
|
||||||
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
|
"saml-user-attribute-mapper",
|
||||||
|
"saml-user-property-mapper",
|
||||||
|
"oidc-usermodel-property-mapper",
|
||||||
|
"oidc-usermodel-attribute-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1517,7 +1535,7 @@
|
|||||||
"defaultLocale": "en",
|
"defaultLocale": "en",
|
||||||
"authenticationFlows": [
|
"authenticationFlows": [
|
||||||
{
|
{
|
||||||
"id": "223ce532-2038-4f24-a606-2a5c73f7bd65",
|
"id": "f664efe4-102d-4ec1-bf11-11af67e3f178",
|
||||||
"alias": "Account verification options",
|
"alias": "Account verification options",
|
||||||
"description": "Method with which to verity the existing account",
|
"description": "Method with which to verity the existing account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1543,7 +1561,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "57e47732-79cc-4d60-bee7-4f0b8fd44540",
|
"id": "8a5630c5-eca1-4b6a-8e59-459cb6c84535",
|
||||||
"alias": "Authentication Options",
|
"alias": "Authentication Options",
|
||||||
"description": "Authentication options.",
|
"description": "Authentication options.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1577,7 +1595,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "c2735d89-60c0-45a4-9b3c-ae5df17df395",
|
"id": "c1a3eed3-25ce-44ae-93d1-f0b8148a0f8c",
|
||||||
"alias": "Browser - Conditional OTP",
|
"alias": "Browser - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1603,7 +1621,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "11a5a507-2b9a-443f-961b-dffd66f4318d",
|
"id": "6eb188ad-1041-44dd-bf8f-37cae0d98bf1",
|
||||||
"alias": "Direct Grant - Conditional OTP",
|
"alias": "Direct Grant - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1629,7 +1647,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "963bd753-6ea7-4d93-ab56-30f9ab59d597",
|
"id": "4ee215ac-f4e5-4edb-bf76-65dc9e211543",
|
||||||
"alias": "First broker login - Conditional OTP",
|
"alias": "First broker login - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1655,7 +1673,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "1db6a489-a3b4-44c4-b480-1d1e8c123d20",
|
"id": "5a1eac7e-06a0-46d8-b9ae-1f2c934331f9",
|
||||||
"alias": "Handle Existing Account",
|
"alias": "Handle Existing Account",
|
||||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1681,7 +1699,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "7a38f32d-4f34-450f-8f03-64802d7cb8f1",
|
"id": "ed165166-4521-4a62-b185-c4b51643cbb1",
|
||||||
"alias": "Reset - Conditional OTP",
|
"alias": "Reset - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1707,7 +1725,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "0df88739-3739-4d70-8893-47c546f19003",
|
"id": "4788fb1f-fd81-4f5d-9abe-4199dd641c1e",
|
||||||
"alias": "User creation or linking",
|
"alias": "User creation or linking",
|
||||||
"description": "Flow for the existing/non-existing user alternatives",
|
"description": "Flow for the existing/non-existing user alternatives",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1734,7 +1752,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "35025424-e291-4c54-8a29-70aadba549ce",
|
"id": "d778a70f-f472-4dd3-ac40-cb5612ddc171",
|
||||||
"alias": "Verify Existing Account by Re-authentication",
|
"alias": "Verify Existing Account by Re-authentication",
|
||||||
"description": "Reauthentication of existing account",
|
"description": "Reauthentication of existing account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1760,7 +1778,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "1813b7f2-c3c2-4b92-8ffc-9ff2d12186c6",
|
"id": "9c1ea8ea-7c23-4e60-b02d-1900d9dc4109",
|
||||||
"alias": "browser",
|
"alias": "browser",
|
||||||
"description": "browser based authentication",
|
"description": "browser based authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1802,7 +1820,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "954283ac-f1c2-40b6-a39f-bf23ff9f3ce8",
|
"id": "0ebdf418-d57d-4318-9359-7bd0cb2381f2",
|
||||||
"alias": "clients",
|
"alias": "clients",
|
||||||
"description": "Base authentication for clients",
|
"description": "Base authentication for clients",
|
||||||
"providerId": "client-flow",
|
"providerId": "client-flow",
|
||||||
@ -1844,7 +1862,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "52a789ce-2cad-4f0f-93b2-295b7fd519f0",
|
"id": "5cc89293-c72e-4c5e-b31c-15558588a60d",
|
||||||
"alias": "direct grant",
|
"alias": "direct grant",
|
||||||
"description": "OpenID Connect Resource Owner Grant",
|
"description": "OpenID Connect Resource Owner Grant",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1878,7 +1896,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "5a6a71e1-9105-45b6-b5f0-52538461357b",
|
"id": "5ae5a321-ccac-449e-9c19-d6dc22ab8085",
|
||||||
"alias": "docker auth",
|
"alias": "docker auth",
|
||||||
"description": "Used by Docker clients to authenticate against the IDP",
|
"description": "Used by Docker clients to authenticate against the IDP",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1896,7 +1914,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "8392b6e7-bdbf-4d7f-97b6-885761c200db",
|
"id": "7737fdd1-0875-47e6-977b-12561cddfdc3",
|
||||||
"alias": "first broker login",
|
"alias": "first broker login",
|
||||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1923,7 +1941,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "52136d70-8d08-42ea-b04b-cf40ea2807aa",
|
"id": "90f975c3-9826-461f-88ca-27c697aff86b",
|
||||||
"alias": "forms",
|
"alias": "forms",
|
||||||
"description": "Username, password, otp and other auth forms.",
|
"description": "Username, password, otp and other auth forms.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1949,7 +1967,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "26bbc7e6-ef01-4cdb-9dba-520e2f3f8993",
|
"id": "ce2722d5-9f4f-41a2-8f81-e01f7b6cee57",
|
||||||
"alias": "http challenge",
|
"alias": "http challenge",
|
||||||
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1975,7 +1993,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "f0887979-04eb-4033-8f19-0ffd8c8b7f6a",
|
"id": "31b5bfa7-98ad-47a2-b8e6-0669022cd8cb",
|
||||||
"alias": "registration",
|
"alias": "registration",
|
||||||
"description": "registration flow",
|
"description": "registration flow",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1994,7 +2012,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a3b7b94b-bfbf-4760-a8c9-7d9cd98d262e",
|
"id": "bf8a950b-be3b-4e44-8602-64e0bba492eb",
|
||||||
"alias": "registration form",
|
"alias": "registration form",
|
||||||
"description": "registration form",
|
"description": "registration form",
|
||||||
"providerId": "form-flow",
|
"providerId": "form-flow",
|
||||||
@ -2036,7 +2054,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "dc68a665-2e51-4a22-aaad-bd693ddc77cc",
|
"id": "e3519800-971b-4b1d-b64e-3983ccd02dea",
|
||||||
"alias": "reset credentials",
|
"alias": "reset credentials",
|
||||||
"description": "Reset credentials for a user if they forgot their password or something",
|
"description": "Reset credentials for a user if they forgot their password or something",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2078,7 +2096,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ae6b73aa-1318-4ae8-a3d9-d01b5e7d957e",
|
"id": "9d5a33a2-e777-4beb-95de-b84812f69c56",
|
||||||
"alias": "saml ecp",
|
"alias": "saml ecp",
|
||||||
"description": "SAML ECP Profile Authentication Flow",
|
"description": "SAML ECP Profile Authentication Flow",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2098,14 +2116,14 @@
|
|||||||
],
|
],
|
||||||
"authenticatorConfig": [
|
"authenticatorConfig": [
|
||||||
{
|
{
|
||||||
"id": "0c18de7f-0714-41f4-9a3f-ed4edd53ae9c",
|
"id": "4901c91d-59bd-4727-b585-8e4e44828d0a",
|
||||||
"alias": "create unique user config",
|
"alias": "create unique user config",
|
||||||
"config": {
|
"config": {
|
||||||
"require.password.update.after.registration": "false"
|
"require.password.update.after.registration": "false"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "65b3c8bb-34a4-4d19-b578-245dc8ff53ea",
|
"id": "5062a078-83a7-4933-b0d5-3f75cc2a5003",
|
||||||
"alias": "review profile config",
|
"alias": "review profile config",
|
||||||
"config": {
|
"config": {
|
||||||
"update.profile.on.first.login": "missing"
|
"update.profile.on.first.login": "missing"
|
||||||
|
@ -764,6 +764,24 @@
|
|||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"protocolMappers": [
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
||||||
"name": "locale",
|
"name": "locale",
|
||||||
@ -1344,14 +1362,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"saml-user-property-mapper",
|
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"oidc-full-name-mapper",
|
"oidc-full-name-mapper",
|
||||||
"oidc-usermodel-property-mapper",
|
|
||||||
"oidc-usermodel-attribute-mapper",
|
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
|
"saml-user-property-mapper",
|
||||||
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
|
"oidc-usermodel-attribute-mapper",
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
"oidc-sha256-pairwise-sub-mapper"
|
"oidc-usermodel-property-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1400,14 +1418,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
|
"saml-user-property-mapper",
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
"oidc-usermodel-property-mapper",
|
|
||||||
"saml-role-list-mapper",
|
|
||||||
"oidc-full-name-mapper",
|
"oidc-full-name-mapper",
|
||||||
"saml-user-property-mapper",
|
"saml-user-attribute-mapper",
|
||||||
|
"oidc-usermodel-property-mapper",
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
"saml-user-attribute-mapper"
|
"saml-role-list-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1525,7 +1543,7 @@
|
|||||||
"defaultLocale": "en",
|
"defaultLocale": "en",
|
||||||
"authenticationFlows": [
|
"authenticationFlows": [
|
||||||
{
|
{
|
||||||
"id": "1f4d4e13-1591-4751-8985-17886a8c98a9",
|
"id": "8ccfe057-5ce6-499b-9fae-3cd89b62bf01",
|
||||||
"alias": "Account verification options",
|
"alias": "Account verification options",
|
||||||
"description": "Method with which to verity the existing account",
|
"description": "Method with which to verity the existing account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1551,7 +1569,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "126f07c3-1bcb-4a02-bf16-bb44674bf55d",
|
"id": "f3b9ab2e-41c2-4e73-876b-e2c275d6d14e",
|
||||||
"alias": "Authentication Options",
|
"alias": "Authentication Options",
|
||||||
"description": "Authentication options.",
|
"description": "Authentication options.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1585,7 +1603,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "eb3a08c8-5f99-49b6-b02b-16b62571f273",
|
"id": "df1329cc-777c-42d8-aa2f-c5d5ddaaf5a4",
|
||||||
"alias": "Browser - Conditional OTP",
|
"alias": "Browser - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1611,7 +1629,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "3dc19838-5025-4bbb-b569-b574bd5a8d90",
|
"id": "f78a4cbc-66ff-4caa-8066-67aff94946f4",
|
||||||
"alias": "Direct Grant - Conditional OTP",
|
"alias": "Direct Grant - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1637,7 +1655,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "70d6fd40-d740-4dae-b0e6-350f8e9d4a1c",
|
"id": "4b20995b-5553-45db-86b0-05c3fe14edb1",
|
||||||
"alias": "First broker login - Conditional OTP",
|
"alias": "First broker login - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1663,7 +1681,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "6e24dcb3-5818-483c-8e44-883858171901",
|
"id": "0a7cc6b7-e427-4f72-b44e-a02133241bad",
|
||||||
"alias": "Handle Existing Account",
|
"alias": "Handle Existing Account",
|
||||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1689,7 +1707,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ac6254cd-403b-457b-b308-22a2a0e4f99d",
|
"id": "e24e73c0-dd51-4fdc-a916-284f11f38487",
|
||||||
"alias": "Reset - Conditional OTP",
|
"alias": "Reset - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1715,7 +1733,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "485e74e6-9b3e-4b2c-a9b9-927802dc4f06",
|
"id": "37ee5a12-01c2-41b0-aafa-e9c6661ff544",
|
||||||
"alias": "User creation or linking",
|
"alias": "User creation or linking",
|
||||||
"description": "Flow for the existing/non-existing user alternatives",
|
"description": "Flow for the existing/non-existing user alternatives",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1742,7 +1760,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ff9bb879-1d6a-4d1c-9836-1e4fab6f8997",
|
"id": "8902a1a7-c2ee-4648-869f-dd5ef89184fc",
|
||||||
"alias": "Verify Existing Account by Re-authentication",
|
"alias": "Verify Existing Account by Re-authentication",
|
||||||
"description": "Reauthentication of existing account",
|
"description": "Reauthentication of existing account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1768,7 +1786,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "af8b2470-d581-401c-9984-762b966ebcc2",
|
"id": "77c78eed-4bcd-4779-b39f-10135be84946",
|
||||||
"alias": "browser",
|
"alias": "browser",
|
||||||
"description": "browser based authentication",
|
"description": "browser based authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1810,7 +1828,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "414dbda4-eb3f-4baa-b23a-d3423af1eae6",
|
"id": "c6398883-01e6-47a1-bb97-c09f2983155d",
|
||||||
"alias": "clients",
|
"alias": "clients",
|
||||||
"description": "Base authentication for clients",
|
"description": "Base authentication for clients",
|
||||||
"providerId": "client-flow",
|
"providerId": "client-flow",
|
||||||
@ -1852,7 +1870,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "1cae0c4b-8dfb-4f5d-a781-e74d0a13c940",
|
"id": "78ab5fb8-f35b-4053-b264-94b208000b13",
|
||||||
"alias": "direct grant",
|
"alias": "direct grant",
|
||||||
"description": "OpenID Connect Resource Owner Grant",
|
"description": "OpenID Connect Resource Owner Grant",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1886,7 +1904,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "e798b655-7d85-4b6b-aee7-1448a3e1e0ea",
|
"id": "959e154b-034e-413d-9b19-211e7d9ba33d",
|
||||||
"alias": "docker auth",
|
"alias": "docker auth",
|
||||||
"description": "Used by Docker clients to authenticate against the IDP",
|
"description": "Used by Docker clients to authenticate against the IDP",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1904,7 +1922,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "eb94b723-1041-426a-87bf-f7b4bd2f485d",
|
"id": "001e253d-bdbd-41e2-81c7-1c7b239feeb1",
|
||||||
"alias": "first broker login",
|
"alias": "first broker login",
|
||||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1931,7 +1949,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "452d1d5f-7632-44d7-bc89-77ff2b209b3e",
|
"id": "45481bb0-18fe-4a26-a77c-35a5afe58436",
|
||||||
"alias": "forms",
|
"alias": "forms",
|
||||||
"description": "Username, password, otp and other auth forms.",
|
"description": "Username, password, otp and other auth forms.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1957,7 +1975,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "7c1b9e8f-6b57-49d1-a9a7-494862f93c0f",
|
"id": "bb47b847-5a55-4c08-909e-9f6f8d8a0636",
|
||||||
"alias": "http challenge",
|
"alias": "http challenge",
|
||||||
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1983,7 +2001,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2b38f34a-1739-499e-bb24-1dff96f32009",
|
"id": "77e6e169-05b7-4b89-af00-09cfe1604eed",
|
||||||
"alias": "registration",
|
"alias": "registration",
|
||||||
"description": "registration flow",
|
"description": "registration flow",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2002,7 +2020,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "d26ae72b-a933-44dc-9927-1c82757004b2",
|
"id": "aef03fe8-1a70-40c3-879f-25588f75c119",
|
||||||
"alias": "registration form",
|
"alias": "registration form",
|
||||||
"description": "registration form",
|
"description": "registration form",
|
||||||
"providerId": "form-flow",
|
"providerId": "form-flow",
|
||||||
@ -2044,7 +2062,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "222ee8d6-1892-4768-9ada-720274b6bf9a",
|
"id": "990abff7-e2ba-4217-984e-8890cbc2b3a9",
|
||||||
"alias": "reset credentials",
|
"alias": "reset credentials",
|
||||||
"description": "Reset credentials for a user if they forgot their password or something",
|
"description": "Reset credentials for a user if they forgot their password or something",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2086,7 +2104,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "e8b4d92c-27c1-4a9b-9b16-7ceb810fa230",
|
"id": "d9894cf6-2f99-493e-ac47-853f54bfc9c6",
|
||||||
"alias": "saml ecp",
|
"alias": "saml ecp",
|
||||||
"description": "SAML ECP Profile Authentication Flow",
|
"description": "SAML ECP Profile Authentication Flow",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2106,14 +2124,14 @@
|
|||||||
],
|
],
|
||||||
"authenticatorConfig": [
|
"authenticatorConfig": [
|
||||||
{
|
{
|
||||||
"id": "e5847a0b-855d-4d93-85fd-94714be3ed92",
|
"id": "101ed8ff-4383-4539-aa52-2d1e69698b78",
|
||||||
"alias": "create unique user config",
|
"alias": "create unique user config",
|
||||||
"config": {
|
"config": {
|
||||||
"require.password.update.after.registration": "false"
|
"require.password.update.after.registration": "false"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a2a18aa4-bd4c-4c2a-9286-e9d6c64f4812",
|
"id": "049042a5-3551-4c16-81a1-64d86f5aa1e5",
|
||||||
"alias": "review profile config",
|
"alias": "review profile config",
|
||||||
"config": {
|
"config": {
|
||||||
"update.profile.on.first.login": "missing"
|
"update.profile.on.first.login": "missing"
|
||||||
|
@ -775,6 +775,24 @@
|
|||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"protocolMappers": [
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
||||||
"name": "locale",
|
"name": "locale",
|
||||||
@ -1355,14 +1373,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-address-mapper",
|
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"saml-role-list-mapper",
|
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"oidc-usermodel-property-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
|
"oidc-address-mapper",
|
||||||
|
"oidc-full-name-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
"saml-user-property-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"saml-user-attribute-mapper"
|
"saml-role-list-mapper",
|
||||||
|
"saml-user-property-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1411,14 +1429,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"saml-user-attribute-mapper",
|
|
||||||
"saml-role-list-mapper",
|
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
|
||||||
"oidc-full-name-mapper",
|
"oidc-full-name-mapper",
|
||||||
|
"oidc-usermodel-attribute-mapper",
|
||||||
|
"saml-role-list-mapper",
|
||||||
|
"saml-user-attribute-mapper",
|
||||||
"oidc-usermodel-property-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
"saml-user-property-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"oidc-usermodel-attribute-mapper"
|
"saml-user-property-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1536,7 +1554,7 @@
|
|||||||
"defaultLocale": "en",
|
"defaultLocale": "en",
|
||||||
"authenticationFlows": [
|
"authenticationFlows": [
|
||||||
{
|
{
|
||||||
"id": "c40791b4-4d59-4df2-bebd-2b71e793704f",
|
"id": "30a878f0-57aa-4d20-bab0-6cf1d7317a5c",
|
||||||
"alias": "Account verification options",
|
"alias": "Account verification options",
|
||||||
"description": "Method with which to verity the existing account",
|
"description": "Method with which to verity the existing account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1562,7 +1580,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "8813b6d1-8b88-4672-b29b-8420ce3f3975",
|
"id": "d386affe-d1fe-472a-bee6-54105d0101f5",
|
||||||
"alias": "Authentication Options",
|
"alias": "Authentication Options",
|
||||||
"description": "Authentication options.",
|
"description": "Authentication options.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1596,7 +1614,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a9937c40-a1ee-4c57-adf7-ede0a9983953",
|
"id": "77b95bc0-bd0c-46b7-8240-3182023e9d50",
|
||||||
"alias": "Browser - Conditional OTP",
|
"alias": "Browser - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1622,7 +1640,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2d494b5a-eb73-40d0-94d3-a8d8024a7db4",
|
"id": "bc96d3d6-29a1-42af-a63e-bb67a8c6d78f",
|
||||||
"alias": "Direct Grant - Conditional OTP",
|
"alias": "Direct Grant - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1648,7 +1666,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2e977f5a-8110-412b-b704-3e15164dbb1b",
|
"id": "7697ca74-5c2b-45ab-9335-e0f6dec59b5c",
|
||||||
"alias": "First broker login - Conditional OTP",
|
"alias": "First broker login - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP is required for the authentication",
|
"description": "Flow to determine if the OTP is required for the authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1674,7 +1692,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "6f171b4b-8723-4e6d-bb1e-6b4293a7bb3f",
|
"id": "534cb120-f600-4f40-9707-7b781bdbce48",
|
||||||
"alias": "Handle Existing Account",
|
"alias": "Handle Existing Account",
|
||||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1700,7 +1718,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2dbb7f27-757d-4178-8217-4a24fdb0163c",
|
"id": "f884b048-b223-4ed6-ae16-e49a4255131e",
|
||||||
"alias": "Reset - Conditional OTP",
|
"alias": "Reset - Conditional OTP",
|
||||||
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1726,7 +1744,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "7295aaf7-acf4-4b78-8186-d2415ea4ede0",
|
"id": "61c7966c-ad72-49f5-84dd-376152348092",
|
||||||
"alias": "User creation or linking",
|
"alias": "User creation or linking",
|
||||||
"description": "Flow for the existing/non-existing user alternatives",
|
"description": "Flow for the existing/non-existing user alternatives",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1753,7 +1771,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "e0d34d7c-7bbb-4847-8864-fbd97a1f3e89",
|
"id": "72412d0f-dd1b-49fe-bb0b-9dad99eb0491",
|
||||||
"alias": "Verify Existing Account by Re-authentication",
|
"alias": "Verify Existing Account by Re-authentication",
|
||||||
"description": "Reauthentication of existing account",
|
"description": "Reauthentication of existing account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1779,7 +1797,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "5f3d0fb0-d95e-4841-89d3-a27d0cdbbcb4",
|
"id": "6b76613e-0d39-440d-aab4-98eaffb1e96a",
|
||||||
"alias": "browser",
|
"alias": "browser",
|
||||||
"description": "browser based authentication",
|
"description": "browser based authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1821,7 +1839,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "c246380d-af25-4151-ab19-1f1e5b553008",
|
"id": "0ff60395-fa89-41be-ad22-fab339e67c49",
|
||||||
"alias": "clients",
|
"alias": "clients",
|
||||||
"description": "Base authentication for clients",
|
"description": "Base authentication for clients",
|
||||||
"providerId": "client-flow",
|
"providerId": "client-flow",
|
||||||
@ -1863,7 +1881,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "abacf398-0f1f-4f28-a310-8d306d588048",
|
"id": "bbb3ece7-7dbf-4aba-80c3-dde4b9cdd0b6",
|
||||||
"alias": "direct grant",
|
"alias": "direct grant",
|
||||||
"description": "OpenID Connect Resource Owner Grant",
|
"description": "OpenID Connect Resource Owner Grant",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1897,7 +1915,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a0f87683-619a-44d4-8b4f-4b053bba2346",
|
"id": "f5f2c0f6-7dbf-4978-845e-6cacac23aa13",
|
||||||
"alias": "docker auth",
|
"alias": "docker auth",
|
||||||
"description": "Used by Docker clients to authenticate against the IDP",
|
"description": "Used by Docker clients to authenticate against the IDP",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1915,7 +1933,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "e8820c7c-22a7-4618-beb7-3e09be72c00c",
|
"id": "cf463104-19e2-41a8-8a53-d3dd30b75344",
|
||||||
"alias": "first broker login",
|
"alias": "first broker login",
|
||||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1942,7 +1960,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cac00c38-ee44-44c9-b95e-cc755bab36ef",
|
"id": "b99b60dc-41ad-487d-be69-a2eefa954a9d",
|
||||||
"alias": "forms",
|
"alias": "forms",
|
||||||
"description": "Username, password, otp and other auth forms.",
|
"description": "Username, password, otp and other auth forms.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1968,7 +1986,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "688cde36-507e-4a68-afdf-18ec4ad626a7",
|
"id": "18731296-2c96-4f98-a884-027e629e4f9d",
|
||||||
"alias": "http challenge",
|
"alias": "http challenge",
|
||||||
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -1994,7 +2012,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "e058697c-f450-4f14-ae64-04e9299fa24f",
|
"id": "9a9dce17-5425-4fd5-b3b8-81410e1dbce4",
|
||||||
"alias": "registration",
|
"alias": "registration",
|
||||||
"description": "registration flow",
|
"description": "registration flow",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2013,7 +2031,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ad768088-32c9-4979-90dd-61bf111fd72e",
|
"id": "d0a24e08-cb69-4949-9518-50ae7a96ee49",
|
||||||
"alias": "registration form",
|
"alias": "registration form",
|
||||||
"description": "registration form",
|
"description": "registration form",
|
||||||
"providerId": "form-flow",
|
"providerId": "form-flow",
|
||||||
@ -2055,7 +2073,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "47d4b090-f965-4588-b5bc-029ccb59876f",
|
"id": "6a9aa554-afba-487f-9c82-e94c81c15b3b",
|
||||||
"alias": "reset credentials",
|
"alias": "reset credentials",
|
||||||
"description": "Reset credentials for a user if they forgot their password or something",
|
"description": "Reset credentials for a user if they forgot their password or something",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2097,7 +2115,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "1f68feec-7f99-4c49-afe6-45d46684ca21",
|
"id": "e0361d46-eab4-41a6-bb2e-1dc6a5a6b073",
|
||||||
"alias": "saml ecp",
|
"alias": "saml ecp",
|
||||||
"description": "SAML ECP Profile Authentication Flow",
|
"description": "SAML ECP Profile Authentication Flow",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
@ -2117,14 +2135,14 @@
|
|||||||
],
|
],
|
||||||
"authenticatorConfig": [
|
"authenticatorConfig": [
|
||||||
{
|
{
|
||||||
"id": "bd7365c7-842b-4bc6-a4ca-498cf025c210",
|
"id": "053d6017-e54c-418a-abe7-44dd4752eacb",
|
||||||
"alias": "create unique user config",
|
"alias": "create unique user config",
|
||||||
"config": {
|
"config": {
|
||||||
"require.password.update.after.registration": "false"
|
"require.password.update.after.registration": "false"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "b929192d-f650-4a09-9701-3d3216547552",
|
"id": "8b545cf4-ab9e-4226-b3c0-d7ac773eae2f",
|
||||||
"alias": "review profile config",
|
"alias": "review profile config",
|
||||||
"config": {
|
"config": {
|
||||||
"update.profile.on.first.login": "missing"
|
"update.profile.on.first.login": "missing"
|
||||||
|
@ -408,9 +408,9 @@
|
|||||||
"otpPolicyPeriod": 30,
|
"otpPolicyPeriod": 30,
|
||||||
"otpPolicyCodeReusable": false,
|
"otpPolicyCodeReusable": false,
|
||||||
"otpSupportedApplications": [
|
"otpSupportedApplications": [
|
||||||
"totpAppGoogleName",
|
|
||||||
"totpAppFreeOTPName",
|
"totpAppFreeOTPName",
|
||||||
"totpAppMicrosoftAuthenticatorName"
|
"totpAppMicrosoftAuthenticatorName",
|
||||||
|
"totpAppGoogleName"
|
||||||
],
|
],
|
||||||
"webAuthnPolicyRpEntityName": "keycloak",
|
"webAuthnPolicyRpEntityName": "keycloak",
|
||||||
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
||||||
@ -779,6 +779,24 @@
|
|||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"protocolMappers": [
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
||||||
"name": "locale",
|
"name": "locale",
|
||||||
@ -1359,13 +1377,13 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"saml-user-attribute-mapper",
|
|
||||||
"saml-user-property-mapper",
|
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
|
||||||
"saml-role-list-mapper",
|
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"oidc-usermodel-property-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
|
"saml-user-property-mapper",
|
||||||
|
"saml-role-list-mapper",
|
||||||
|
"oidc-full-name-mapper",
|
||||||
|
"saml-user-attribute-mapper",
|
||||||
"oidc-address-mapper"
|
"oidc-address-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1415,14 +1433,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-address-mapper",
|
|
||||||
"oidc-usermodel-property-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"saml-user-property-mapper",
|
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
"saml-user-attribute-mapper"
|
"saml-user-attribute-mapper",
|
||||||
|
"saml-user-property-mapper",
|
||||||
|
"oidc-usermodel-attribute-mapper",
|
||||||
|
"oidc-address-mapper",
|
||||||
|
"oidc-full-name-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
2201
src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-22.json
Normal file
2201
src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-22.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -789,6 +789,24 @@
|
|||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"protocolMappers": [
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "59cde7ae-2218-4a8e-83af-cad992c3a700",
|
"id": "59cde7ae-2218-4a8e-83af-cad992c3a700",
|
||||||
"name": "locale",
|
"name": "locale",
|
||||||
@ -1401,14 +1419,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"saml-role-list-mapper",
|
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"oidc-full-name-mapper",
|
"oidc-full-name-mapper",
|
||||||
|
"oidc-usermodel-property-mapper",
|
||||||
|
"oidc-usermodel-attribute-mapper",
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
"saml-user-property-mapper",
|
"saml-user-property-mapper",
|
||||||
"oidc-usermodel-property-mapper"
|
"saml-role-list-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1477,14 +1495,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"saml-user-attribute-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
|
||||||
"oidc-address-mapper",
|
|
||||||
"saml-user-property-mapper",
|
"saml-user-property-mapper",
|
||||||
|
"oidc-usermodel-attribute-mapper",
|
||||||
|
"saml-user-attribute-mapper",
|
||||||
|
"oidc-address-mapper",
|
||||||
"oidc-full-name-mapper",
|
"oidc-full-name-mapper",
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper"
|
||||||
"oidc-usermodel-property-mapper"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -919,6 +919,24 @@
|
|||||||
"claim.name": "locale",
|
"claim.name": "locale",
|
||||||
"jsonType.label": "String"
|
"jsonType.label": "String"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
|
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
|
||||||
@ -1545,14 +1563,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
|
"oidc-full-name-mapper",
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
|
"saml-user-attribute-mapper",
|
||||||
|
"oidc-usermodel-attribute-mapper",
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
"oidc-usermodel-property-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
"saml-user-attribute-mapper",
|
|
||||||
"saml-user-property-mapper",
|
"saml-user-property-mapper",
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper"
|
||||||
"oidc-usermodel-attribute-mapper",
|
|
||||||
"oidc-full-name-mapper"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1584,14 +1602,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
|
"oidc-address-mapper",
|
||||||
"oidc-full-name-mapper",
|
"oidc-full-name-mapper",
|
||||||
"oidc-usermodel-property-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
"oidc-address-mapper",
|
"saml-user-property-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-attribute-mapper"
|
||||||
"saml-user-property-mapper"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -964,6 +964,24 @@
|
|||||||
"claim.name": "locale",
|
"claim.name": "locale",
|
||||||
"jsonType.label": "String"
|
"jsonType.label": "String"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||||
|
"name": "allowed-origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "allowed-origins",
|
||||||
|
"jsonType.label": "JSON",
|
||||||
|
"access.tokenResponse.claim": "false",
|
||||||
|
"claim.value": "[\"*\"]",
|
||||||
|
"lightweight.claim": "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"defaultClientScopes": [
|
"defaultClientScopes": [
|
||||||
@ -1618,14 +1636,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"saml-role-list-mapper",
|
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"saml-user-property-mapper",
|
|
||||||
"saml-user-attribute-mapper",
|
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
|
"saml-role-list-mapper",
|
||||||
|
"saml-user-property-mapper",
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"oidc-usermodel-property-mapper"
|
"saml-user-attribute-mapper",
|
||||||
|
"oidc-usermodel-property-mapper",
|
||||||
|
"oidc-full-name-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1657,12 +1675,12 @@
|
|||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
|
"oidc-usermodel-property-mapper",
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"oidc-usermodel-attribute-mapper",
|
|
||||||
"saml-user-property-mapper",
|
"saml-user-property-mapper",
|
||||||
"oidc-usermodel-property-mapper"
|
"oidc-usermodel-attribute-mapper",
|
||||||
|
"oidc-full-name-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -997,7 +997,7 @@
|
|||||||
"claim.value": "[\"*\"]",
|
"claim.value": "[\"*\"]",
|
||||||
"userinfo.token.claim": "true",
|
"userinfo.token.claim": "true",
|
||||||
"id.token.claim": "false",
|
"id.token.claim": "false",
|
||||||
"lightweight.claim": "false",
|
"lightweight.claim": "true",
|
||||||
"access.token.claim": "true",
|
"access.token.claim": "true",
|
||||||
"claim.name": "allowed-origins",
|
"claim.name": "allowed-origins",
|
||||||
"jsonType.label": "JSON",
|
"jsonType.label": "JSON",
|
||||||
@ -1628,7 +1628,7 @@
|
|||||||
"smtpServer": {},
|
"smtpServer": {},
|
||||||
"loginTheme": "keycloakify-starter",
|
"loginTheme": "keycloakify-starter",
|
||||||
"accountTheme": "",
|
"accountTheme": "",
|
||||||
"adminTheme": "",
|
"adminTheme": "keycloakify-starter",
|
||||||
"emailTheme": "",
|
"emailTheme": "",
|
||||||
"eventsEnabled": false,
|
"eventsEnabled": false,
|
||||||
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||||
@ -1657,13 +1657,13 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"oidc-usermodel-property-mapper",
|
|
||||||
"saml-user-property-mapper",
|
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
"saml-user-attribute-mapper",
|
"saml-user-attribute-mapper",
|
||||||
"saml-role-list-mapper",
|
"oidc-usermodel-property-mapper",
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
|
||||||
"oidc-usermodel-attribute-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
|
"saml-user-property-mapper",
|
||||||
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
|
"saml-role-list-mapper",
|
||||||
"oidc-full-name-mapper"
|
"oidc-full-name-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1694,14 +1694,14 @@
|
|||||||
"subComponents": {},
|
"subComponents": {},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
"saml-user-attribute-mapper",
|
"oidc-usermodel-attribute-mapper",
|
||||||
"oidc-full-name-mapper",
|
|
||||||
"oidc-sha256-pairwise-sub-mapper",
|
"oidc-sha256-pairwise-sub-mapper",
|
||||||
"saml-user-property-mapper",
|
|
||||||
"oidc-usermodel-property-mapper",
|
|
||||||
"saml-role-list-mapper",
|
"saml-role-list-mapper",
|
||||||
"oidc-address-mapper",
|
"oidc-address-mapper",
|
||||||
"oidc-usermodel-attribute-mapper"
|
"oidc-full-name-mapper",
|
||||||
|
"saml-user-property-mapper",
|
||||||
|
"oidc-usermodel-property-mapper",
|
||||||
|
"saml-user-attribute-mapper"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -67,7 +67,7 @@ export async function dumpContainerConfig(params: {
|
|||||||
...["--db", "dev-file"],
|
...["--db", "dev-file"],
|
||||||
...[
|
...[
|
||||||
"--db-url",
|
"--db-url",
|
||||||
"'jdbc:h2:file:/tmp/h2/keycloakdb;NON_KEYWORDS=VALUE'"
|
'"jdbc:h2:file:/tmp/h2/keycloakdb;NON_KEYWORDS=VALUE"'
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
@ -1,28 +1,20 @@
|
|||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { ParsedRealmJson } from "./ParsedRealmJson";
|
import type { ParsedRealmJson } from "./ParsedRealmJson";
|
||||||
import { getDefaultConfig } from "./defaultConfig";
|
import { getDefaultConfig } from "./defaultConfig";
|
||||||
import type { BuildContext } from "../../shared/buildContext";
|
import { TEST_APP_URL, type ThemeType, THEME_TYPES } from "../../shared/constants";
|
||||||
import { objectKeys } from "tsafe/objectKeys";
|
|
||||||
import { TEST_APP_URL } from "../../shared/constants";
|
|
||||||
import { sameFactory } from "evt/tools/inDepth/same";
|
import { sameFactory } from "evt/tools/inDepth/same";
|
||||||
|
|
||||||
export type BuildContextLike = {
|
|
||||||
themeNames: BuildContext["themeNames"];
|
|
||||||
implementedThemeTypes: BuildContext["implementedThemeTypes"];
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>;
|
|
||||||
|
|
||||||
export function prepareRealmConfig(params: {
|
export function prepareRealmConfig(params: {
|
||||||
parsedRealmJson: ParsedRealmJson;
|
parsedRealmJson: ParsedRealmJson;
|
||||||
keycloakMajorVersionNumber: number;
|
keycloakMajorVersionNumber: number;
|
||||||
buildContext: BuildContextLike;
|
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||||
}): {
|
}): {
|
||||||
realmName: string;
|
realmName: string;
|
||||||
clientName: string;
|
clientName: string;
|
||||||
username: string;
|
username: string;
|
||||||
} {
|
} {
|
||||||
const { parsedRealmJson, keycloakMajorVersionNumber, buildContext } = params;
|
const { parsedRealmJson, keycloakMajorVersionNumber, parsedKeycloakThemesJsonEntry } =
|
||||||
|
params;
|
||||||
|
|
||||||
const { username } = addOrEditTestUser({
|
const { username } = addOrEditTestUser({
|
||||||
parsedRealmJson,
|
parsedRealmJson,
|
||||||
@ -38,8 +30,7 @@ export function prepareRealmConfig(params: {
|
|||||||
|
|
||||||
enableCustomThemes({
|
enableCustomThemes({
|
||||||
parsedRealmJson,
|
parsedRealmJson,
|
||||||
themeName: buildContext.themeNames[0],
|
parsedKeycloakThemesJsonEntry
|
||||||
implementedThemeTypes: buildContext.implementedThemeTypes
|
|
||||||
});
|
});
|
||||||
|
|
||||||
enable_custom_events_listeners: {
|
enable_custom_events_listeners: {
|
||||||
@ -63,17 +54,15 @@ export function prepareRealmConfig(params: {
|
|||||||
|
|
||||||
function enableCustomThemes(params: {
|
function enableCustomThemes(params: {
|
||||||
parsedRealmJson: ParsedRealmJson;
|
parsedRealmJson: ParsedRealmJson;
|
||||||
themeName: string;
|
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||||
implementedThemeTypes: BuildContextLike["implementedThemeTypes"];
|
|
||||||
}) {
|
}) {
|
||||||
const { parsedRealmJson, themeName, implementedThemeTypes } = params;
|
const { parsedRealmJson, parsedKeycloakThemesJsonEntry } = params;
|
||||||
|
|
||||||
for (const themeType of objectKeys(implementedThemeTypes)) {
|
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||||
if (!implementedThemeTypes[themeType].isImplemented) {
|
parsedRealmJson[`${themeType}Theme` as const] =
|
||||||
continue;
|
!parsedKeycloakThemesJsonEntry.types.includes(themeType)
|
||||||
}
|
? ""
|
||||||
|
: parsedKeycloakThemesJsonEntry.name;
|
||||||
parsedRealmJson[`${themeType}Theme` as const] = themeName;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +101,6 @@ function addOrEditTestUser(params: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
newUser.username = defaultUser_default.username;
|
newUser.username = defaultUser_default.username;
|
||||||
newUser.email = defaultUser_default.email;
|
|
||||||
|
|
||||||
delete_existing_password_credential_if_any: {
|
delete_existing_password_credential_if_any: {
|
||||||
const i = newUser.credentials.findIndex(
|
const i = newUser.credentials.findIndex(
|
||||||
@ -333,7 +321,7 @@ function editAccountConsoleAndSecurityAdminConsole(params: {
|
|||||||
"claim.value": '["*"]',
|
"claim.value": '["*"]',
|
||||||
"userinfo.token.claim": "true",
|
"userinfo.token.claim": "true",
|
||||||
"id.token.claim": "false",
|
"id.token.claim": "false",
|
||||||
"lightweight.claim": "false",
|
"lightweight.claim": "true",
|
||||||
"access.token.claim": "true",
|
"access.token.claim": "true",
|
||||||
"claim.name": "allowed-origins",
|
"claim.name": "allowed-origins",
|
||||||
"jsonType.label": "JSON",
|
"jsonType.label": "JSON",
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import type { BuildContext } from "../../shared/buildContext";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { runPrettier, getIsPrettierAvailable } from "../../tools/runPrettier";
|
|
||||||
import { getDefaultConfig } from "./defaultConfig";
|
import { getDefaultConfig } from "./defaultConfig";
|
||||||
import {
|
import { prepareRealmConfig } from "./prepareRealmConfig";
|
||||||
prepareRealmConfig,
|
|
||||||
type BuildContextLike as BuildContextLike_prepareRealmConfig
|
|
||||||
} from "./prepareRealmConfig";
|
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import {
|
import {
|
||||||
join as pathJoin,
|
join as pathJoin,
|
||||||
@ -14,25 +10,30 @@ import {
|
|||||||
sep as pathSep
|
sep as pathSep
|
||||||
} from "path";
|
} from "path";
|
||||||
import { existsAsync } from "../../tools/fs.existsAsync";
|
import { existsAsync } from "../../tools/fs.existsAsync";
|
||||||
import { readRealmJsonFile, type ParsedRealmJson } from "./ParsedRealmJson";
|
import {
|
||||||
|
readRealmJsonFile,
|
||||||
|
writeRealmJsonFile,
|
||||||
|
type ParsedRealmJson
|
||||||
|
} from "./ParsedRealmJson";
|
||||||
import {
|
import {
|
||||||
dumpContainerConfig,
|
dumpContainerConfig,
|
||||||
type BuildContextLike as BuildContextLike_dumpContainerConfig
|
type BuildContextLike as BuildContextLike_dumpContainerConfig
|
||||||
} from "./dumpContainerConfig";
|
} from "./dumpContainerConfig";
|
||||||
import * as runExclusive from "run-exclusive";
|
import * as runExclusive from "run-exclusive";
|
||||||
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
|
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
|
||||||
|
import type { ThemeType } from "../../shared/constants";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
|
||||||
export type BuildContextLike = BuildContextLike_dumpContainerConfig &
|
export type BuildContextLike = BuildContextLike_dumpContainerConfig & {
|
||||||
BuildContextLike_prepareRealmConfig & {
|
projectDirPath: string;
|
||||||
projectDirPath: string;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>;
|
assert<BuildContext extends BuildContextLike ? true : false>;
|
||||||
|
|
||||||
export async function getRealmConfig(params: {
|
export async function getRealmConfig(params: {
|
||||||
keycloakMajorVersionNumber: number;
|
keycloakMajorVersionNumber: number;
|
||||||
realmJsonFilePath_userProvided: string | undefined;
|
realmJsonFilePath_userProvided: string | undefined;
|
||||||
|
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
realmJsonFilePath: string;
|
realmJsonFilePath: string;
|
||||||
@ -41,8 +42,12 @@ export async function getRealmConfig(params: {
|
|||||||
username: string;
|
username: string;
|
||||||
onRealmConfigChange: () => Promise<void>;
|
onRealmConfigChange: () => Promise<void>;
|
||||||
}> {
|
}> {
|
||||||
const { keycloakMajorVersionNumber, realmJsonFilePath_userProvided, buildContext } =
|
const {
|
||||||
params;
|
keycloakMajorVersionNumber,
|
||||||
|
realmJsonFilePath_userProvided,
|
||||||
|
parsedKeycloakThemesJsonEntry,
|
||||||
|
buildContext
|
||||||
|
} = params;
|
||||||
|
|
||||||
const realmJsonFilePath = pathJoin(
|
const realmJsonFilePath = pathJoin(
|
||||||
buildContext.projectDirPath,
|
buildContext.projectDirPath,
|
||||||
@ -68,8 +73,8 @@ export async function getRealmConfig(params: {
|
|||||||
|
|
||||||
const { clientName, realmName, username } = prepareRealmConfig({
|
const { clientName, realmName, username } = prepareRealmConfig({
|
||||||
parsedRealmJson,
|
parsedRealmJson,
|
||||||
buildContext,
|
keycloakMajorVersionNumber,
|
||||||
keycloakMajorVersionNumber
|
parsedKeycloakThemesJsonEntry
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -80,22 +85,11 @@ export async function getRealmConfig(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeRealmJsonFile = async (params: { parsedRealmJson: ParsedRealmJson }) => {
|
await writeRealmJsonFile({
|
||||||
const { parsedRealmJson } = params;
|
realmJsonFilePath,
|
||||||
|
parsedRealmJson,
|
||||||
let sourceCode = JSON.stringify(parsedRealmJson, null, 2);
|
keycloakMajorVersionNumber
|
||||||
|
});
|
||||||
if (await getIsPrettierAvailable()) {
|
|
||||||
sourceCode = await runPrettier({
|
|
||||||
sourceCode,
|
|
||||||
filePath: realmJsonFilePath
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(realmJsonFilePath, sourceCode);
|
|
||||||
};
|
|
||||||
|
|
||||||
await writeRealmJsonFile({ parsedRealmJson });
|
|
||||||
|
|
||||||
const { onRealmConfigChange } = (() => {
|
const { onRealmConfigChange } = (() => {
|
||||||
const run = runExclusive.build(async () => {
|
const run = runExclusive.build(async () => {
|
||||||
@ -119,7 +113,11 @@ export async function getRealmConfig(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeRealmJsonFile({ parsedRealmJson });
|
await writeRealmJsonFile({
|
||||||
|
realmJsonFilePath,
|
||||||
|
parsedRealmJson,
|
||||||
|
keycloakMajorVersionNumber
|
||||||
|
});
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
[
|
[
|
||||||
|
@ -4,7 +4,8 @@ import {
|
|||||||
CONTAINER_NAME,
|
CONTAINER_NAME,
|
||||||
KEYCLOAKIFY_SPA_DEV_SERVER_PORT,
|
KEYCLOAKIFY_SPA_DEV_SERVER_PORT,
|
||||||
KEYCLOAKIFY_LOGIN_JAR_BASENAME,
|
KEYCLOAKIFY_LOGIN_JAR_BASENAME,
|
||||||
TEST_APP_URL
|
TEST_APP_URL,
|
||||||
|
ThemeType
|
||||||
} from "../shared/constants";
|
} from "../shared/constants";
|
||||||
import { SemVer } from "../tools/SemVer";
|
import { SemVer } from "../tools/SemVer";
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
@ -34,6 +35,7 @@ import { startViteDevServer } from "./startViteDevServer";
|
|||||||
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
|
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
|
||||||
import { getSupportedDockerImageTags } from "./getSupportedDockerImageTags";
|
import { getSupportedDockerImageTags } from "./getSupportedDockerImageTags";
|
||||||
import { getRealmConfig } from "./realmConfig";
|
import { getRealmConfig } from "./realmConfig";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
export async function command(params: {
|
export async function command(params: {
|
||||||
buildContext: BuildContext;
|
buildContext: BuildContext;
|
||||||
@ -51,11 +53,17 @@ export async function command(params: {
|
|||||||
.execSync("docker --version", {
|
.execSync("docker --version", {
|
||||||
stdio: ["ignore", "pipe", "ignore"]
|
stdio: ["ignore", "pipe", "ignore"]
|
||||||
})
|
})
|
||||||
?.toString("utf8");
|
.toString("utf8");
|
||||||
} catch {}
|
} catch {
|
||||||
|
commandOutput = "";
|
||||||
|
}
|
||||||
|
|
||||||
if (commandOutput?.includes("Docker") || commandOutput?.includes("podman")) {
|
commandOutput = commandOutput.trim().toLowerCase();
|
||||||
break exit_if_docker_not_installed;
|
|
||||||
|
for (const term of ["docker", "podman"]) {
|
||||||
|
if (commandOutput.includes(term)) {
|
||||||
|
break exit_if_docker_not_installed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@ -97,7 +105,7 @@ export async function command(params: {
|
|||||||
|
|
||||||
const { cliCommandOptions, buildContext } = params;
|
const { cliCommandOptions, buildContext } = params;
|
||||||
|
|
||||||
const availableTags = await getSupportedDockerImageTags({
|
const { allSupportedTags, latestMajorTags } = await getSupportedDockerImageTags({
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -105,7 +113,7 @@ export async function command(params: {
|
|||||||
if (cliCommandOptions.keycloakVersion !== undefined) {
|
if (cliCommandOptions.keycloakVersion !== undefined) {
|
||||||
const cliCommandOptions_keycloakVersion = cliCommandOptions.keycloakVersion;
|
const cliCommandOptions_keycloakVersion = cliCommandOptions.keycloakVersion;
|
||||||
|
|
||||||
const tag = availableTags.find(tag =>
|
const tag = allSupportedTags.find(tag =>
|
||||||
tag.startsWith(cliCommandOptions_keycloakVersion)
|
tag.startsWith(cliCommandOptions_keycloakVersion)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -142,15 +150,96 @@ export async function command(params: {
|
|||||||
].join("\n")
|
].join("\n")
|
||||||
);
|
);
|
||||||
|
|
||||||
const { value: tag } = await cliSelect<string>({
|
const tag_userSelected = await (async () => {
|
||||||
values: availableTags
|
let tag: string;
|
||||||
}).catch(() => {
|
|
||||||
process.exit(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`→ ${tag}`);
|
let latestMajorTags_copy = [...latestMajorTags];
|
||||||
|
|
||||||
return { dockerImageTag: tag };
|
while (true) {
|
||||||
|
const { value } = await cliSelect<string>({
|
||||||
|
values: latestMajorTags_copy
|
||||||
|
}).catch(() => {
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
tag = value;
|
||||||
|
|
||||||
|
{
|
||||||
|
const doImplementAccountMpa =
|
||||||
|
buildContext.implementedThemeTypes.account.isImplemented &&
|
||||||
|
buildContext.implementedThemeTypes.account.type === "Multi-Page";
|
||||||
|
|
||||||
|
if (doImplementAccountMpa && tag.startsWith("22.")) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
`You are implementing a Multi-Page Account theme. Keycloak 22 is not supported, select another version`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
latestMajorTags_copy = latestMajorTags_copy.filter(
|
||||||
|
tag => !tag.startsWith("22.")
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const readMajor = (tag: string) => {
|
||||||
|
const major = parseInt(tag.split(".")[0]);
|
||||||
|
assert(!isNaN(major));
|
||||||
|
return major;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const major = readMajor(tag);
|
||||||
|
|
||||||
|
const doImplementAdminTheme =
|
||||||
|
buildContext.implementedThemeTypes.admin.isImplemented;
|
||||||
|
|
||||||
|
const getIsSupported = (major: number) => major >= 23;
|
||||||
|
|
||||||
|
if (doImplementAdminTheme && !getIsSupported(major)) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
`You are implementing an Admin theme. Only Keycloak 23 and later are supported, select another version`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
latestMajorTags_copy = latestMajorTags_copy.filter(tag =>
|
||||||
|
getIsSupported(readMajor(tag))
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const doImplementAccountSpa =
|
||||||
|
buildContext.implementedThemeTypes.account.isImplemented &&
|
||||||
|
buildContext.implementedThemeTypes.account.type === "Single-Page";
|
||||||
|
|
||||||
|
const major = readMajor(tag);
|
||||||
|
|
||||||
|
const getIsSupported = (major: number) => major >= 19;
|
||||||
|
|
||||||
|
if (doImplementAccountSpa && !getIsSupported(major)) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
`You are implementing a Single-Page Account theme. Only Keycloak 19 and later are supported, select another version`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
latestMajorTags_copy = latestMajorTags_copy.filter(tag =>
|
||||||
|
getIsSupported(readMajor(tag))
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
})();
|
||||||
|
|
||||||
|
console.log(`→ ${tag_userSelected}`);
|
||||||
|
|
||||||
|
return { dockerImageTag: tag_userSelected };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const keycloakMajorVersionNumber = (() => {
|
const keycloakMajorVersionNumber = (() => {
|
||||||
@ -189,32 +278,6 @@ export async function command(params: {
|
|||||||
return wrap.majorVersionNumber;
|
return wrap.majorVersionNumber;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const { clientName, onRealmConfigChange, realmJsonFilePath, realmName, username } =
|
|
||||||
await getRealmConfig({
|
|
||||||
keycloakMajorVersionNumber,
|
|
||||||
realmJsonFilePath_userProvided: await (async () => {
|
|
||||||
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
|
||||||
return getAbsoluteAndInOsFormatPath({
|
|
||||||
pathIsh: cliCommandOptions.realmJsonFilePath,
|
|
||||||
cwd: process.cwd()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) {
|
|
||||||
assert(
|
|
||||||
await existsAsync(
|
|
||||||
buildContext.startKeycloakOptions.realmJsonFilePath
|
|
||||||
),
|
|
||||||
`${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist`
|
|
||||||
);
|
|
||||||
return buildContext.startKeycloakOptions.realmJsonFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
})(),
|
|
||||||
buildContext
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const { isAppBuildSuccess } = await appBuild({
|
const { isAppBuildSuccess } = await appBuild({
|
||||||
buildContext
|
buildContext
|
||||||
@ -295,10 +358,24 @@ export async function command(params: {
|
|||||||
))
|
))
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let parsedKeycloakThemesJson = id<
|
||||||
|
{ themes: { name: string; types: (ThemeType | "email")[] }[] } | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
async function extractThemeResourcesFromJar() {
|
async function extractThemeResourcesFromJar() {
|
||||||
await extractArchive({
|
await extractArchive({
|
||||||
archiveFilePath: jarFilePath,
|
archiveFilePath: jarFilePath,
|
||||||
onArchiveFile: async ({ relativeFilePathInArchive, writeFile }) => {
|
onArchiveFile: async ({ relativeFilePathInArchive, writeFile, readFile }) => {
|
||||||
|
if (
|
||||||
|
relativeFilePathInArchive ===
|
||||||
|
pathJoin("META-INF", "keycloak-themes.json") &&
|
||||||
|
parsedKeycloakThemesJson === undefined
|
||||||
|
) {
|
||||||
|
parsedKeycloakThemesJson = JSON.parse(
|
||||||
|
(await readFile()).toString("utf8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isInside({ dirPath: "theme", filePath: relativeFilePathInArchive })) {
|
if (isInside({ dirPath: "theme", filePath: relativeFilePathInArchive })) {
|
||||||
await writeFile({
|
await writeFile({
|
||||||
filePath: pathJoin(
|
filePath: pathJoin(
|
||||||
@ -320,6 +397,43 @@ export async function command(params: {
|
|||||||
|
|
||||||
await extractThemeResourcesFromJar();
|
await extractThemeResourcesFromJar();
|
||||||
|
|
||||||
|
assert(parsedKeycloakThemesJson !== undefined);
|
||||||
|
|
||||||
|
const { clientName, onRealmConfigChange, realmJsonFilePath, realmName, username } =
|
||||||
|
await getRealmConfig({
|
||||||
|
keycloakMajorVersionNumber,
|
||||||
|
parsedKeycloakThemesJsonEntry: (() => {
|
||||||
|
const entry = parsedKeycloakThemesJson.themes.find(
|
||||||
|
({ name }) => name === buildContext.themeNames[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(entry !== undefined);
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
})(),
|
||||||
|
realmJsonFilePath_userProvided: await (async () => {
|
||||||
|
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
||||||
|
return getAbsoluteAndInOsFormatPath({
|
||||||
|
pathIsh: cliCommandOptions.realmJsonFilePath,
|
||||||
|
cwd: process.cwd()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) {
|
||||||
|
assert(
|
||||||
|
await existsAsync(
|
||||||
|
buildContext.startKeycloakOptions.realmJsonFilePath
|
||||||
|
),
|
||||||
|
`${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist`
|
||||||
|
);
|
||||||
|
return buildContext.startKeycloakOptions.realmJsonFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
})(),
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
const jarFilePath_cacheDir = pathJoin(
|
const jarFilePath_cacheDir = pathJoin(
|
||||||
buildContext.cacheDirPath,
|
buildContext.cacheDirPath,
|
||||||
pathBasename(jarFilePath)
|
pathBasename(jarFilePath)
|
||||||
@ -623,42 +737,90 @@ export async function command(params: {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.on("all", async (...[, filePath]) => {
|
.on("all", async (...[, filePath]) => {
|
||||||
ignore_account_spa: {
|
ignore_path_covered_by_hmr: {
|
||||||
const doImplementAccountSpa =
|
if (filePath.endsWith(".properties")) {
|
||||||
buildContext.implementedThemeTypes.account.isImplemented &&
|
break ignore_path_covered_by_hmr;
|
||||||
buildContext.implementedThemeTypes.account.type === "Single-Page";
|
|
||||||
|
|
||||||
if (!doImplementAccountSpa) {
|
|
||||||
break ignore_account_spa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!doStartDevServer) {
|
||||||
!isInside({
|
break ignore_path_covered_by_hmr;
|
||||||
dirPath: pathJoin(buildContext.themeSrcDirPath, "account"),
|
|
||||||
filePath
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
break ignore_account_spa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
ignore_account_spa: {
|
||||||
}
|
const doImplementAccountSpa =
|
||||||
|
buildContext.implementedThemeTypes.account.isImplemented &&
|
||||||
|
buildContext.implementedThemeTypes.account.type ===
|
||||||
|
"Single-Page";
|
||||||
|
|
||||||
ignore_admin: {
|
if (!doImplementAccountSpa) {
|
||||||
if (!buildContext.implementedThemeTypes.admin.isImplemented) {
|
break ignore_account_spa;
|
||||||
break ignore_admin;
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isInside({
|
||||||
|
dirPath: pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
"account"
|
||||||
|
),
|
||||||
|
filePath
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break ignore_account_spa;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
ignore_admin: {
|
||||||
!isInside({
|
if (!buildContext.implementedThemeTypes.admin.isImplemented) {
|
||||||
dirPath: pathJoin(buildContext.themeSrcDirPath, "admin"),
|
break ignore_admin;
|
||||||
filePath
|
}
|
||||||
})
|
|
||||||
) {
|
if (
|
||||||
break ignore_admin;
|
!isInside({
|
||||||
|
dirPath: pathJoin(buildContext.themeSrcDirPath, "admin"),
|
||||||
|
filePath
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break ignore_admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
ignore_patternfly: {
|
||||||
|
if (
|
||||||
|
!isInside({
|
||||||
|
dirPath: pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
"shared",
|
||||||
|
"@patternfly"
|
||||||
|
),
|
||||||
|
filePath
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break ignore_patternfly;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ignore_keycloak_ui_shared: {
|
||||||
|
if (
|
||||||
|
!isInside({
|
||||||
|
dirPath: pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
"shared",
|
||||||
|
"keycloak-ui-shared"
|
||||||
|
),
|
||||||
|
filePath
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break ignore_keycloak_ui_shared;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Detected changes in ${filePath}`);
|
console.log(`Detected changes in ${filePath}`);
|
||||||
|
@ -10,14 +10,15 @@ import { crawlAsync } from "../tools/crawlAsync";
|
|||||||
import { getIsPrettierAvailable, getPrettier } from "../tools/runPrettier";
|
import { getIsPrettierAvailable, getPrettier } from "../tools/runPrettier";
|
||||||
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||||
import {
|
import {
|
||||||
getUiModuleFileSourceCodeReadyToBeCopied,
|
getExtensionModuleFileSourceCodeReadyToBeCopied,
|
||||||
type BuildContextLike as BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied
|
type BuildContextLike as BuildContextLike_getExtensionModuleFileSourceCodeReadyToBeCopied
|
||||||
} from "./getUiModuleFileSourceCodeReadyToBeCopied";
|
} from "./getExtensionModuleFileSourceCodeReadyToBeCopied";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import { KEYCLOAK_THEME } from "../shared/constants";
|
import { KEYCLOAK_THEME } from "../shared/constants";
|
||||||
import { exclude } from "tsafe/exclude";
|
import { exclude } from "tsafe/exclude";
|
||||||
|
import { isAmong } from "tsafe/isAmong";
|
||||||
|
|
||||||
export type UiModuleMeta = {
|
export type ExtensionModuleMeta = {
|
||||||
moduleName: string;
|
moduleName: string;
|
||||||
version: string;
|
version: string;
|
||||||
files: {
|
files: {
|
||||||
@ -28,8 +29,8 @@ export type UiModuleMeta = {
|
|||||||
peerDependencies: Record<string, string>;
|
peerDependencies: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const zUiModuleMeta = (() => {
|
const zExtensionModuleMeta = (() => {
|
||||||
type ExpectedType = UiModuleMeta;
|
type ExpectedType = ExtensionModuleMeta;
|
||||||
|
|
||||||
const zTargetType = z.object({
|
const zTargetType = z.object({
|
||||||
moduleName: z.string(),
|
moduleName: z.string(),
|
||||||
@ -55,7 +56,7 @@ type ParsedCacheFile = {
|
|||||||
keycloakifyVersion: string;
|
keycloakifyVersion: string;
|
||||||
prettierConfigHash: string | null;
|
prettierConfigHash: string | null;
|
||||||
thisFilePath: string;
|
thisFilePath: string;
|
||||||
uiModuleMetas: UiModuleMeta[];
|
extensionModuleMetas: ExtensionModuleMeta[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const zParsedCacheFile = (() => {
|
const zParsedCacheFile = (() => {
|
||||||
@ -65,7 +66,7 @@ const zParsedCacheFile = (() => {
|
|||||||
keycloakifyVersion: z.string(),
|
keycloakifyVersion: z.string(),
|
||||||
prettierConfigHash: z.union([z.string(), z.null()]),
|
prettierConfigHash: z.union([z.string(), z.null()]),
|
||||||
thisFilePath: z.string(),
|
thisFilePath: z.string(),
|
||||||
uiModuleMetas: z.array(zUiModuleMeta)
|
extensionModuleMetas: z.array(zExtensionModuleMeta)
|
||||||
});
|
});
|
||||||
|
|
||||||
type InferredType = z.infer<typeof zTargetType>;
|
type InferredType = z.infer<typeof zTargetType>;
|
||||||
@ -75,10 +76,10 @@ const zParsedCacheFile = (() => {
|
|||||||
return id<z.ZodType<ExpectedType>>(zTargetType);
|
return id<z.ZodType<ExpectedType>>(zTargetType);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const CACHE_FILE_RELATIVE_PATH = pathJoin("ui-modules", "cache.json");
|
const CACHE_FILE_RELATIVE_PATH = pathJoin("extension-modules", "cache.json");
|
||||||
|
|
||||||
export type BuildContextLike =
|
export type BuildContextLike =
|
||||||
BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied & {
|
BuildContextLike_getExtensionModuleFileSourceCodeReadyToBeCopied & {
|
||||||
cacheDirPath: string;
|
cacheDirPath: string;
|
||||||
packageJsonFilePath: string;
|
packageJsonFilePath: string;
|
||||||
projectDirPath: string;
|
projectDirPath: string;
|
||||||
@ -86,9 +87,9 @@ export type BuildContextLike =
|
|||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export async function getUiModuleMetas(params: {
|
export async function getExtensionModuleMetas(params: {
|
||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
}): Promise<UiModuleMeta[]> {
|
}): Promise<ExtensionModuleMeta[]> {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH);
|
const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH);
|
||||||
@ -105,10 +106,9 @@ export async function getUiModuleMetas(params: {
|
|||||||
return configHash;
|
return configHash;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const installedUiModules = await (async () => {
|
const installedExtensionModules = await (async () => {
|
||||||
const installedModulesWithKeycloakifyInTheName = await listInstalledModules({
|
const installedModulesWithKeycloakifyInTheName = await listInstalledModules({
|
||||||
packageJsonFilePath: buildContext.packageJsonFilePath,
|
packageJsonFilePath: buildContext.packageJsonFilePath,
|
||||||
projectDirPath: buildContext.packageJsonFilePath,
|
|
||||||
filter: ({ moduleName }) =>
|
filter: ({ moduleName }) =>
|
||||||
moduleName.includes("keycloakify") && moduleName !== "keycloakify"
|
moduleName.includes("keycloakify") && moduleName !== "keycloakify"
|
||||||
});
|
});
|
||||||
@ -133,7 +133,7 @@ export async function getUiModuleMetas(params: {
|
|||||||
return await fsPr.readFile(cacheFilePath);
|
return await fsPr.readFile(cacheFilePath);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const uiModuleMetas_cacheUpToDate: UiModuleMeta[] = await (async () => {
|
const extensionModuleMetas_cacheUpToDate: ExtensionModuleMeta[] = await (async () => {
|
||||||
const parsedCacheFile: ParsedCacheFile | undefined = await (async () => {
|
const parsedCacheFile: ParsedCacheFile | undefined = await (async () => {
|
||||||
if (cacheContent === undefined) {
|
if (cacheContent === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -176,45 +176,51 @@ export async function getUiModuleMetas(params: {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiModuleMetas_cacheUpToDate = parsedCacheFile.uiModuleMetas.filter(
|
const extensionModuleMetas_cacheUpToDate =
|
||||||
uiModuleMeta => {
|
parsedCacheFile.extensionModuleMetas.filter(extensionModuleMeta => {
|
||||||
const correspondingInstalledUiModule = installedUiModules.find(
|
const correspondingInstalledExtensionModule =
|
||||||
installedUiModule =>
|
installedExtensionModules.find(
|
||||||
installedUiModule.moduleName === uiModuleMeta.moduleName
|
installedExtensionModule =>
|
||||||
);
|
installedExtensionModule.moduleName ===
|
||||||
|
extensionModuleMeta.moduleName
|
||||||
|
);
|
||||||
|
|
||||||
if (correspondingInstalledUiModule === undefined) {
|
if (correspondingInstalledExtensionModule === undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return correspondingInstalledUiModule.version === uiModuleMeta.version;
|
return (
|
||||||
}
|
correspondingInstalledExtensionModule.version ===
|
||||||
);
|
extensionModuleMeta.version
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return uiModuleMetas_cacheUpToDate;
|
return extensionModuleMetas_cacheUpToDate;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const uiModuleMetas = await Promise.all(
|
const extensionModuleMetas = await Promise.all(
|
||||||
installedUiModules.map(
|
installedExtensionModules.map(
|
||||||
async ({
|
async ({
|
||||||
moduleName,
|
moduleName,
|
||||||
version,
|
version,
|
||||||
peerDependencies,
|
peerDependencies,
|
||||||
dirPath
|
dirPath
|
||||||
}): Promise<UiModuleMeta> => {
|
}): Promise<ExtensionModuleMeta> => {
|
||||||
use_cache: {
|
use_cache: {
|
||||||
const uiModuleMeta_cache = uiModuleMetas_cacheUpToDate.find(
|
const extensionModuleMeta_cache =
|
||||||
uiModuleMeta => uiModuleMeta.moduleName === moduleName
|
extensionModuleMetas_cacheUpToDate.find(
|
||||||
);
|
extensionModuleMeta =>
|
||||||
|
extensionModuleMeta.moduleName === moduleName
|
||||||
|
);
|
||||||
|
|
||||||
if (uiModuleMeta_cache === undefined) {
|
if (extensionModuleMeta_cache === undefined) {
|
||||||
break use_cache;
|
break use_cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
return uiModuleMeta_cache;
|
return extensionModuleMeta_cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
const files: UiModuleMeta["files"] = [];
|
const files: ExtensionModuleMeta["files"] = [];
|
||||||
|
|
||||||
{
|
{
|
||||||
const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME);
|
const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME);
|
||||||
@ -224,13 +230,13 @@ export async function getUiModuleMetas(params: {
|
|||||||
returnedPathsType: "relative to dirPath",
|
returnedPathsType: "relative to dirPath",
|
||||||
onFileFound: async fileRelativePath => {
|
onFileFound: async fileRelativePath => {
|
||||||
const sourceCode =
|
const sourceCode =
|
||||||
await getUiModuleFileSourceCodeReadyToBeCopied({
|
await getExtensionModuleFileSourceCodeReadyToBeCopied({
|
||||||
buildContext,
|
buildContext,
|
||||||
fileRelativePath,
|
fileRelativePath,
|
||||||
isForEjection: false,
|
isOwnershipAction: false,
|
||||||
uiModuleDirPath: dirPath,
|
extensionModuleDirPath: dirPath,
|
||||||
uiModuleName: moduleName,
|
extensionModuleName: moduleName,
|
||||||
uiModuleVersion: version
|
extensionModuleVersion: version
|
||||||
});
|
});
|
||||||
|
|
||||||
const hash = computeHash(sourceCode);
|
const hash = computeHash(sourceCode);
|
||||||
@ -260,11 +266,16 @@ export async function getUiModuleMetas(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return id<UiModuleMeta>({
|
return id<ExtensionModuleMeta>({
|
||||||
moduleName,
|
moduleName,
|
||||||
version,
|
version,
|
||||||
files,
|
files,
|
||||||
peerDependencies
|
peerDependencies: Object.fromEntries(
|
||||||
|
Object.entries(peerDependencies).filter(
|
||||||
|
([moduleName]) =>
|
||||||
|
!isAmong(["react", "@types/react"], moduleName)
|
||||||
|
)
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -275,7 +286,7 @@ export async function getUiModuleMetas(params: {
|
|||||||
keycloakifyVersion,
|
keycloakifyVersion,
|
||||||
prettierConfigHash,
|
prettierConfigHash,
|
||||||
thisFilePath: cacheFilePath,
|
thisFilePath: cacheFilePath,
|
||||||
uiModuleMetas
|
extensionModuleMetas
|
||||||
});
|
});
|
||||||
|
|
||||||
const cacheContent_new = Buffer.from(
|
const cacheContent_new = Buffer.from(
|
||||||
@ -300,7 +311,7 @@ export async function getUiModuleMetas(params: {
|
|||||||
await fsPr.writeFile(cacheFilePath, cacheContent_new);
|
await fsPr.writeFile(cacheFilePath, cacheContent_new);
|
||||||
}
|
}
|
||||||
|
|
||||||
return uiModuleMetas;
|
return extensionModuleMetas;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeHash(data: Buffer) {
|
export function computeHash(data: Buffer) {
|
@ -0,0 +1,151 @@
|
|||||||
|
import { getIsPrettierAvailable, runPrettier } from "../tools/runPrettier";
|
||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
import { join as pathJoin, sep as pathSep } from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
import { KEYCLOAK_THEME } from "../shared/constants";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
themeSrcDirPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export async function getExtensionModuleFileSourceCodeReadyToBeCopied(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
fileRelativePath: string;
|
||||||
|
isOwnershipAction: boolean;
|
||||||
|
extensionModuleDirPath: string;
|
||||||
|
extensionModuleName: string;
|
||||||
|
extensionModuleVersion: string;
|
||||||
|
}): Promise<Buffer> {
|
||||||
|
const {
|
||||||
|
buildContext,
|
||||||
|
extensionModuleDirPath,
|
||||||
|
fileRelativePath,
|
||||||
|
isOwnershipAction,
|
||||||
|
extensionModuleName,
|
||||||
|
extensionModuleVersion
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
let sourceCode = (
|
||||||
|
await fsPr.readFile(
|
||||||
|
pathJoin(extensionModuleDirPath, KEYCLOAK_THEME, fileRelativePath)
|
||||||
|
)
|
||||||
|
).toString("utf8");
|
||||||
|
|
||||||
|
sourceCode = addCommentToSourceCode({
|
||||||
|
sourceCode,
|
||||||
|
fileRelativePath,
|
||||||
|
commentLines: (() => {
|
||||||
|
const path = fileRelativePath.split(pathSep).join("/");
|
||||||
|
|
||||||
|
return isOwnershipAction
|
||||||
|
? [
|
||||||
|
`This file has been claimed for ownership from ${extensionModuleName} version ${extensionModuleVersion}.`,
|
||||||
|
`To relinquish ownership and restore this file to its original content, run the following command:`,
|
||||||
|
``,
|
||||||
|
`$ npx keycloakify own --path "${path}" --revert`
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
`WARNING: Before modifying this file, run the following command:`,
|
||||||
|
``,
|
||||||
|
`$ npx keycloakify own --path "${path}"`,
|
||||||
|
``,
|
||||||
|
`This file is provided by ${extensionModuleName} version ${extensionModuleVersion}.`,
|
||||||
|
`It was copied into your repository by the postinstall script: \`keycloakify sync-extensions\`.`
|
||||||
|
];
|
||||||
|
})()
|
||||||
|
});
|
||||||
|
|
||||||
|
const destFilePath = pathJoin(buildContext.themeSrcDirPath, fileRelativePath);
|
||||||
|
|
||||||
|
format: {
|
||||||
|
if (!(await getIsPrettierAvailable())) {
|
||||||
|
break format;
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceCode = await runPrettier({
|
||||||
|
filePath: destFilePath,
|
||||||
|
sourceCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.from(sourceCode, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCommentToSourceCode(params: {
|
||||||
|
sourceCode: string;
|
||||||
|
fileRelativePath: string;
|
||||||
|
commentLines: string[];
|
||||||
|
}): string {
|
||||||
|
const { sourceCode, fileRelativePath, commentLines } = params;
|
||||||
|
|
||||||
|
const toResult = (comment: string) => {
|
||||||
|
return [comment, ``, sourceCode].join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const ext of [".ts", ".tsx", ".css", ".less", ".sass", ".js", ".jsx"]) {
|
||||||
|
if (!fileRelativePath.endsWith(ext)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return toResult(
|
||||||
|
[`/**`, ...commentLines.map(line => ` * ${line}`), ` */`].join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileRelativePath.endsWith(".properties")) {
|
||||||
|
return toResult(commentLines.map(line => `# ${line}`).join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileRelativePath.endsWith(".ftl")) {
|
||||||
|
const comment = [`<#--`, ...commentLines.map(line => ` ${line}`), `-->`].join(
|
||||||
|
"\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sourceCode.trim().startsWith("<#ftl")) {
|
||||||
|
const [first, ...rest] = sourceCode.split(">");
|
||||||
|
|
||||||
|
const last = rest.join(">");
|
||||||
|
|
||||||
|
return [`${first}>`, comment, last].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return toResult(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) {
|
||||||
|
const comment = [
|
||||||
|
`<!--`,
|
||||||
|
...commentLines.map(
|
||||||
|
line =>
|
||||||
|
` ${line
|
||||||
|
.replace("--path", "-t")
|
||||||
|
.replace("--revert", "-r")
|
||||||
|
.replace("Before modifying", "Before modifying or replacing")}`
|
||||||
|
),
|
||||||
|
`-->`
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
if (fileRelativePath.endsWith(".html") && sourceCode.trim().startsWith("<!")) {
|
||||||
|
const [first, ...rest] = sourceCode.split(">");
|
||||||
|
|
||||||
|
const last = rest.join(">");
|
||||||
|
|
||||||
|
return [`${first}>`, comment, last].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileRelativePath.endsWith(".svg") && sourceCode.trim().startsWith("<?")) {
|
||||||
|
const [first, ...rest] = sourceCode.split("?>");
|
||||||
|
|
||||||
|
const last = rest.join("?>");
|
||||||
|
|
||||||
|
return [`${first}?>`, comment, last].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return toResult(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceCode;
|
||||||
|
}
|
1
src/bin/sync-extensions/index.ts
Normal file
1
src/bin/sync-extensions/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./sync-extension";
|
@ -1,6 +1,6 @@
|
|||||||
import { assert, type Equals, is } from "tsafe/assert";
|
import { assert, type Equals, is } from "tsafe/assert";
|
||||||
import type { BuildContext } from "../shared/buildContext";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import type { UiModuleMeta } from "./uiModuleMeta";
|
import type { ExtensionModuleMeta } from "./extensionModuleMeta";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
import * as fsPr from "fs/promises";
|
import * as fsPr from "fs/promises";
|
||||||
@ -16,29 +16,29 @@ export type BuildContextLike = {
|
|||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export type UiModuleMetaLike = {
|
export type ExtensionModuleMetaLike = {
|
||||||
moduleName: string;
|
moduleName: string;
|
||||||
peerDependencies: Record<string, string>;
|
peerDependencies: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<UiModuleMeta extends UiModuleMetaLike ? true : false>();
|
assert<ExtensionModuleMeta extends ExtensionModuleMetaLike ? true : false>();
|
||||||
|
|
||||||
export async function installUiModulesPeerDependencies(params: {
|
export async function installExtensionModulesPeerDependencies(params: {
|
||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
uiModuleMetas: UiModuleMetaLike[];
|
extensionModuleMetas: ExtensionModuleMetaLike[];
|
||||||
}): Promise<void | never> {
|
}): Promise<void | never> {
|
||||||
const { buildContext, uiModuleMetas } = params;
|
const { buildContext, extensionModuleMetas } = params;
|
||||||
|
|
||||||
const { uiModulesPerDependencies } = (() => {
|
const { extensionModulesPerDependencies } = (() => {
|
||||||
const uiModulesPerDependencies: Record<string, string> = {};
|
const extensionModulesPerDependencies: Record<string, string> = {};
|
||||||
|
|
||||||
for (const { peerDependencies } of uiModuleMetas) {
|
for (const { peerDependencies } of extensionModuleMetas) {
|
||||||
for (const [peerDependencyName, versionRange_candidate] of Object.entries(
|
for (const [peerDependencyName, versionRange_candidate] of Object.entries(
|
||||||
peerDependencies
|
peerDependencies
|
||||||
)) {
|
)) {
|
||||||
const versionRange = (() => {
|
const versionRange = (() => {
|
||||||
const versionRange_current =
|
const versionRange_current =
|
||||||
uiModulesPerDependencies[peerDependencyName];
|
extensionModulesPerDependencies[peerDependencyName];
|
||||||
|
|
||||||
if (versionRange_current === undefined) {
|
if (versionRange_current === undefined) {
|
||||||
return versionRange_candidate;
|
return versionRange_candidate;
|
||||||
@ -76,11 +76,11 @@ export async function installUiModulesPeerDependencies(params: {
|
|||||||
return versionRange;
|
return versionRange;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
uiModulesPerDependencies[peerDependencyName] = versionRange;
|
extensionModulesPerDependencies[peerDependencyName] = versionRange;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { uiModulesPerDependencies };
|
return { extensionModulesPerDependencies };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const parsedPackageJson = await (async () => {
|
const parsedPackageJson = await (async () => {
|
||||||
@ -117,7 +117,9 @@ export async function installUiModulesPeerDependencies(params: {
|
|||||||
|
|
||||||
const parsedPackageJson_before = JSON.parse(JSON.stringify(parsedPackageJson));
|
const parsedPackageJson_before = JSON.parse(JSON.stringify(parsedPackageJson));
|
||||||
|
|
||||||
for (const [moduleName, versionRange] of Object.entries(uiModulesPerDependencies)) {
|
for (const [moduleName, versionRange] of Object.entries(
|
||||||
|
extensionModulesPerDependencies
|
||||||
|
)) {
|
||||||
if (moduleName.startsWith("@types/")) {
|
if (moduleName.startsWith("@types/")) {
|
||||||
(parsedPackageJson.devDependencies ??= {})[moduleName] = versionRange;
|
(parsedPackageJson.devDependencies ??= {})[moduleName] = versionRange;
|
||||||
continue;
|
continue;
|
||||||
@ -149,7 +151,7 @@ export async function installUiModulesPeerDependencies(params: {
|
|||||||
|
|
||||||
await fsPr.writeFile(buildContext.packageJsonFilePath, packageJsonContentStr);
|
await fsPr.writeFile(buildContext.packageJsonFilePath, packageJsonContentStr);
|
||||||
|
|
||||||
npmInstall({
|
await npmInstall({
|
||||||
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||||
});
|
});
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
|||||||
} from "path";
|
} from "path";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { BuildContext } from "../shared/buildContext";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import type { UiModuleMeta } from "./uiModuleMeta";
|
import type { ExtensionModuleMeta } from "./extensionModuleMeta";
|
||||||
import { existsAsync } from "../tools/fs.existsAsync";
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||||
|
|
||||||
@ -17,17 +17,17 @@ export type BuildContextLike = {
|
|||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
const DELIMITER_START = `# === Ejected files start ===`;
|
const DELIMITER_START = `# === Owned files start ===`;
|
||||||
const DELIMITER_END = `# === Ejected files end =====`;
|
const DELIMITER_END = `# === Owned files end =====`;
|
||||||
|
|
||||||
export async function writeManagedGitignoreFile(params: {
|
export async function writeManagedGitignoreFile(params: {
|
||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
uiModuleMetas: UiModuleMeta[];
|
extensionModuleMetas: ExtensionModuleMeta[];
|
||||||
ejectedFilesRelativePaths: string[];
|
ownedFilesRelativePaths: string[];
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { buildContext, uiModuleMetas, ejectedFilesRelativePaths } = params;
|
const { buildContext, extensionModuleMetas, ownedFilesRelativePaths } = params;
|
||||||
|
|
||||||
if (uiModuleMetas.length === 0) {
|
if (extensionModuleMetas.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,19 +38,19 @@ export async function writeManagedGitignoreFile(params: {
|
|||||||
`# This file is managed by Keycloakify, do not edit it manually.`,
|
`# This file is managed by Keycloakify, do not edit it manually.`,
|
||||||
``,
|
``,
|
||||||
DELIMITER_START,
|
DELIMITER_START,
|
||||||
...ejectedFilesRelativePaths
|
...ownedFilesRelativePaths
|
||||||
.map(fileRelativePath => fileRelativePath.split(pathSep).join("/"))
|
.map(fileRelativePath => fileRelativePath.split(pathSep).join("/"))
|
||||||
.map(line => `# ${line}`),
|
.map(line => `# ${line}`),
|
||||||
DELIMITER_END,
|
DELIMITER_END,
|
||||||
``,
|
``,
|
||||||
...uiModuleMetas
|
...extensionModuleMetas
|
||||||
.map(uiModuleMeta => [
|
.map(extensionModuleMeta => [
|
||||||
`# === ${uiModuleMeta.moduleName} v${uiModuleMeta.version} ===`,
|
`# === ${extensionModuleMeta.moduleName} v${extensionModuleMeta.version} ===`,
|
||||||
...uiModuleMeta.files
|
...extensionModuleMeta.files
|
||||||
.map(({ fileRelativePath }) => fileRelativePath)
|
.map(({ fileRelativePath }) => fileRelativePath)
|
||||||
.filter(
|
.filter(
|
||||||
fileRelativePath =>
|
fileRelativePath =>
|
||||||
!ejectedFilesRelativePaths.includes(fileRelativePath)
|
!ownedFilesRelativePaths.includes(fileRelativePath)
|
||||||
)
|
)
|
||||||
.map(
|
.map(
|
||||||
fileRelativePath =>
|
fileRelativePath =>
|
||||||
@ -92,14 +92,14 @@ export async function writeManagedGitignoreFile(params: {
|
|||||||
export async function readManagedGitignoreFile(params: {
|
export async function readManagedGitignoreFile(params: {
|
||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
ejectedFilesRelativePaths: string[];
|
ownedFilesRelativePaths: string[];
|
||||||
}> {
|
}> {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
|
const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
|
||||||
|
|
||||||
if (!(await existsAsync(filePath))) {
|
if (!(await existsAsync(filePath))) {
|
||||||
return { ejectedFilesRelativePaths: [] };
|
return { ownedFilesRelativePaths: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentStr = (await fsPr.readFile(filePath)).toString("utf8");
|
const contentStr = (await fsPr.readFile(filePath)).toString("utf8");
|
||||||
@ -116,10 +116,10 @@ export async function readManagedGitignoreFile(params: {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
if (payload === undefined) {
|
if (payload === undefined) {
|
||||||
return { ejectedFilesRelativePaths: [] };
|
return { ownedFilesRelativePaths: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ejectedFilesRelativePaths = payload
|
const ownedFilesRelativePaths = payload
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map(line => line.trim())
|
.map(line => line.trim())
|
||||||
.map(line => line.replace(/^# /, ""))
|
.map(line => line.replace(/^# /, ""))
|
||||||
@ -132,5 +132,5 @@ export async function readManagedGitignoreFile(params: {
|
|||||||
)
|
)
|
||||||
.map(filePath => pathRelative(buildContext.themeSrcDirPath, filePath));
|
.map(filePath => pathRelative(buildContext.themeSrcDirPath, filePath));
|
||||||
|
|
||||||
return { ejectedFilesRelativePaths };
|
return { ownedFilesRelativePaths };
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import type { BuildContext } from "../shared/buildContext";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import { getUiModuleMetas, computeHash } from "./uiModuleMeta";
|
import { getExtensionModuleMetas, computeHash } from "./extensionModuleMeta";
|
||||||
import { installUiModulesPeerDependencies } from "./installUiModulesPeerDependencies";
|
import { installExtensionModulesPeerDependencies } from "./installExtensionModulesPeerDependencies";
|
||||||
import {
|
import {
|
||||||
readManagedGitignoreFile,
|
readManagedGitignoreFile,
|
||||||
writeManagedGitignoreFile
|
writeManagedGitignoreFile
|
||||||
@ -9,36 +9,36 @@ import { dirname as pathDirname } from "path";
|
|||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { existsAsync } from "../tools/fs.existsAsync";
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
import * as fsPr from "fs/promises";
|
import * as fsPr from "fs/promises";
|
||||||
import { getIsTrackedByGit } from "../tools/isTrackedByGit";
|
import { getIsKnownByGit } from "../tools/isKnownByGit";
|
||||||
import { untrackFromGit } from "../tools/untrackFromGit";
|
import { untrackFromGit } from "../tools/untrackFromGit";
|
||||||
|
|
||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const uiModuleMetas = await getUiModuleMetas({ buildContext });
|
const extensionModuleMetas = await getExtensionModuleMetas({ buildContext });
|
||||||
|
|
||||||
await installUiModulesPeerDependencies({
|
await installExtensionModulesPeerDependencies({
|
||||||
buildContext,
|
buildContext,
|
||||||
uiModuleMetas
|
extensionModuleMetas
|
||||||
});
|
});
|
||||||
|
|
||||||
const { ejectedFilesRelativePaths } = await readManagedGitignoreFile({
|
const { ownedFilesRelativePaths } = await readManagedGitignoreFile({
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
await writeManagedGitignoreFile({
|
await writeManagedGitignoreFile({
|
||||||
buildContext,
|
buildContext,
|
||||||
ejectedFilesRelativePaths,
|
ownedFilesRelativePaths,
|
||||||
uiModuleMetas
|
extensionModuleMetas
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
uiModuleMetas
|
extensionModuleMetas
|
||||||
.map(uiModuleMeta =>
|
.map(extensionModuleMeta =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
uiModuleMeta.files.map(
|
extensionModuleMeta.files.map(
|
||||||
async ({ fileRelativePath, copyableFilePath, hash }) => {
|
async ({ fileRelativePath, copyableFilePath, hash }) => {
|
||||||
if (ejectedFilesRelativePaths.includes(fileRelativePath)) {
|
if (ownedFilesRelativePaths.includes(fileRelativePath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,19 +65,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
git_untrack: {
|
if (await getIsKnownByGit({ filePath: destFilePath })) {
|
||||||
if (!doesFileExist) {
|
|
||||||
break git_untrack;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isTracked = await getIsTrackedByGit({
|
|
||||||
filePath: destFilePath
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isTracked) {
|
|
||||||
break git_untrack;
|
|
||||||
}
|
|
||||||
|
|
||||||
await untrackFromGit({
|
await untrackFromGit({
|
||||||
filePath: destFilePath
|
filePath: destFilePath
|
||||||
});
|
});
|
@ -1,12 +0,0 @@
|
|||||||
type PropertiesThatCanBeUndefined<T extends Record<string, unknown>> = {
|
|
||||||
[Key in keyof T]: undefined extends T[Key] ? Key : never;
|
|
||||||
}[keyof T];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OptionalIfCanBeUndefined<{ p1: string | undefined; p2: string; }>
|
|
||||||
* is
|
|
||||||
* { p1?: string | undefined; p2: string }
|
|
||||||
*/
|
|
||||||
export type OptionalIfCanBeUndefined<T extends Record<string, unknown>> = {
|
|
||||||
[K in PropertiesThatCanBeUndefined<T>]?: T[K];
|
|
||||||
} & { [K in Exclude<keyof T, PropertiesThatCanBeUndefined<T>>]: T[K] };
|
|
99
src/bin/tools/Stringifyable.ts
Normal file
99
src/bin/tools/Stringifyable.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { same } from "evt/tools/inDepth/same";
|
||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
|
export type Stringifyable =
|
||||||
|
| StringifyableAtomic
|
||||||
|
| StringifyableObject
|
||||||
|
| StringifyableArray;
|
||||||
|
|
||||||
|
export type StringifyableAtomic = string | number | boolean | null;
|
||||||
|
|
||||||
|
// NOTE: Use Record<string, Stringifyable>
|
||||||
|
interface StringifyableObject {
|
||||||
|
[key: string]: Stringifyable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Use Stringifyable[]
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
interface StringifyableArray extends Array<Stringifyable> {}
|
||||||
|
|
||||||
|
export const zStringifyableAtomic = (() => {
|
||||||
|
type TargetType = StringifyableAtomic;
|
||||||
|
|
||||||
|
const zTargetType = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
||||||
|
|
||||||
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
export const zStringifyable: z.ZodType<Stringifyable> = z
|
||||||
|
.any()
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
const isStringifyable = same(JSON.parse(JSON.stringify(val)), val);
|
||||||
|
if (!isStringifyable) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Not stringifyable"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getIsAtomic(
|
||||||
|
stringifyable: Stringifyable
|
||||||
|
): stringifyable is StringifyableAtomic {
|
||||||
|
return (
|
||||||
|
["string", "number", "boolean"].includes(typeof stringifyable) ||
|
||||||
|
stringifyable === null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { getValueAtPath } = (() => {
|
||||||
|
function getValueAtPath_rec(
|
||||||
|
stringifyable: Stringifyable,
|
||||||
|
path: (string | number)[]
|
||||||
|
): Stringifyable | undefined {
|
||||||
|
if (path.length === 0) {
|
||||||
|
return stringifyable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getIsAtomic(stringifyable)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [first, ...rest] = path;
|
||||||
|
|
||||||
|
let dereferenced: Stringifyable | undefined;
|
||||||
|
|
||||||
|
if (stringifyable instanceof Array) {
|
||||||
|
if (typeof first !== "number") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
dereferenced = stringifyable[first];
|
||||||
|
} else {
|
||||||
|
if (typeof first !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
dereferenced = stringifyable[first];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dereferenced === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getValueAtPath_rec(dereferenced, rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValueAtPath(
|
||||||
|
stringifyableObjectOrArray: Record<string, Stringifyable> | Stringifyable[],
|
||||||
|
path: (string | number)[]
|
||||||
|
): Stringifyable | undefined {
|
||||||
|
return getValueAtPath_rec(stringifyableObjectOrArray, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getValueAtPath };
|
||||||
|
})();
|
164
src/bin/tools/canonicalStringify.ts
Normal file
164
src/bin/tools/canonicalStringify.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { getIsAtomic, getValueAtPath, type Stringifyable } from "./Stringifyable";
|
||||||
|
|
||||||
|
export function canonicalStringify(params: {
|
||||||
|
data: Record<string, Stringifyable> | Stringifyable[];
|
||||||
|
referenceData: Record<string, Stringifyable> | Stringifyable[];
|
||||||
|
}): string {
|
||||||
|
const { data, referenceData } = params;
|
||||||
|
|
||||||
|
return JSON.stringify(
|
||||||
|
makeDeterministicCopy({
|
||||||
|
path: [],
|
||||||
|
data,
|
||||||
|
getCanonicalKeys: path => {
|
||||||
|
const referenceValue = (() => {
|
||||||
|
const path_patched: (string | number)[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < path.length; i++) {
|
||||||
|
let value_i = getValueAtPath(referenceData, [
|
||||||
|
...path_patched,
|
||||||
|
path[i]
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (value_i !== undefined) {
|
||||||
|
path_patched.push(path[i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof path[i] !== "number") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
value_i = getValueAtPath(referenceData, [...path_patched, 0]);
|
||||||
|
|
||||||
|
if (value_i !== undefined) {
|
||||||
|
path_patched.push(0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getValueAtPath(referenceData, path_patched);
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (referenceValue === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getIsAtomic(referenceValue)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (referenceValue instanceof Array) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(referenceValue);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDeterministicCopy(params: {
|
||||||
|
path: (string | number)[];
|
||||||
|
data: Stringifyable;
|
||||||
|
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
|
||||||
|
}): Stringifyable {
|
||||||
|
const { path, data, getCanonicalKeys } = params;
|
||||||
|
|
||||||
|
if (getIsAtomic(data)) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data instanceof Array) {
|
||||||
|
return makeDeterministicCopy_array({
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
getCanonicalKeys
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeDeterministicCopy_record({
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
getCanonicalKeys
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDeterministicCopy_record(params: {
|
||||||
|
path: (string | number)[];
|
||||||
|
data: Record<string, Stringifyable>;
|
||||||
|
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
|
||||||
|
}): Record<string, Stringifyable> {
|
||||||
|
const { path, data, getCanonicalKeys } = params;
|
||||||
|
|
||||||
|
const keysOfAtomicValues: string[] = [];
|
||||||
|
const keysOfNonAtomicValues: string[] = [];
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
if (getIsAtomic(value)) {
|
||||||
|
keysOfAtomicValues.push(key);
|
||||||
|
} else {
|
||||||
|
keysOfNonAtomicValues.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keysOfAtomicValues.sort();
|
||||||
|
keysOfNonAtomicValues.sort();
|
||||||
|
|
||||||
|
const keys = [...keysOfAtomicValues, ...keysOfNonAtomicValues];
|
||||||
|
|
||||||
|
reorder_according_to_canonical: {
|
||||||
|
const canonicalKeys = getCanonicalKeys(path);
|
||||||
|
|
||||||
|
if (canonicalKeys === undefined) {
|
||||||
|
break reorder_according_to_canonical;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys_toPrepend: string[] = [];
|
||||||
|
|
||||||
|
for (const key of canonicalKeys) {
|
||||||
|
const indexOfKey = keys.indexOf(key);
|
||||||
|
|
||||||
|
if (indexOfKey === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.splice(indexOfKey, 1);
|
||||||
|
keys_toPrepend.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.unshift(...keys_toPrepend);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Record<string, Stringifyable> = {};
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
result[key] = makeDeterministicCopy({
|
||||||
|
path: [...path, key],
|
||||||
|
data: data[key],
|
||||||
|
getCanonicalKeys
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDeterministicCopy_array(params: {
|
||||||
|
path: (string | number)[];
|
||||||
|
data: Stringifyable[];
|
||||||
|
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
|
||||||
|
}): Stringifyable[] {
|
||||||
|
const { path, data, getCanonicalKeys } = params;
|
||||||
|
|
||||||
|
return [...data].map((entry, i) =>
|
||||||
|
makeDeterministicCopy({
|
||||||
|
path: [...path, i],
|
||||||
|
data: entry,
|
||||||
|
getCanonicalKeys
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
@ -1,73 +0,0 @@
|
|||||||
import { Readable } from "stream";
|
|
||||||
|
|
||||||
const crc32tab = [
|
|
||||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535,
|
|
||||||
0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd,
|
|
||||||
0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d,
|
|
||||||
0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
|
|
||||||
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4,
|
|
||||||
0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
|
|
||||||
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac,
|
|
||||||
0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
|
|
||||||
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab,
|
|
||||||
0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f,
|
|
||||||
0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb,
|
|
||||||
0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
|
|
||||||
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea,
|
|
||||||
0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce,
|
|
||||||
0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a,
|
|
||||||
0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
|
|
||||||
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409,
|
|
||||||
0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
|
|
||||||
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739,
|
|
||||||
0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
|
|
||||||
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268,
|
|
||||||
0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0,
|
|
||||||
0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8,
|
|
||||||
0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
|
||||||
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef,
|
|
||||||
0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703,
|
|
||||||
0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7,
|
|
||||||
0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
|
|
||||||
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae,
|
|
||||||
0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
|
|
||||||
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6,
|
|
||||||
0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
|
|
||||||
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d,
|
|
||||||
0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5,
|
|
||||||
0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
|
|
||||||
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
|
||||||
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param input either a byte stream, a string or a buffer, you want to have the checksum for
|
|
||||||
* @returns a promise for a checksum (uint32)
|
|
||||||
*/
|
|
||||||
export function crc32(input: Readable | String | Buffer): Promise<number> {
|
|
||||||
if (typeof input === "string") {
|
|
||||||
let crc = ~0;
|
|
||||||
for (let i = 0; i < input.length; i++)
|
|
||||||
crc = (crc >>> 8) ^ crc32tab[(crc ^ input.charCodeAt(i)) & 0xff];
|
|
||||||
return Promise.resolve((crc ^ -1) >>> 0);
|
|
||||||
} else if (input instanceof Buffer) {
|
|
||||||
let crc = ~0;
|
|
||||||
for (let i = 0; i < input.length; i++)
|
|
||||||
crc = (crc >>> 8) ^ crc32tab[(crc ^ input[i]) & 0xff];
|
|
||||||
return Promise.resolve((crc ^ -1) >>> 0);
|
|
||||||
} else if (input instanceof Readable) {
|
|
||||||
return new Promise<number>((resolve, reject) => {
|
|
||||||
let crc = ~0;
|
|
||||||
input.setMaxListeners(Infinity);
|
|
||||||
input.on("end", () => resolve((crc ^ -1) >>> 0));
|
|
||||||
input.on("error", e => reject(e));
|
|
||||||
input.on("data", (chunk: Buffer) => {
|
|
||||||
for (let i = 0; i < chunk.length; i++)
|
|
||||||
crc = (crc >>> 8) ^ crc32tab[(crc ^ chunk[i]) & 0xff];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error("Unsupported input " + typeof input);
|
|
||||||
}
|
|
||||||
}
|
|
90
src/bin/tools/createObjectThatThrowsIfAccessed.ts
Normal file
90
src/bin/tools/createObjectThatThrowsIfAccessed.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
const keyIsTrapped = "isTrapped_zSskDe9d";
|
||||||
|
|
||||||
|
export class AccessError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createObjectThatThrowsIfAccessed<T extends object>(params?: {
|
||||||
|
debugMessage?: string;
|
||||||
|
isPropertyWhitelisted?: (prop: string | number | symbol) => boolean;
|
||||||
|
}): T {
|
||||||
|
const { debugMessage = "", isPropertyWhitelisted = () => false } = params ?? {};
|
||||||
|
|
||||||
|
const get: NonNullable<ProxyHandler<T>["get"]> = (...args) => {
|
||||||
|
const [, prop] = args;
|
||||||
|
|
||||||
|
if (isPropertyWhitelisted(prop)) {
|
||||||
|
return Reflect.get(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === keyIsTrapped) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AccessError(`Cannot access ${String(prop)} yet ${debugMessage}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const trappedObject = new Proxy<T>({} as any, {
|
||||||
|
get,
|
||||||
|
set: get
|
||||||
|
});
|
||||||
|
|
||||||
|
return trappedObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createObjectThatThrowsIfAccessedFactory(params: {
|
||||||
|
isPropertyWhitelisted?: (prop: string | number | symbol) => boolean;
|
||||||
|
}) {
|
||||||
|
const { isPropertyWhitelisted } = params;
|
||||||
|
|
||||||
|
return {
|
||||||
|
createObjectThatThrowsIfAccessed: <T extends object>(params?: {
|
||||||
|
debugMessage?: string;
|
||||||
|
}) => {
|
||||||
|
const { debugMessage } = params ?? {};
|
||||||
|
|
||||||
|
return createObjectThatThrowsIfAccessed<T>({
|
||||||
|
debugMessage,
|
||||||
|
isPropertyWhitelisted
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObjectThatThrowIfAccessed(obj: object) {
|
||||||
|
return (obj as any)[keyIsTrapped] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const THROW_IF_ACCESSED = {
|
||||||
|
__brand: "THROW_IF_ACCESSED"
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createObjectWithSomePropertiesThatThrowIfAccessed<
|
||||||
|
T extends Record<string, unknown>
|
||||||
|
>(obj: { [K in keyof T]: T[K] | typeof THROW_IF_ACCESSED }, debugMessage?: string): T {
|
||||||
|
return Object.defineProperties(
|
||||||
|
obj,
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(obj)
|
||||||
|
.filter(([, value]) => value === THROW_IF_ACCESSED)
|
||||||
|
.map(([key]) => {
|
||||||
|
const getAndSet = () => {
|
||||||
|
throw new AccessError(
|
||||||
|
`Cannot access ${key} yet ${debugMessage ?? ""}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pd = {
|
||||||
|
get: getAndSet,
|
||||||
|
set: getAndSet,
|
||||||
|
enumerable: true
|
||||||
|
};
|
||||||
|
|
||||||
|
return [key, pd];
|
||||||
|
})
|
||||||
|
)
|
||||||
|
) as any;
|
||||||
|
}
|
@ -1,61 +0,0 @@
|
|||||||
import { PassThrough, Readable, TransformCallback, Writable } from "stream";
|
|
||||||
import { pipeline } from "stream/promises";
|
|
||||||
import { deflateRaw as deflateRawCb, createDeflateRaw } from "zlib";
|
|
||||||
import { promisify } from "util";
|
|
||||||
|
|
||||||
import { crc32 } from "./crc32";
|
|
||||||
import tee from "./tee";
|
|
||||||
|
|
||||||
const deflateRaw = promisify(deflateRawCb);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A stream transformer that records the number of bytes
|
|
||||||
* passed in its `size` property.
|
|
||||||
*/
|
|
||||||
class ByteCounter extends PassThrough {
|
|
||||||
size: number = 0;
|
|
||||||
_transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) {
|
|
||||||
if ("length" in chunk) this.size += chunk.length;
|
|
||||||
super._transform(chunk, encoding, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param data buffer containing the data to be compressed
|
|
||||||
* @returns a buffer containing the compressed/deflated data and the crc32 checksum
|
|
||||||
* of the source data
|
|
||||||
*/
|
|
||||||
export async function deflateBuffer(data: Buffer) {
|
|
||||||
const [deflated, checksum] = await Promise.all([deflateRaw(data), crc32(data)]);
|
|
||||||
return { deflated, crc32: checksum };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param input a byte stream, containing data to be compressed
|
|
||||||
* @param sink a method that will accept chunks of compressed data; We don't pass
|
|
||||||
* a writable here, since we don't want the writablestream to be closed after
|
|
||||||
* a single file
|
|
||||||
* @returns a promise, which will resolve with the crc32 checksum and the
|
|
||||||
* compressed size
|
|
||||||
*/
|
|
||||||
export async function deflateStream(input: Readable, sink: (chunk: Buffer) => void) {
|
|
||||||
const deflateWriter = new Writable({
|
|
||||||
write(chunk, _, callback) {
|
|
||||||
sink(chunk);
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// tee the input stream, so we can compress and calc crc32 in parallel
|
|
||||||
const [rs1, rs2] = tee(input);
|
|
||||||
const byteCounter = new ByteCounter();
|
|
||||||
const [_, crc] = await Promise.all([
|
|
||||||
// pipe input into zip compressor, count the bytes
|
|
||||||
// returned and pass compressed data to the sink
|
|
||||||
pipeline(rs1, createDeflateRaw(), byteCounter, deflateWriter),
|
|
||||||
// calc checksum
|
|
||||||
crc32(rs2)
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { crc32: crc, compressedSize: byteCounter.size };
|
|
||||||
}
|
|
@ -14,6 +14,8 @@ export function getAbsoluteAndInOsFormatPath(params: {
|
|||||||
|
|
||||||
let pathOut = pathIsh;
|
let pathOut = pathIsh;
|
||||||
|
|
||||||
|
pathOut = pathOut.replace(/^['"]/, "").replace(/['"]$/, "");
|
||||||
|
|
||||||
pathOut = pathOut.replace(/\//g, pathSep);
|
pathOut = pathOut.replace(/\//g, pathSep);
|
||||||
|
|
||||||
if (pathOut.startsWith("~")) {
|
if (pathOut.startsWith("~")) {
|
||||||
|
@ -2,40 +2,42 @@ import { join as pathJoin } from "path";
|
|||||||
import { existsAsync } from "./fs.existsAsync";
|
import { existsAsync } from "./fs.existsAsync";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
|
import { getIsRootPath } from "../tools/isRootPath";
|
||||||
|
|
||||||
export async function getInstalledModuleDirPath(params: {
|
export async function getInstalledModuleDirPath(params: {
|
||||||
moduleName: string;
|
moduleName: string;
|
||||||
packageJsonDirPath: string;
|
packageJsonDirPath: string;
|
||||||
projectDirPath: string;
|
|
||||||
}) {
|
}) {
|
||||||
const { moduleName, packageJsonDirPath, projectDirPath } = params;
|
const { moduleName, packageJsonDirPath } = params;
|
||||||
|
|
||||||
common_case: {
|
{
|
||||||
const dirPath = pathJoin(
|
let dirPath = packageJsonDirPath;
|
||||||
...[packageJsonDirPath, "node_modules", ...moduleName.split("/")]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!(await existsAsync(dirPath))) {
|
while (true) {
|
||||||
break common_case;
|
const dirPath_candidate = pathJoin(
|
||||||
|
dirPath,
|
||||||
|
"node_modules",
|
||||||
|
...moduleName.split("/")
|
||||||
|
);
|
||||||
|
|
||||||
|
let doesExist: boolean;
|
||||||
|
|
||||||
|
try {
|
||||||
|
doesExist = await existsAsync(dirPath_candidate);
|
||||||
|
} catch {
|
||||||
|
doesExist = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doesExist) {
|
||||||
|
return dirPath_candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getIsRootPath(dirPath)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
dirPath = pathJoin(dirPath, "..");
|
||||||
}
|
}
|
||||||
|
|
||||||
return dirPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
node_modules_at_root_case: {
|
|
||||||
if (projectDirPath === packageJsonDirPath) {
|
|
||||||
break node_modules_at_root_case;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dirPath = pathJoin(
|
|
||||||
...[projectDirPath, "node_modules", ...moduleName.split("/")]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!(await existsAsync(dirPath))) {
|
|
||||||
break node_modules_at_root_case;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dirPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dirPath = child_process
|
const dirPath = child_process
|
||||||
|
45
src/bin/tools/isKnownByGit.ts
Normal file
45
src/bin/tools/isKnownByGit.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import * as child_process from "child_process";
|
||||||
|
import {
|
||||||
|
dirname as pathDirname,
|
||||||
|
basename as pathBasename,
|
||||||
|
join as pathJoin,
|
||||||
|
sep as pathSep
|
||||||
|
} from "path";
|
||||||
|
import { Deferred } from "evt/tools/Deferred";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
export function getIsKnownByGit(params: { filePath: string }): Promise<boolean> {
|
||||||
|
const { filePath } = params;
|
||||||
|
|
||||||
|
const dIsKnownByGit = new Deferred<boolean>();
|
||||||
|
|
||||||
|
let relativePath = pathBasename(filePath);
|
||||||
|
|
||||||
|
let dirPath = pathDirname(filePath);
|
||||||
|
|
||||||
|
while (!fs.existsSync(dirPath)) {
|
||||||
|
relativePath = pathJoin(pathBasename(dirPath), relativePath);
|
||||||
|
|
||||||
|
dirPath = pathDirname(dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
child_process.exec(
|
||||||
|
`git ls-files --error-unmatch '${relativePath.split(pathSep).join("/")}'`,
|
||||||
|
{ cwd: dirPath },
|
||||||
|
error => {
|
||||||
|
if (error === null) {
|
||||||
|
dIsKnownByGit.resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 1) {
|
||||||
|
dIsKnownByGit.resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dIsKnownByGit.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return dIsKnownByGit.pr;
|
||||||
|
}
|
22
src/bin/tools/isRootPath.ts
Normal file
22
src/bin/tools/isRootPath.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { normalize as pathNormalize } from "path";
|
||||||
|
|
||||||
|
export function getIsRootPath(filePath: string): boolean {
|
||||||
|
const path_normalized = pathNormalize(filePath);
|
||||||
|
|
||||||
|
// Unix-like root ("/")
|
||||||
|
if (path_normalized === "/") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Windows drive root (e.g., "C:\\")
|
||||||
|
if (/^[a-zA-Z]:\\$/.test(path_normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for UNC root (e.g., "\\server\share")
|
||||||
|
if (/^\\\\[^\\]+\\[^\\]+\\?$/.test(path_normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
@ -1,29 +0,0 @@
|
|||||||
import * as child_process from "child_process";
|
|
||||||
import { dirname as pathDirname, basename as pathBasename } from "path";
|
|
||||||
import { Deferred } from "evt/tools/Deferred";
|
|
||||||
|
|
||||||
export function getIsTrackedByGit(params: { filePath: string }): Promise<boolean> {
|
|
||||||
const { filePath } = params;
|
|
||||||
|
|
||||||
const dIsTracked = new Deferred<boolean>();
|
|
||||||
|
|
||||||
child_process.exec(
|
|
||||||
`git ls-files --error-unmatch ${pathBasename(filePath)}`,
|
|
||||||
{ cwd: pathDirname(filePath) },
|
|
||||||
error => {
|
|
||||||
if (error === null) {
|
|
||||||
dIsTracked.resolve(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.code === 1) {
|
|
||||||
dIsTracked.resolve(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dIsTracked.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return dIsTracked.pr;
|
|
||||||
}
|
|
@ -8,7 +8,6 @@ import { exclude } from "tsafe/exclude";
|
|||||||
|
|
||||||
export async function listInstalledModules(params: {
|
export async function listInstalledModules(params: {
|
||||||
packageJsonFilePath: string;
|
packageJsonFilePath: string;
|
||||||
projectDirPath: string;
|
|
||||||
filter: (params: { moduleName: string }) => boolean;
|
filter: (params: { moduleName: string }) => boolean;
|
||||||
}): Promise<
|
}): Promise<
|
||||||
{
|
{
|
||||||
@ -18,13 +17,13 @@ export async function listInstalledModules(params: {
|
|||||||
peerDependencies: Record<string, string>;
|
peerDependencies: Record<string, string>;
|
||||||
}[]
|
}[]
|
||||||
> {
|
> {
|
||||||
const { packageJsonFilePath, projectDirPath, filter } = params;
|
const { packageJsonFilePath, filter } = params;
|
||||||
|
|
||||||
const parsedPackageJson = await readPackageJsonDependencies({
|
const parsedPackageJson = await readPackageJsonDependencies({
|
||||||
packageJsonFilePath
|
packageJsonFilePath
|
||||||
});
|
});
|
||||||
|
|
||||||
const uiModuleNames = (
|
const extensionModuleNames = (
|
||||||
[parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const
|
[parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const
|
||||||
)
|
)
|
||||||
.filter(exclude(undefined))
|
.filter(exclude(undefined))
|
||||||
@ -33,11 +32,10 @@ export async function listInstalledModules(params: {
|
|||||||
.filter(moduleName => filter({ moduleName }));
|
.filter(moduleName => filter({ moduleName }));
|
||||||
|
|
||||||
const result = await Promise.all(
|
const result = await Promise.all(
|
||||||
uiModuleNames.map(async moduleName => {
|
extensionModuleNames.map(async moduleName => {
|
||||||
const dirPath = await getInstalledModuleDirPath({
|
const dirPath = await getInstalledModuleDirPath({
|
||||||
moduleName,
|
moduleName,
|
||||||
packageJsonDirPath: pathDirname(packageJsonFilePath),
|
packageJsonDirPath: pathDirname(packageJsonFilePath)
|
||||||
projectDirPath
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { version, peerDependencies } =
|
const { version, peerDependencies } =
|
||||||
|
@ -1,10 +1,29 @@
|
|||||||
import { sep as pathSep } from "path";
|
import { sep as pathSep, dirname as pathDirname, join as pathJoin } from "path";
|
||||||
|
import { getThisCodebaseRootDirPath } from "./getThisCodebaseRootDirPath";
|
||||||
|
import { getInstalledModuleDirPath } from "./getInstalledModuleDirPath";
|
||||||
|
import { existsAsync } from "./fs.existsAsync";
|
||||||
|
import { z } from "zod";
|
||||||
|
import * as fs from "fs/promises";
|
||||||
|
import { assert, is, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
let cache: string | undefined = undefined;
|
let cache_bestEffort: string | undefined = undefined;
|
||||||
|
|
||||||
export function getNodeModulesBinDirPath() {
|
/** NOTE: Careful, this function can fail when the binary
|
||||||
if (cache !== undefined) {
|
* Used is not in the node_modules directory of the project
|
||||||
return cache;
|
* (for example when running tests with vscode extension we'll get
|
||||||
|
* '/Users/dylan/.vscode/extensions/vitest.explorer-1.16.0/dist/worker.js'
|
||||||
|
*
|
||||||
|
* instead of
|
||||||
|
* '/Users/joseph/.nvm/versions/node/v22.12.0/bin/node'
|
||||||
|
* or
|
||||||
|
* '/Users/joseph/github/keycloakify-starter/node_modules/.bin/vite'
|
||||||
|
*
|
||||||
|
* as the value of process.argv[1]
|
||||||
|
*/
|
||||||
|
function getNodeModulesBinDirPath_bestEffort() {
|
||||||
|
if (cache_bestEffort !== undefined) {
|
||||||
|
return cache_bestEffort;
|
||||||
}
|
}
|
||||||
|
|
||||||
const binPath = process.argv[1];
|
const binPath = process.argv[1];
|
||||||
@ -30,9 +49,122 @@ export function getNodeModulesBinDirPath() {
|
|||||||
segments.unshift(segment);
|
segments.unshift(segment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!foundNodeModules) {
|
||||||
|
throw new Error(`Could not find node_modules in path ${binPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
const nodeModulesBinDirPath = segments.join(pathSep);
|
const nodeModulesBinDirPath = segments.join(pathSep);
|
||||||
|
|
||||||
cache = nodeModulesBinDirPath;
|
cache_bestEffort = nodeModulesBinDirPath;
|
||||||
|
|
||||||
return nodeModulesBinDirPath;
|
return nodeModulesBinDirPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cache_withPackageJsonFileDirPath:
|
||||||
|
| { packageJsonFilePath: string; nodeModulesBinDirPath: string }
|
||||||
|
| undefined = undefined;
|
||||||
|
|
||||||
|
async function getNodeModulesBinDirPath_withPackageJsonFileDirPath(params: {
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
}): Promise<string> {
|
||||||
|
const { packageJsonFilePath } = params;
|
||||||
|
|
||||||
|
use_cache: {
|
||||||
|
if (cache_withPackageJsonFileDirPath === undefined) {
|
||||||
|
break use_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
cache_withPackageJsonFileDirPath.packageJsonFilePath !== packageJsonFilePath
|
||||||
|
) {
|
||||||
|
cache_withPackageJsonFileDirPath = undefined;
|
||||||
|
break use_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache_withPackageJsonFileDirPath.nodeModulesBinDirPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [...]node_modules/keycloakify
|
||||||
|
const installedModuleDirPath = await getInstalledModuleDirPath({
|
||||||
|
// Here it will always be "keycloakify" but since we are in tools/ we make something generic
|
||||||
|
moduleName: await (async () => {
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = (() => {
|
||||||
|
type TargetType = ParsedPackageJson;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
name: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const parsedPackageJson = JSON.parse(
|
||||||
|
(
|
||||||
|
await fs.readFile(
|
||||||
|
pathJoin(getThisCodebaseRootDirPath(), "package.json")
|
||||||
|
)
|
||||||
|
).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
|
|
||||||
|
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||||
|
|
||||||
|
return parsedPackageJson.name;
|
||||||
|
})(),
|
||||||
|
packageJsonDirPath: pathDirname(packageJsonFilePath)
|
||||||
|
});
|
||||||
|
|
||||||
|
const segments = installedModuleDirPath.split(pathSep);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const segment = segments.pop();
|
||||||
|
|
||||||
|
if (segment === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find .bin directory relative to ${packageJsonFilePath}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment !== "node_modules") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate = pathJoin(segments.join(pathSep), segment, ".bin");
|
||||||
|
|
||||||
|
if (!(await existsAsync(candidate))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache_withPackageJsonFileDirPath = {
|
||||||
|
packageJsonFilePath,
|
||||||
|
nodeModulesBinDirPath: candidate
|
||||||
|
};
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache_withPackageJsonFileDirPath.nodeModulesBinDirPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNodeModulesBinDirPath(params: {
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
}): Promise<string>;
|
||||||
|
export function getNodeModulesBinDirPath(params: {
|
||||||
|
packageJsonFilePath: undefined;
|
||||||
|
}): string;
|
||||||
|
export function getNodeModulesBinDirPath(params: {
|
||||||
|
packageJsonFilePath: string | undefined;
|
||||||
|
}): string | Promise<string> {
|
||||||
|
const { packageJsonFilePath } = params ?? {};
|
||||||
|
|
||||||
|
return packageJsonFilePath === undefined
|
||||||
|
? getNodeModulesBinDirPath_bestEffort()
|
||||||
|
: getNodeModulesBinDirPath_withPackageJsonFileDirPath({ packageJsonFilePath });
|
||||||
|
}
|
||||||
|
@ -9,8 +9,9 @@ import { objectKeys } from "tsafe/objectKeys";
|
|||||||
import { getAbsoluteAndInOsFormatPath } from "./getAbsoluteAndInOsFormatPath";
|
import { getAbsoluteAndInOsFormatPath } from "./getAbsoluteAndInOsFormatPath";
|
||||||
import { exclude } from "tsafe/exclude";
|
import { exclude } from "tsafe/exclude";
|
||||||
import { rmSync } from "./fs.rmSync";
|
import { rmSync } from "./fs.rmSync";
|
||||||
|
import { Deferred } from "evt/tools/Deferred";
|
||||||
|
|
||||||
export function npmInstall(params: { packageJsonDirPath: string }) {
|
export async function npmInstall(params: { packageJsonDirPath: string }) {
|
||||||
const { packageJsonDirPath } = params;
|
const { packageJsonDirPath } = params;
|
||||||
|
|
||||||
const packageManagerBinName = (() => {
|
const packageManagerBinName = (() => {
|
||||||
@ -68,7 +69,7 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
|
|||||||
|
|
||||||
console.log(chalk.green("Installing in a way that won't break the links..."));
|
console.log(chalk.green("Installing in a way that won't break the links..."));
|
||||||
|
|
||||||
installWithoutBreakingLinks({
|
await installWithoutBreakingLinks({
|
||||||
packageJsonDirPath,
|
packageJsonDirPath,
|
||||||
garronejLinkInfos
|
garronejLinkInfos
|
||||||
});
|
});
|
||||||
@ -77,9 +78,9 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
child_process.execSync(`${packageManagerBinName} install`, {
|
await runPackageManagerInstall({
|
||||||
cwd: packageJsonDirPath,
|
packageManagerBinName,
|
||||||
stdio: "inherit"
|
cwd: packageJsonDirPath
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
console.log(
|
console.log(
|
||||||
@ -90,6 +91,42 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runPackageManagerInstall(params: {
|
||||||
|
packageManagerBinName: string;
|
||||||
|
cwd: string;
|
||||||
|
}) {
|
||||||
|
const { packageManagerBinName, cwd } = params;
|
||||||
|
|
||||||
|
const dCompleted = new Deferred<void>();
|
||||||
|
|
||||||
|
const child = child_process.spawn(packageManagerBinName, ["install"], {
|
||||||
|
cwd,
|
||||||
|
env: process.env,
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdout.on("data", data => process.stdout.write(data));
|
||||||
|
|
||||||
|
child.stderr.on("data", data => {
|
||||||
|
if (data.toString("utf8").includes("peer dependency")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stderr.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("exit", code => {
|
||||||
|
if (code !== 0) {
|
||||||
|
dCompleted.reject(new Error(`Failed with code ${code}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dCompleted.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
await dCompleted.pr;
|
||||||
|
}
|
||||||
|
|
||||||
function getGarronejLinkInfos(params: {
|
function getGarronejLinkInfos(params: {
|
||||||
packageJsonDirPath: string;
|
packageJsonDirPath: string;
|
||||||
}): { linkedModuleNames: string[]; yarnHomeDirPath: string } | undefined {
|
}): { linkedModuleNames: string[]; yarnHomeDirPath: string } | undefined {
|
||||||
@ -180,7 +217,7 @@ function getGarronejLinkInfos(params: {
|
|||||||
return { linkedModuleNames, yarnHomeDirPath };
|
return { linkedModuleNames, yarnHomeDirPath };
|
||||||
}
|
}
|
||||||
|
|
||||||
function installWithoutBreakingLinks(params: {
|
async function installWithoutBreakingLinks(params: {
|
||||||
packageJsonDirPath: string;
|
packageJsonDirPath: string;
|
||||||
garronejLinkInfos: Exclude<ReturnType<typeof getGarronejLinkInfos>, undefined>;
|
garronejLinkInfos: Exclude<ReturnType<typeof getGarronejLinkInfos>, undefined>;
|
||||||
}) {
|
}) {
|
||||||
@ -261,9 +298,9 @@ function installWithoutBreakingLinks(params: {
|
|||||||
pathJoin(tmpProjectDirPath, YARN_LOCK)
|
pathJoin(tmpProjectDirPath, YARN_LOCK)
|
||||||
);
|
);
|
||||||
|
|
||||||
child_process.execSync(`yarn install`, {
|
await runPackageManagerInstall({
|
||||||
cwd: tmpProjectDirPath,
|
packageManagerBinName: "yarn",
|
||||||
stdio: "inherit"
|
cwd: tmpProjectDirPath
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: Moving the modules from the tmp project to the actual project
|
// NOTE: Moving the modules from the tmp project to the actual project
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
import { listTagsFactory } from "./listTags";
|
|
||||||
import type { Octokit } from "@octokit/rest";
|
|
||||||
import { SemVer } from "../SemVer";
|
|
||||||
|
|
||||||
export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) {
|
|
||||||
const { octokit } = params;
|
|
||||||
|
|
||||||
async function getLatestsSemVersionedTag(params: {
|
|
||||||
owner: string;
|
|
||||||
repo: string;
|
|
||||||
count: number;
|
|
||||||
doIgnoreReleaseCandidates: boolean;
|
|
||||||
}): Promise<
|
|
||||||
{
|
|
||||||
tag: string;
|
|
||||||
version: SemVer;
|
|
||||||
}[]
|
|
||||||
> {
|
|
||||||
const { owner, repo, count, doIgnoreReleaseCandidates } = params;
|
|
||||||
|
|
||||||
const semVersionedTags: { tag: string; version: SemVer }[] = [];
|
|
||||||
|
|
||||||
const { listTags } = listTagsFactory({ octokit });
|
|
||||||
|
|
||||||
for await (const tag of listTags({ owner, repo })) {
|
|
||||||
let version: SemVer;
|
|
||||||
|
|
||||||
try {
|
|
||||||
version = SemVer.parse(tag.replace(/^[vV]?/, ""));
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doIgnoreReleaseCandidates && version.rc !== undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
semVersionedTags.push({ tag, version });
|
|
||||||
}
|
|
||||||
|
|
||||||
return semVersionedTags
|
|
||||||
.sort(({ version: vX }, { version: vY }) => SemVer.compare(vY, vX))
|
|
||||||
.slice(0, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { getLatestsSemVersionedTag };
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
import type { Octokit } from "@octokit/rest";
|
|
||||||
|
|
||||||
const per_page = 99;
|
|
||||||
|
|
||||||
export function listTagsFactory(params: { octokit: Octokit }) {
|
|
||||||
const { octokit } = params;
|
|
||||||
|
|
||||||
const octokit_repo_listTags = async (params: {
|
|
||||||
owner: string;
|
|
||||||
repo: string;
|
|
||||||
per_page: number;
|
|
||||||
page: number;
|
|
||||||
}) => {
|
|
||||||
return octokit.repos.listTags(params);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function* listTags(params: {
|
|
||||||
owner: string;
|
|
||||||
repo: string;
|
|
||||||
}): AsyncGenerator<string> {
|
|
||||||
const { owner, repo } = params;
|
|
||||||
|
|
||||||
let page = 1;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const resp = await octokit_repo_listTags({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
per_page,
|
|
||||||
page: page++
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const branch of resp.data.map(({ name }) => name)) {
|
|
||||||
yield branch;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resp.data.length < 99) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the same "latest" tag as deno.land/x, not actually the latest though */
|
|
||||||
async function getLatestTag(params: {
|
|
||||||
owner: string;
|
|
||||||
repo: string;
|
|
||||||
}): Promise<string | undefined> {
|
|
||||||
const { owner, repo } = params;
|
|
||||||
|
|
||||||
const itRes = await listTags({ owner, repo }).next();
|
|
||||||
|
|
||||||
if (itRes.done) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return itRes.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { listTags, getLatestTag };
|
|
||||||
}
|
|
@ -15,7 +15,9 @@ export async function getIsPrettierAvailable(): Promise<boolean> {
|
|||||||
return getIsPrettierAvailable.cache;
|
return getIsPrettierAvailable.cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
|
const nodeModulesBinDirPath = getNodeModulesBinDirPath({
|
||||||
|
packageJsonFilePath: undefined
|
||||||
|
});
|
||||||
|
|
||||||
const prettierBinPath = pathJoin(nodeModulesBinDirPath, "prettier");
|
const prettierBinPath = pathJoin(nodeModulesBinDirPath, "prettier");
|
||||||
|
|
||||||
@ -50,10 +52,26 @@ export async function getPrettier(): Promise<PrettierAndConfigHash> {
|
|||||||
// So we do a sketchy eval to bypass ncc.
|
// So we do a sketchy eval to bypass ncc.
|
||||||
// We make sure to only do that when linking, otherwise we import properly.
|
// We make sure to only do that when linking, otherwise we import properly.
|
||||||
if (readThisNpmPackageVersion().startsWith("0.0.0")) {
|
if (readThisNpmPackageVersion().startsWith("0.0.0")) {
|
||||||
eval(
|
const prettierDirPath = pathResolve(
|
||||||
`${symToStr({ prettier })} = require("${pathResolve(pathJoin(getNodeModulesBinDirPath(), "..", "prettier"))}")`
|
pathJoin(
|
||||||
|
getNodeModulesBinDirPath({ packageJsonFilePath: undefined }),
|
||||||
|
"..",
|
||||||
|
"prettier"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isCJS = typeof module !== "undefined" && module.exports;
|
||||||
|
|
||||||
|
if (isCJS) {
|
||||||
|
eval(`${symToStr({ prettier })} = require("${prettierDirPath}")`);
|
||||||
|
} else {
|
||||||
|
prettier = await new Promise(_resolve => {
|
||||||
|
eval(
|
||||||
|
`import("file:///${pathJoin(prettierDirPath, "index.mjs").replace(/\\/g, "/")}").then(prettier => _resolve(prettier))`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
assert(!is<undefined>(prettier));
|
assert(!is<undefined>(prettier));
|
||||||
|
|
||||||
break import_prettier;
|
break import_prettier;
|
||||||
@ -64,7 +82,7 @@ export async function getPrettier(): Promise<PrettierAndConfigHash> {
|
|||||||
|
|
||||||
const configHash = await (async () => {
|
const configHash = await (async () => {
|
||||||
const configFilePath = await prettier.resolveConfigFile(
|
const configFilePath = await prettier.resolveConfigFile(
|
||||||
pathJoin(getNodeModulesBinDirPath(), "..")
|
pathJoin(getNodeModulesBinDirPath({ packageJsonFilePath: undefined }), "..")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (configFilePath === null) {
|
if (configFilePath === null) {
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
import { PassThrough, Readable } from "stream";
|
|
||||||
|
|
||||||
export default function tee(input: Readable) {
|
|
||||||
const a = new PassThrough();
|
|
||||||
const b = new PassThrough();
|
|
||||||
|
|
||||||
let aFull = false;
|
|
||||||
let bFull = false;
|
|
||||||
|
|
||||||
a.setMaxListeners(Infinity);
|
|
||||||
|
|
||||||
a.on("drain", () => {
|
|
||||||
aFull = false;
|
|
||||||
if (!aFull && !bFull) input.resume();
|
|
||||||
});
|
|
||||||
b.on("drain", () => {
|
|
||||||
bFull = false;
|
|
||||||
if (!aFull && !bFull) input.resume();
|
|
||||||
});
|
|
||||||
|
|
||||||
input.on("error", e => {
|
|
||||||
a.emit("error", e);
|
|
||||||
b.emit("error", e);
|
|
||||||
});
|
|
||||||
|
|
||||||
input.on("data", chunk => {
|
|
||||||
aFull = !a.write(chunk);
|
|
||||||
bFull = !b.write(chunk);
|
|
||||||
|
|
||||||
if (aFull || bFull) input.pause();
|
|
||||||
});
|
|
||||||
|
|
||||||
input.on("end", () => {
|
|
||||||
a.end();
|
|
||||||
b.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
return [a, b] as const;
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Concatenate the string fragments and interpolated values
|
|
||||||
* to get a single string.
|
|
||||||
*/
|
|
||||||
function populateTemplate(strings: TemplateStringsArray, ...args: unknown[]) {
|
|
||||||
const chunks: string[] = [];
|
|
||||||
for (let i = 0; i < strings.length; i++) {
|
|
||||||
let lastStringLineLength = 0;
|
|
||||||
if (strings[i]) {
|
|
||||||
chunks.push(strings[i]);
|
|
||||||
// remember last indent of the string portion
|
|
||||||
lastStringLineLength = strings[i].split("\n").slice(-1)[0]?.length ?? 0;
|
|
||||||
}
|
|
||||||
if (args[i]) {
|
|
||||||
// if the interpolation value has newlines, indent the interpolation values
|
|
||||||
// using the last known string indent
|
|
||||||
const chunk = String(args[i]).replace(
|
|
||||||
/([\r?\n])/g,
|
|
||||||
"$1" + " ".repeat(lastStringLineLength)
|
|
||||||
);
|
|
||||||
chunks.push(chunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return chunks.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shift all lines left by the *smallest* indentation level,
|
|
||||||
* and remove initial newline and all trailing spaces.
|
|
||||||
*/
|
|
||||||
export default function trimIndent(strings: TemplateStringsArray, ...args: any[]) {
|
|
||||||
// Remove initial and final newlines
|
|
||||||
let string = populateTemplate(strings, ...args)
|
|
||||||
.replace(/^[\r\n]/, "")
|
|
||||||
.replace(/\r?\n *$/, "");
|
|
||||||
const dents =
|
|
||||||
string
|
|
||||||
.match(/^([ \t])+/gm)
|
|
||||||
?.filter(s => /^\s+$/.test(s))
|
|
||||||
?.map(s => s.length) ?? [];
|
|
||||||
// No dents? no change required
|
|
||||||
if (!dents || dents.length == 0) return string;
|
|
||||||
const minDent = Math.min(...dents);
|
|
||||||
// The min indentation is 0, no change needed
|
|
||||||
if (!minDent) return string;
|
|
||||||
const re = new RegExp(`^${" ".repeat(minDent)}`, "gm");
|
|
||||||
const dedented = string.replace(re, "");
|
|
||||||
return dedented;
|
|
||||||
}
|
|
@ -1,15 +1,31 @@
|
|||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { dirname as pathDirname, basename as pathBasename } from "path";
|
import {
|
||||||
|
dirname as pathDirname,
|
||||||
|
basename as pathBasename,
|
||||||
|
join as pathJoin,
|
||||||
|
sep as pathSep
|
||||||
|
} from "path";
|
||||||
import { Deferred } from "evt/tools/Deferred";
|
import { Deferred } from "evt/tools/Deferred";
|
||||||
|
import { existsAsync } from "./fs.existsAsync";
|
||||||
|
|
||||||
export async function untrackFromGit(params: { filePath: string }): Promise<void> {
|
export async function untrackFromGit(params: { filePath: string }): Promise<void> {
|
||||||
const { filePath } = params;
|
const { filePath } = params;
|
||||||
|
|
||||||
const dDone = new Deferred<void>();
|
const dDone = new Deferred<void>();
|
||||||
|
|
||||||
|
let relativePath = pathBasename(filePath);
|
||||||
|
|
||||||
|
let dirPath = pathDirname(filePath);
|
||||||
|
|
||||||
|
while (!(await existsAsync(dirPath))) {
|
||||||
|
relativePath = pathJoin(pathBasename(dirPath), relativePath);
|
||||||
|
|
||||||
|
dirPath = pathDirname(dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
child_process.exec(
|
child_process.exec(
|
||||||
`git rm --cached ${pathBasename(filePath)}`,
|
`git rm --cached '${relativePath.split(pathSep).join("/")}'`,
|
||||||
{ cwd: pathDirname(filePath) },
|
{ cwd: dirPath },
|
||||||
error => {
|
error => {
|
||||||
if (error !== null) {
|
if (error !== null) {
|
||||||
dDone.reject(error);
|
dDone.reject(error);
|
||||||
|
@ -10,5 +10,5 @@
|
|||||||
"rootDir": "."
|
"rootDir": "."
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.tsx"],
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
"exclude": ["initialize-account-theme/src"]
|
"exclude": ["initialize-account-theme/multi-page-boilerplate"]
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
await command({ buildContext });
|
await command({ buildContext });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
commandName: "update-kc-gen",
|
commandName: "update-kc-gen",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
@ -769,6 +769,8 @@ export declare namespace Validators {
|
|||||||
export type PasswordPolicies = {
|
export type PasswordPolicies = {
|
||||||
/** The minimum length of the password */
|
/** The minimum length of the password */
|
||||||
length?: number;
|
length?: number;
|
||||||
|
/** The maximum length of the password */
|
||||||
|
maxLength?: number;
|
||||||
/** The minimum number of digits required in the password */
|
/** The minimum number of digits required in the password */
|
||||||
digits?: number;
|
digits?: number;
|
||||||
/** The minimum number of lowercase characters required in the password */
|
/** The minimum number of lowercase characters required in the password */
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { JSX } from "keycloakify/tools/JSX";
|
import type { JSX } from "keycloakify/tools/JSX";
|
||||||
import { useEffect, useReducer, Fragment } from "react";
|
import { useEffect, Fragment } from "react";
|
||||||
import { assert } from "keycloakify/tools/assert";
|
import { assert } from "keycloakify/tools/assert";
|
||||||
|
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||||
import type { KcClsx } from "keycloakify/login/lib/kcClsx";
|
import type { KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import {
|
import {
|
||||||
useUserProfileForm,
|
useUserProfileForm,
|
||||||
@ -89,7 +90,6 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
|
|||||||
{advancedMsg(attribute.annotations.inputHelperTextAfter)}
|
{advancedMsg(attribute.annotations.inputHelperTextAfter)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{AfterField !== undefined && (
|
{AfterField !== undefined && (
|
||||||
<AfterField
|
<AfterField
|
||||||
attribute={attribute}
|
attribute={attribute}
|
||||||
@ -106,6 +106,10 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{/* See: https://github.com/keycloak/keycloak/issues/38029 */}
|
||||||
|
{kcContext.locale !== undefined && formFieldStates.find(x => x.attribute.name === "locale") === undefined && (
|
||||||
|
<input type="hidden" name="locale" value={i18n.currentLanguage.languageTag} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -249,15 +253,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
|||||||
|
|
||||||
const { msgStr } = i18n;
|
const { msgStr } = i18n;
|
||||||
|
|
||||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const passwordInputElement = document.getElementById(passwordInputId);
|
|
||||||
|
|
||||||
assert(passwordInputElement instanceof HTMLInputElement);
|
|
||||||
|
|
||||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
|
||||||
}, [isPasswordRevealed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={kcClsx("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
|
@ -509,6 +509,8 @@ function formStateSelector(params: { state: internal.State }): FormState {
|
|||||||
switch (error.source.name) {
|
switch (error.source.name) {
|
||||||
case "length":
|
case "length":
|
||||||
return hasLostFocusAtLeastOnce;
|
return hasLostFocusAtLeastOnce;
|
||||||
|
case "maxLength":
|
||||||
|
return hasLostFocusAtLeastOnce;
|
||||||
case "digits":
|
case "digits":
|
||||||
return hasLostFocusAtLeastOnce;
|
return hasLostFocusAtLeastOnce;
|
||||||
case "lowerCase":
|
case "lowerCase":
|
||||||
@ -967,6 +969,34 @@ function createGetErrors(params: { kcContext: KcContextLike_useGetErrors }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_password_policy_x: {
|
||||||
|
const policyName = "maxLength";
|
||||||
|
|
||||||
|
const policy = passwordPolicies[policyName];
|
||||||
|
|
||||||
|
if (!policy) {
|
||||||
|
break check_password_policy_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxLength = policy;
|
||||||
|
|
||||||
|
if (value.length <= maxLength) {
|
||||||
|
break check_password_policy_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
advancedMsgArgs: [
|
||||||
|
"invalidPasswordMaxLengthMessage" satisfies MessageKey_defaultSet,
|
||||||
|
`${maxLength}`
|
||||||
|
] as const,
|
||||||
|
fieldIndex: undefined,
|
||||||
|
source: {
|
||||||
|
type: "passwordPolicy",
|
||||||
|
name: policyName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
check_password_policy_x: {
|
check_password_policy_x: {
|
||||||
const policyName = "digits";
|
const policyName = "digits";
|
||||||
|
|
||||||
|
@ -31,10 +31,10 @@ export default function Info(props: PageProps<Extract<KcContext, { pageId: "info
|
|||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: kcSanitize(
|
__html: kcSanitize(
|
||||||
(() => {
|
(() => {
|
||||||
let html = message.summary;
|
let html = message.summary?.trim();
|
||||||
|
|
||||||
if (requiredActions) {
|
if (requiredActions) {
|
||||||
html += "<b>";
|
html += " <b>";
|
||||||
|
|
||||||
html += requiredActions.map(requiredAction => advancedMsgStr(`requiredAction.${requiredAction}`)).join(", ");
|
html += requiredActions.map(requiredAction => advancedMsgStr(`requiredAction.${requiredAction}`)).join(", ");
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { JSX } from "keycloakify/tools/JSX";
|
import type { JSX } from "keycloakify/tools/JSX";
|
||||||
import { useState, useEffect, useReducer } from "react";
|
import { useState } from "react";
|
||||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||||
import { assert } from "keycloakify/tools/assert";
|
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
@ -200,15 +200,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
|||||||
|
|
||||||
const { msgStr } = i18n;
|
const { msgStr } = i18n;
|
||||||
|
|
||||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const passwordInputElement = document.getElementById(passwordInputId);
|
|
||||||
|
|
||||||
assert(passwordInputElement instanceof HTMLInputElement);
|
|
||||||
|
|
||||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
|
||||||
}, [isPasswordRevealed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={kcClsx("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
@ -17,6 +17,8 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
|
|||||||
|
|
||||||
const { msg, msgStr } = i18n;
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
kcContext={kcContext}
|
kcContext={kcContext}
|
||||||
@ -26,7 +28,16 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
|
|||||||
displayMessage={!messagesPerField.existsError("totp")}
|
displayMessage={!messagesPerField.existsError("totp")}
|
||||||
headerNode={msg("doLogIn")}
|
headerNode={msg("doLogIn")}
|
||||||
>
|
>
|
||||||
<form id="kc-otp-login-form" className={kcClsx("kcFormClass")} action={url.loginAction} method="post">
|
<form
|
||||||
|
id="kc-otp-login-form"
|
||||||
|
className={kcClsx("kcFormClass")}
|
||||||
|
action={url.loginAction}
|
||||||
|
onSubmit={() => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
method="post"
|
||||||
|
>
|
||||||
{otpLogin.userOtpCredentials.length > 1 && (
|
{otpLogin.userOtpCredentials.length > 1 && (
|
||||||
<div className={kcClsx("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div className={kcClsx("kcInputWrapperClass")}>
|
<div className={kcClsx("kcInputWrapperClass")}>
|
||||||
@ -94,6 +105,7 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
|
|||||||
id="kc-login"
|
id="kc-login"
|
||||||
type="submit"
|
type="submit"
|
||||||
value={msgStr("doLogIn")}
|
value={msgStr("doLogIn")}
|
||||||
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import type { JSX } from "keycloakify/tools/JSX";
|
import type { JSX } from "keycloakify/tools/JSX";
|
||||||
import { useState, useEffect, useReducer } from "react";
|
import { useState } from "react";
|
||||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
import { assert } from "keycloakify/tools/assert";
|
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
@ -107,15 +107,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
|||||||
|
|
||||||
const { msgStr } = i18n;
|
const { msgStr } = i18n;
|
||||||
|
|
||||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const passwordInputElement = document.getElementById(passwordInputId);
|
|
||||||
|
|
||||||
assert(passwordInputElement instanceof HTMLInputElement);
|
|
||||||
|
|
||||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
|
||||||
}, [isPasswordRevealed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={kcClsx("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { JSX } from "keycloakify/tools/JSX";
|
import type { JSX } from "keycloakify/tools/JSX";
|
||||||
import { useEffect, useReducer } from "react";
|
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||||
import { assert } from "keycloakify/tools/assert";
|
|
||||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
@ -146,15 +145,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
|||||||
|
|
||||||
const { msgStr } = i18n;
|
const { msgStr } = i18n;
|
||||||
|
|
||||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const passwordInputElement = document.getElementById(passwordInputId);
|
|
||||||
|
|
||||||
assert(passwordInputElement instanceof HTMLInputElement);
|
|
||||||
|
|
||||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
|
||||||
}, [isPasswordRevealed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={kcClsx("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
|
@ -98,7 +98,7 @@ export default function LoginUsername(props: PageProps<Extract<KcContext, { page
|
|||||||
defaultValue={login.username ?? ""}
|
defaultValue={login.username ?? ""}
|
||||||
type="text"
|
type="text"
|
||||||
autoFocus
|
autoFocus
|
||||||
autoComplete="off"
|
autoComplete="username"
|
||||||
aria-invalid={messagesPerField.existsError("username")}
|
aria-invalid={messagesPerField.existsError("username")}
|
||||||
/>
|
/>
|
||||||
{messagesPerField.existsError("username") && (
|
{messagesPerField.existsError("username") && (
|
||||||
|
45
src/tools/useIsPasswordRevealed.ts
Normal file
45
src/tools/useIsPasswordRevealed.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { useEffect, useReducer } from "react";
|
||||||
|
import { assert } from "keycloakify/tools/assert";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initially false, state that enables to dynamically control if
|
||||||
|
* the type of a password input is "password" (false) or "text" (true).
|
||||||
|
*/
|
||||||
|
export function useIsPasswordRevealed(params: { passwordInputId: string }) {
|
||||||
|
const { passwordInputId } = params;
|
||||||
|
|
||||||
|
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer(
|
||||||
|
(isPasswordRevealed: boolean) => !isPasswordRevealed,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const passwordInputElement = document.getElementById(passwordInputId);
|
||||||
|
|
||||||
|
assert(passwordInputElement instanceof HTMLInputElement);
|
||||||
|
|
||||||
|
const type = isPasswordRevealed ? "text" : "password";
|
||||||
|
|
||||||
|
passwordInputElement.type = type;
|
||||||
|
|
||||||
|
const observer = new MutationObserver(mutations => {
|
||||||
|
mutations.forEach(mutation => {
|
||||||
|
if (mutation.attributeName !== "type") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (passwordInputElement.type === type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
passwordInputElement.type = type;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(passwordInputElement, { attributes: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [isPasswordRevealed]);
|
||||||
|
|
||||||
|
return { isPasswordRevealed, toggleIsPasswordRevealed };
|
||||||
|
}
|
@ -155,8 +155,9 @@ export function keycloakify(params: keycloakify.Params) {
|
|||||||
{
|
{
|
||||||
const isJavascriptFile = id.endsWith(".js") || id.endsWith(".jsx");
|
const isJavascriptFile = id.endsWith(".js") || id.endsWith(".jsx");
|
||||||
const isTypeScriptFile = id.endsWith(".ts") || id.endsWith(".tsx");
|
const isTypeScriptFile = id.endsWith(".ts") || id.endsWith(".tsx");
|
||||||
|
const isSvelteFile = id.endsWith(".svelte");
|
||||||
|
|
||||||
if (!isTypeScriptFile && !isJavascriptFile) {
|
if (!isTypeScriptFile && !isJavascriptFile && !isSvelteFile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ export const WithRequiredActions: Story = {
|
|||||||
kcContext={{
|
kcContext={{
|
||||||
messageHeader: "Message header",
|
messageHeader: "Message header",
|
||||||
message: {
|
message: {
|
||||||
summary: "Required actions: "
|
summary: "Required actions:"
|
||||||
},
|
},
|
||||||
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PROFILE", "VERIFY_EMAIL", "CUSTOM_ACTION"],
|
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PROFILE", "VERIFY_EMAIL", "CUSTOM_ACTION"],
|
||||||
"x-keycloakify": {
|
"x-keycloakify": {
|
||||||
|
@ -62,3 +62,22 @@ export const WithPasswordConfirmError: Story = {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WithAppInitiatedAction:
|
||||||
|
* - Purpose: Tests when the update password action was triggered by an app.
|
||||||
|
* - Scenario: Simulates the case where the user presses a 'change password' button in an app and is redirected to Keycloak to change it.
|
||||||
|
* - Key Aspect: Ensures the 'Cancel' button is shown correctly, which displays only when the action is app initiated.
|
||||||
|
*/
|
||||||
|
export const WithAppInitiatedAction: Story = {
|
||||||
|
render: () => (
|
||||||
|
<KcPageStory
|
||||||
|
kcContext={{
|
||||||
|
url: {
|
||||||
|
loginAction: "/mock-login-action"
|
||||||
|
},
|
||||||
|
isAppInitiatedAction: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
@ -17,5 +17,5 @@
|
|||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": ["../src", "."],
|
"include": ["../src", "."],
|
||||||
"exclude": ["../src/bin/initialize-account-theme/src"]
|
"exclude": ["../src/bin/initialize-account-theme/multi-page-boilerplate"]
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user