Compare commits
No commits in common. "main" and "v11.5.1" have entirely different histories.
@ -327,42 +327,6 @@
|
||||
"contributions": [
|
||||
"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,
|
||||
|
1
.github/FUNDING.yaml
vendored
1
.github/FUNDING.yaml
vendored
@ -1,3 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [garronej]
|
||||
custom: ['https://www.ringerhq.com/experts/garronej']
|
||||
|
10
README.md
10
README.md
@ -6,7 +6,7 @@
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://github.com/garronej/keycloakify/actions">
|
||||
<img src="https://github.com/keycloakify/keycloakify/actions/workflows/ci.yaml/badge.svg">
|
||||
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/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**
|
||||
> 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.
|
||||
> 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).
|
||||
> 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).
|
||||
|
||||
## Sponsors
|
||||
|
||||
@ -168,12 +168,6 @@ 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/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>
|
||||
<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>
|
||||
</table>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "11.8.23",
|
||||
"version": "11.5.1",
|
||||
"description": "Framework to create custom Keycloak UIs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -67,9 +67,7 @@ export function vendorFrontendDependencies(params: { distDirPath: string }) {
|
||||
)
|
||||
);
|
||||
|
||||
run(`npx webpack --config ${pathBasename(webpackConfigJsFilePath)}`, {
|
||||
cwd: pathDirname(webpackConfigJsFilePath)
|
||||
});
|
||||
run(`npx webpack --config ${webpackConfigJsFilePath}`);
|
||||
|
||||
fs.readdirSync(webpackOutputDirPath)
|
||||
.filter(fileBasename => !fileBasename.endsWith(".txt"))
|
||||
|
@ -3,9 +3,10 @@ import child_process from "child_process";
|
||||
import { SemVer } from "../src/bin/tools/SemVer";
|
||||
import { dumpContainerConfig } from "../src/bin/start-keycloak/realmConfig/dumpContainerConfig";
|
||||
import { cacheDirPath } from "./shared/cacheDirPath";
|
||||
import { runPrettier } from "../src/bin/tools/runPrettier";
|
||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import { writeRealmJsonFile } from "../src/bin/start-keycloak/realmConfig/ParsedRealmJson";
|
||||
import { join as pathJoin } from "path";
|
||||
import * as fs from "fs";
|
||||
import chalk from "chalk";
|
||||
|
||||
(async () => {
|
||||
@ -25,7 +26,9 @@ import chalk from "chalk";
|
||||
realmName: "myrealm"
|
||||
});
|
||||
|
||||
const realmJsonFilePath = pathJoin(
|
||||
let sourceCode = JSON.stringify(parsedRealmJson, null, 2);
|
||||
|
||||
const filePath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
@ -35,11 +38,12 @@ import chalk from "chalk";
|
||||
`realm-kc-${keycloakMajorVersionNumber}.json`
|
||||
);
|
||||
|
||||
await writeRealmJsonFile({
|
||||
parsedRealmJson,
|
||||
realmJsonFilePath,
|
||||
keycloakMajorVersionNumber
|
||||
sourceCode = await runPrettier({
|
||||
sourceCode,
|
||||
filePath
|
||||
});
|
||||
|
||||
console.log(chalk.green(`Realm config dumped to ${realmJsonFilePath}`));
|
||||
fs.writeFileSync(filePath, Buffer.from(sourceCode, "utf8"));
|
||||
|
||||
console.log(chalk.green(`Realm config dumped to ${filePath}`));
|
||||
})();
|
||||
|
@ -45,10 +45,7 @@ const commonThirdPartyDeps = [
|
||||
.replace(/"!\.\/dist\//g, '"!./');
|
||||
|
||||
modifiedPackageJsonContent = JSON.stringify(
|
||||
{
|
||||
...JSON.parse(modifiedPackageJsonContent),
|
||||
version: `0.0.0-rc.${~~(Math.random() * 1000000)}`
|
||||
},
|
||||
{ ...JSON.parse(modifiedPackageJsonContent), version: "0.0.0" },
|
||||
null,
|
||||
4
|
||||
);
|
||||
|
@ -280,24 +280,6 @@ export async function downloadKeycloakDefaultTheme(params: {
|
||||
"fonts",
|
||||
"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("jquery", "dist", "jquery.min.js"),
|
||||
pathJoin("rfc4648", "lib", "rfc4648.js")
|
||||
|
@ -20,7 +20,7 @@ import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_dele
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "add-story",
|
||||
buildContext
|
||||
});
|
||||
@ -74,7 +74,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
|
||||
if (themeType === "admin") {
|
||||
console.log(
|
||||
`${chalk.red("✗")} Sorry, there is no Storybook support for the Admin UI.`
|
||||
`${chalk.red("✗")} Sorry, there is no Storybook support for the Account UI.`
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
|
@ -11,7 +11,7 @@ import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "copy-keycloak-resources-to-public",
|
||||
buildContext
|
||||
});
|
||||
|
68
src/bin/eject-file.ts
Normal file
68
src/bin/eject-file.ts
Normal file
@ -0,0 +1,68 @@
|
||||
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,7 +11,12 @@ import {
|
||||
} from "./shared/constants";
|
||||
import { capitalize } from "tsafe/capitalize";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import {
|
||||
join as pathJoin,
|
||||
relative as pathRelative,
|
||||
dirname as pathDirname,
|
||||
basename as pathBasename
|
||||
} from "path";
|
||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
@ -22,7 +27,7 @@ import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "eject-page",
|
||||
buildContext
|
||||
});
|
||||
@ -62,7 +67,9 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
})();
|
||||
|
||||
if (themeType === "admin") {
|
||||
console.log("Use `npx keycloakify own` command instead, see documentation");
|
||||
console.log(
|
||||
"Use `npx keycloakify eject-file` command instead, see documentation"
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
@ -72,16 +79,85 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
||||
buildContext.implementedThemeTypes.account.type === "Single-Page")
|
||||
) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
[
|
||||
"You are implementing a Single-Page Account theme.",
|
||||
"The eject-page command isn't applicable in this context"
|
||||
].join("\n")
|
||||
)
|
||||
const srcDirPath = pathJoin(
|
||||
pathDirname(buildContext.packageJsonFilePath),
|
||||
"node_modules",
|
||||
"@keycloakify",
|
||||
`keycloak-account-ui`,
|
||||
"src"
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
console.log(
|
||||
[
|
||||
`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:`,
|
||||
``,
|
||||
`${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`,
|
||||
``
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
eject_entrypoint: {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -92,14 +168,12 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
const templateValue = "Template.tsx (Layout common to every page)";
|
||||
const userProfileFormFieldsValue =
|
||||
"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<
|
||||
| LoginThemePageId
|
||||
| AccountThemePageId
|
||||
| typeof templateValue
|
||||
| typeof userProfileFormFieldsValue
|
||||
| typeof otherPageValue
|
||||
>({
|
||||
values: (() => {
|
||||
switch (themeType) {
|
||||
@ -107,11 +181,10 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
return [
|
||||
templateValue,
|
||||
userProfileFormFieldsValue,
|
||||
...LOGIN_THEME_PAGE_IDS,
|
||||
otherPageValue
|
||||
...LOGIN_THEME_PAGE_IDS
|
||||
];
|
||||
case "account":
|
||||
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS, otherPageValue];
|
||||
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS];
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()
|
||||
@ -119,17 +192,6 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
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}`);
|
||||
|
||||
const componentBasename = (() => {
|
||||
|
32
src/bin/initialize-account-theme/copyBoilerplate.ts
Normal file
32
src/bin/initialize-account-theme/copyBoilerplate.ts
Normal file
@ -0,0 +1,32 @@
|
||||
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,12 +7,11 @@ import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeIm
|
||||
import { command as updateKcGenCommand } from "../update-kc-gen";
|
||||
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
|
||||
import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges";
|
||||
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "initialize-account-theme",
|
||||
buildContext
|
||||
});
|
||||
@ -23,6 +22,22 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
|
||||
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({
|
||||
projectDirPath: buildContext.projectDirPath
|
||||
});
|
||||
@ -36,41 +51,23 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
switch (accountThemeType) {
|
||||
case "Multi-Page":
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
fs.cpSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"initialize-account-theme",
|
||||
"multi-page-boilerplate"
|
||||
),
|
||||
accountThemeSrcDirPath,
|
||||
{ recursive: true }
|
||||
const { initializeAccountTheme_multiPage } = await import(
|
||||
"./initializeAccountTheme_multiPage"
|
||||
);
|
||||
|
||||
await initializeAccountTheme_multiPage({
|
||||
accountThemeSrcDirPath
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "Single-Page":
|
||||
{
|
||||
const { initializeSpa } = await import("../shared/initializeSpa");
|
||||
const { initializeAccountTheme_singlePage } = await import(
|
||||
"./initializeAccountTheme_singlePage"
|
||||
);
|
||||
|
||||
await initializeSpa({
|
||||
themeType: "account",
|
||||
await initializeAccountTheme_singlePage({
|
||||
accountThemeSrcDirPath,
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
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")
|
||||
);
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
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 type { ThemeName } from "../kc.gen";
|
||||
|
||||
/** @see: https://docs.keycloakify.dev/features/i18n */
|
||||
/** @see: https://docs.keycloakify.dev/i18n */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { useI18n, ofTypeI18n } = i18nBuilder.withThemeName<ThemeName>().build();
|
||||
|
||||
type I18n = typeof ofTypeI18n;
|
@ -0,0 +1,7 @@
|
||||
import type { KcContextLike } from "@keycloakify/keycloak-account-ui";
|
||||
import type { KcEnvName } from "../kc.gen";
|
||||
|
||||
export type KcContext = KcContextLike & {
|
||||
themeType: "account";
|
||||
properties: Record<KcEnvName, string>;
|
||||
};
|
11
src/bin/initialize-account-theme/src/single-page/KcPage.tsx
Normal file
11
src/bin/initialize-account-theme/src/single-page/KcPage.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
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} />;
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
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,23 +1,18 @@
|
||||
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 cliSelect from "cli-select";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges";
|
||||
|
||||
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";
|
||||
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import fetch from "make-fetch-happen";
|
||||
import { SemVer } from "./tools/SemVer";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "initialize-email-theme",
|
||||
buildContext
|
||||
});
|
||||
@ -26,10 +21,6 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
return;
|
||||
}
|
||||
|
||||
exitIfUncommittedChanges({
|
||||
projectDirPath: buildContext.projectDirPath
|
||||
});
|
||||
|
||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
||||
|
||||
if (
|
||||
@ -37,120 +28,93 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
fs.readdirSync(emailThemeSrcDirPath).length > 0
|
||||
) {
|
||||
console.warn(
|
||||
chalk.red(
|
||||
`There is already a ${pathRelative(
|
||||
process.cwd(),
|
||||
emailThemeSrcDirPath
|
||||
)} directory in your project. Aborting.`
|
||||
)
|
||||
`There is already a non empty ${pathRelative(
|
||||
process.cwd(),
|
||||
emailThemeSrcDirPath
|
||||
)} directory in your project. Aborting.`
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const { value: emailThemeType } = await cliSelect({
|
||||
values: [
|
||||
"native (FreeMarker)" as const,
|
||||
"Another email templating solution" as const
|
||||
]
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
console.log("Initialize with the base email theme from which version of Keycloak?");
|
||||
|
||||
if (emailThemeType === "Another email templating solution") {
|
||||
console.log(
|
||||
[
|
||||
"There is currently no automated support for keycloakify-email, it has to be done manually, see documentation:",
|
||||
"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,
|
||||
let { keycloakVersion } = await promptKeycloakVersion({
|
||||
// NOTE: This is arbitrary
|
||||
startingFromMajor: 17,
|
||||
excludeMajorVersions: [],
|
||||
doOmitPatch: false,
|
||||
buildContext
|
||||
});
|
||||
|
||||
const moduleName = `@keycloakify/email-native`;
|
||||
const getUrl = (keycloakVersion: string) => {
|
||||
return `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`;
|
||||
};
|
||||
|
||||
const [version] = ((): string[] => {
|
||||
const cmdOutput = child_process
|
||||
.execSync(`npm show ${moduleName} versions --json`)
|
||||
.toString("utf8")
|
||||
.trim();
|
||||
keycloakVersion = await (async () => {
|
||||
const keycloakVersionParsed = SemVer.parse(keycloakVersion);
|
||||
|
||||
const versions = JSON.parse(cmdOutput) as string | string[];
|
||||
while (true) {
|
||||
const url = getUrl(SemVer.stringify(keycloakVersionParsed));
|
||||
|
||||
// NOTE: Bug in some older npm versions
|
||||
if (typeof versions === "string") {
|
||||
return [versions];
|
||||
const response = await fetch(url, buildContext.fetchOptions);
|
||||
|
||||
if (response.ok) {
|
||||
break;
|
||||
}
|
||||
|
||||
assert(keycloakVersionParsed.patch !== 0);
|
||||
|
||||
keycloakVersionParsed.patch--;
|
||||
}
|
||||
|
||||
return versions;
|
||||
})()
|
||||
.reverse()
|
||||
.filter(version => !version.includes("-"));
|
||||
return SemVer.stringify(keycloakVersionParsed);
|
||||
})();
|
||||
|
||||
assert(version !== undefined);
|
||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||
url: getUrl(keycloakVersion),
|
||||
cacheDirPath: buildContext.cacheDirPath,
|
||||
fetchOptions: buildContext.fetchOptions,
|
||||
uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme",
|
||||
onArchiveFile: async ({ fileRelativePath, writeFile }) => {
|
||||
const fileRelativePath_target = pathRelative(
|
||||
pathJoin("theme", "base", "email"),
|
||||
fileRelativePath
|
||||
);
|
||||
|
||||
(parsedPackageJson.dependencies ??= {})[moduleName] = `~${version}`;
|
||||
if (fileRelativePath_target.startsWith("..")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedPackageJson.devDependencies !== undefined) {
|
||||
delete parsedPackageJson.devDependencies[moduleName];
|
||||
}
|
||||
await writeFile({ fileRelativePath: fileRelativePath_target });
|
||||
}
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: extractedDirPath,
|
||||
destDirPath: emailThemeSrcDirPath
|
||||
});
|
||||
|
||||
{
|
||||
let sourceCode = JSON.stringify(parsedPackageJson, undefined, 2);
|
||||
|
||||
if (await getIsPrettierAvailable()) {
|
||||
sourceCode = await runPrettier({
|
||||
sourceCode,
|
||||
filePath: buildContext.packageJsonFilePath
|
||||
});
|
||||
}
|
||||
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties");
|
||||
|
||||
fs.writeFileSync(
|
||||
buildContext.packageJsonFilePath,
|
||||
Buffer.from(sourceCode, "utf8")
|
||||
themePropertyFilePath,
|
||||
Buffer.from(
|
||||
[
|
||||
`parent=base`,
|
||||
fs.readFileSync(themePropertyFilePath).toString("utf8")
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await npmInstall({
|
||||
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||
});
|
||||
|
||||
console.log(chalk.green("Email theme initialized."));
|
||||
console.log(
|
||||
`The \`${pathJoin(
|
||||
".",
|
||||
pathRelative(process.cwd(), emailThemeSrcDirPath)
|
||||
)}\` directory have been created.`
|
||||
);
|
||||
console.log("You can delete any file you don't modify.");
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import { readFileSync } from "fs";
|
||||
import { isInside } from "../../tools/isInside";
|
||||
import child_process from "child_process";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
||||
import { existsAsync } from "../../tools/fs.existsAsync";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_generatePom & {
|
||||
@ -105,55 +106,29 @@ export async function buildJar(params: {
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
const filePath = pathJoin(
|
||||
tmpResourcesDirPath,
|
||||
"META-INF",
|
||||
"keycloak-themes.json"
|
||||
);
|
||||
remove_account_v1_in_meta_inf: {
|
||||
if (!doesImplementAccountV1Theme) {
|
||||
// NOTE: We do not have account v1 anyway
|
||||
break remove_account_v1_in_meta_inf;
|
||||
}
|
||||
|
||||
await fs.mkdir(pathDirname(filePath));
|
||||
if (keycloakAccountV1Version !== null) {
|
||||
// NOTE: No, we need to keep account-v1 in meta-inf
|
||||
break remove_account_v1_in_meta_inf;
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
Buffer.from(
|
||||
JSON.stringify(
|
||||
{
|
||||
themes: await (async () => {
|
||||
const dirPath = pathJoin(tmpResourcesDirPath, "theme");
|
||||
writeMetaInfKeycloakThemes({
|
||||
resourcesDirPath: tmpResourcesDirPath,
|
||||
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
|
||||
assert(metaInfKeycloakTheme !== undefined);
|
||||
|
||||
const themeNames = (await fs.readdir(dirPath)).sort(
|
||||
(a, b) => {
|
||||
const indexA = buildContext.themeNames.indexOf(a);
|
||||
const indexB = buildContext.themeNames.indexOf(b);
|
||||
metaInfKeycloakTheme.themes = metaInfKeycloakTheme.themes.filter(
|
||||
({ name }) => name !== "account-v1"
|
||||
);
|
||||
|
||||
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"
|
||||
)
|
||||
);
|
||||
return metaInfKeycloakTheme;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
route_legacy_pages: {
|
||||
@ -220,39 +195,31 @@ export async function buildJar(params: {
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const mvnBuildCmd = `mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`;
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
child_process.exec(
|
||||
`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
|
||||
)}`
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
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();
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
await fs.rename(
|
||||
pathJoin(
|
||||
|
@ -190,7 +190,7 @@ function decodeHtmlEntities(htmlStr){
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
|
||||
key == "loginAction" &&
|
||||
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", "frontchannel-logout.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"]?seq_contains(xKeycloakify.pageId) &&
|
||||
!(auth?has_content && auth.showTryAnotherWayLink())
|
||||
) || (
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
|
||||
|
@ -6,7 +6,8 @@ import {
|
||||
join as pathJoin,
|
||||
relative as pathRelative,
|
||||
dirname as pathDirname,
|
||||
basename as pathBasename
|
||||
extname as pathExtname,
|
||||
sep as pathSep
|
||||
} from "path";
|
||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||
@ -30,13 +31,15 @@ import {
|
||||
type BuildContextLike as BuildContextLike_generateMessageProperties
|
||||
} from "./generateMessageProperties";
|
||||
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
||||
import {
|
||||
writeMetaInfKeycloakThemes,
|
||||
type MetaInfKeycloakTheme
|
||||
} from "../../shared/metaInfKeycloakThemes";
|
||||
import { objectEntries } from "tsafe/objectEntries";
|
||||
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
||||
import * as child_process from "child_process";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
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 &
|
||||
BuildContextLike_generateMessageProperties & {
|
||||
@ -57,8 +60,6 @@ export async function generateResources(params: {
|
||||
buildContext: BuildContextLike;
|
||||
resourcesDirPath: string;
|
||||
}): Promise<void> {
|
||||
const start = Date.now();
|
||||
|
||||
const { resourcesDirPath, buildContext } = params;
|
||||
|
||||
const [themeName] = buildContext.themeNames;
|
||||
@ -76,23 +77,12 @@ export async function generateResources(params: {
|
||||
};
|
||||
|
||||
const writeMessagePropertiesFilesByThemeType: Partial<
|
||||
Record<
|
||||
ThemeType | "email",
|
||||
(params: { messageDirPath: string; themeName: string }) => void
|
||||
>
|
||||
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
|
||||
> = {};
|
||||
|
||||
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||
let isNative: boolean;
|
||||
|
||||
{
|
||||
const v = buildContext.implementedThemeTypes[themeType];
|
||||
|
||||
if (!v.isImplemented && !v.isImplemented_native) {
|
||||
continue;
|
||||
}
|
||||
|
||||
isNative = !v.isImplemented && v.isImplemented_native;
|
||||
for (const themeType of THEME_TYPES) {
|
||||
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const getAccountThemeType = () => {
|
||||
@ -111,18 +101,12 @@ export async function generateResources(params: {
|
||||
return getAccountThemeType() === "Single-Page";
|
||||
case "admin":
|
||||
return true;
|
||||
case "email":
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
|
||||
|
||||
apply_replacers_and_move_to_theme_resources: {
|
||||
if (isNative) {
|
||||
break apply_replacers_and_move_to_theme_resources;
|
||||
}
|
||||
|
||||
const destDirPath = pathJoin(
|
||||
themeTypeDirPath,
|
||||
"resources",
|
||||
@ -206,93 +190,59 @@ export async function generateResources(params: {
|
||||
});
|
||||
}
|
||||
|
||||
generate_ftl_files: {
|
||||
if (isNative) {
|
||||
break generate_ftl_files;
|
||||
}
|
||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||
themeName,
|
||||
indexHtmlCode: fs
|
||||
.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");
|
||||
|
||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||
themeName,
|
||||
indexHtmlCode: fs
|
||||
.readFileSync(
|
||||
pathJoin(buildContext.projectBuildDirPath, "index.html")
|
||||
)
|
||||
.toString("utf8"),
|
||||
buildContext,
|
||||
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 };
|
||||
[
|
||||
...(() => {
|
||||
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")
|
||||
);
|
||||
});
|
||||
|
||||
let languageTags: string[] | undefined = undefined;
|
||||
|
||||
i18n_multi_page: {
|
||||
if (isNative) {
|
||||
break i18n_multi_page;
|
||||
}
|
||||
|
||||
i18n_messages_generation: {
|
||||
if (isSpa) {
|
||||
break i18n_multi_page;
|
||||
break i18n_messages_generation;
|
||||
}
|
||||
|
||||
assert(themeType !== "admin" && themeType !== "email");
|
||||
assert(themeType !== "admin");
|
||||
|
||||
const wrap = generateMessageProperties({
|
||||
buildContext,
|
||||
@ -306,43 +256,21 @@ export async function generateResources(params: {
|
||||
writeMessagePropertiesFiles;
|
||||
}
|
||||
|
||||
let isLegacyAccountSpa = false;
|
||||
|
||||
// NOTE: Eventually remove this block.
|
||||
i18n_single_page_account_legacy: {
|
||||
bring_in_spas_messages: {
|
||||
if (!isSpa) {
|
||||
break i18n_single_page_account_legacy;
|
||||
break bring_in_spas_messages;
|
||||
}
|
||||
|
||||
if (themeType !== "account") {
|
||||
break i18n_single_page_account_legacy;
|
||||
}
|
||||
assert(themeType !== "login");
|
||||
|
||||
const [moduleMeta] = await listInstalledModules({
|
||||
packageJsonFilePath: buildContext.packageJsonFilePath,
|
||||
filter: ({ moduleName }) =>
|
||||
moduleName === "@keycloakify/keycloak-account-ui"
|
||||
});
|
||||
const accountUiDirPath = child_process
|
||||
.execSync(`npm list @keycloakify/keycloak-${themeType}-ui --parseable`, {
|
||||
cwd: pathDirname(buildContext.packageJsonFilePath)
|
||||
})
|
||||
.toString("utf8")
|
||||
.trim();
|
||||
|
||||
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");
|
||||
const messageDirPath_defaults = pathJoin(accountUiDirPath, "messages");
|
||||
|
||||
if (!fs.existsSync(messageDirPath_defaults)) {
|
||||
throw new Error(
|
||||
@ -350,10 +278,8 @@ export async function generateResources(params: {
|
||||
);
|
||||
}
|
||||
|
||||
isLegacyAccountSpa = true;
|
||||
|
||||
const messagesDirPath_dest = pathJoin(
|
||||
getThemeTypeDirPath({ themeName, themeType: "account" }),
|
||||
getThemeTypeDirPath({ themeName, themeType }),
|
||||
"messages"
|
||||
);
|
||||
|
||||
@ -365,7 +291,7 @@ export async function generateResources(params: {
|
||||
apply_theme_changes: {
|
||||
const messagesDirPath_theme = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
"account",
|
||||
themeType,
|
||||
"messages"
|
||||
);
|
||||
|
||||
@ -413,167 +339,7 @@ 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: {
|
||||
if (isNative) {
|
||||
break keycloak_static_resources;
|
||||
}
|
||||
|
||||
if (isSpa) {
|
||||
break keycloak_static_resources;
|
||||
}
|
||||
@ -590,167 +356,183 @@ export async function generateResources(params: {
|
||||
});
|
||||
}
|
||||
|
||||
bring_in_account_v1: {
|
||||
if (isNative) {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
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>>(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"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (themeType !== "account") {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
email: {
|
||||
if (!buildContext.implementedThemeTypes.email.isImplemented) {
|
||||
break email;
|
||||
}
|
||||
|
||||
assert(buildContext.implementedThemeTypes.account.isImplemented);
|
||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
||||
|
||||
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
transformCodebase({
|
||||
srcDirPath: emailThemeSrcDirPath,
|
||||
destDirPath: getThemeTypeDirPath({ themeName, themeType: "email" })
|
||||
});
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
|
||||
destDirPath: getThemeTypeDirPath({
|
||||
themeName: "account-v1",
|
||||
themeType: "account"
|
||||
})
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
generate_theme_properties: {
|
||||
if (isNative) {
|
||||
break generate_theme_properties;
|
||||
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 };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 themeName of buildContext.themeNames) {
|
||||
for (const [themeType, writeMessagePropertiesFiles] of objectEntries(
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
modify_email_theme_per_variant: {
|
||||
if (!buildContext.implementedThemeTypes.email.isImplemented) {
|
||||
break modify_email_theme_per_variant;
|
||||
}
|
||||
|
||||
{
|
||||
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 };
|
||||
}
|
||||
for (const themeName of buildContext.themeNames) {
|
||||
const emailThemeDirPath = getThemeTypeDirPath({
|
||||
themeName,
|
||||
themeType: "email"
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: emailThemeDirPath,
|
||||
destDirPath: emailThemeDirPath,
|
||||
transformSourceCode: ({ filePath, sourceCode }) => {
|
||||
if (!filePath.endsWith(".ftl")) {
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
run_writeMessagePropertiesFiles: {
|
||||
const writeMessagePropertiesFiles =
|
||||
writeMessagePropertiesFilesByThemeType[themeType];
|
||||
|
||||
if (writeMessagePropertiesFiles === undefined) {
|
||||
break run_writeMessagePropertiesFiles;
|
||||
return {
|
||||
modifiedSourceCode: Buffer.from(
|
||||
sourceCode
|
||||
.toString("utf8")
|
||||
.replace(/xKeycloakify\.themeName/g, `"${themeName}"`),
|
||||
"utf8"
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
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,6 +5,9 @@ import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
|
||||
import * as child_process from "child_process";
|
||||
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import { SemVer } from "./tools/SemVer";
|
||||
import { assert, is } from "tsafe/assert";
|
||||
import chalk from "chalk";
|
||||
|
||||
type CliCommandOptions = {
|
||||
projectDirPath: string | undefined;
|
||||
@ -134,11 +137,47 @@ program
|
||||
handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => {
|
||||
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({
|
||||
buildContext: getBuildContext({ projectDirPath }),
|
||||
cliCommandOptions: {
|
||||
keycloakVersion:
|
||||
keycloakVersion === undefined ? undefined : `${keycloakVersion}`,
|
||||
keycloakVersion,
|
||||
port,
|
||||
realmJsonFilePath
|
||||
}
|
||||
@ -191,7 +230,7 @@ program
|
||||
program
|
||||
.command({
|
||||
name: "initialize-account-theme",
|
||||
description: "Initialize an Account Single-Page or Multi-Page custom Account UI."
|
||||
description: "Initialize the account theme."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
@ -202,20 +241,6 @@ 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
|
||||
.command({
|
||||
name: "copy-keycloak-resources-to-public",
|
||||
@ -248,30 +273,13 @@ program
|
||||
|
||||
program
|
||||
.command({
|
||||
name: "sync-extensions",
|
||||
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")
|
||||
name: "postinstall",
|
||||
description: "Initialize all the Keycloakify UI modules installed in the project."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./sync-extensions");
|
||||
const { command } = await import("./postinstall");
|
||||
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
@ -279,66 +287,36 @@ program
|
||||
|
||||
program
|
||||
.command<{
|
||||
path: string;
|
||||
revert: boolean;
|
||||
file: string;
|
||||
}>({
|
||||
name: "own",
|
||||
name: "eject-file",
|
||||
description: [
|
||||
"Manages ownership of auto-generated files provided by Keycloakify extensions.",
|
||||
"",
|
||||
"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'`"
|
||||
"WARNING: Not usable yet, will be used for future features",
|
||||
"Take ownership over a given file"
|
||||
].join(" ")
|
||||
})
|
||||
.option({
|
||||
key: "revert",
|
||||
key: "file",
|
||||
name: (() => {
|
||||
const long = "revert";
|
||||
const short = "r";
|
||||
const name = "file";
|
||||
|
||||
optionsKeys.push(long, short);
|
||||
optionsKeys.push(name);
|
||||
|
||||
return { long, short };
|
||||
return name;
|
||||
})(),
|
||||
description: [
|
||||
"Restores a file or directory to its original auto-generated state,",
|
||||
"removing your ownership claim and reverting any modifications."
|
||||
].join(" "),
|
||||
defaultValue: false
|
||||
"Relative path of the file relative to the directory of your keycloak theme source",
|
||||
"Example `--file src/login/page/Login.tsx`"
|
||||
].join(" ")
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath, path, revert }) => {
|
||||
const { command } = await import("./own");
|
||||
handler: async ({ projectDirPath, file }) => {
|
||||
const { command } = await import("./eject-file");
|
||||
|
||||
await command({
|
||||
buildContext: getBuildContext({ projectDirPath }),
|
||||
cliCommandOptions: { path, isRevert: revert }
|
||||
cliCommandOptions: { file }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
208
src/bin/own.ts
208
src/bin/own.ts
@ -1,208 +0,0 @@
|
||||
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 });
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
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
src/bin/postinstall/index.ts
Normal file
1
src/bin/postinstall/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./postinstall";
|
@ -1,6 +1,6 @@
|
||||
import { assert, type Equals, is } from "tsafe/assert";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import type { ExtensionModuleMeta } from "./extensionModuleMeta";
|
||||
import type { UiModuleMeta } from "./uiModuleMeta";
|
||||
import { z } from "zod";
|
||||
import { id } from "tsafe/id";
|
||||
import * as fsPr from "fs/promises";
|
||||
@ -16,29 +16,29 @@ export type BuildContextLike = {
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export type ExtensionModuleMetaLike = {
|
||||
export type UiModuleMetaLike = {
|
||||
moduleName: string;
|
||||
peerDependencies: Record<string, string>;
|
||||
};
|
||||
|
||||
assert<ExtensionModuleMeta extends ExtensionModuleMetaLike ? true : false>();
|
||||
assert<UiModuleMeta extends UiModuleMetaLike ? true : false>();
|
||||
|
||||
export async function installExtensionModulesPeerDependencies(params: {
|
||||
export async function installUiModulesPeerDependencies(params: {
|
||||
buildContext: BuildContextLike;
|
||||
extensionModuleMetas: ExtensionModuleMetaLike[];
|
||||
uiModuleMetas: UiModuleMetaLike[];
|
||||
}): Promise<void | never> {
|
||||
const { buildContext, extensionModuleMetas } = params;
|
||||
const { buildContext, uiModuleMetas } = params;
|
||||
|
||||
const { extensionModulesPerDependencies } = (() => {
|
||||
const extensionModulesPerDependencies: Record<string, string> = {};
|
||||
const { uiModulesPerDependencies } = (() => {
|
||||
const uiModulesPerDependencies: Record<string, string> = {};
|
||||
|
||||
for (const { peerDependencies } of extensionModuleMetas) {
|
||||
for (const { peerDependencies } of uiModuleMetas) {
|
||||
for (const [peerDependencyName, versionRange_candidate] of Object.entries(
|
||||
peerDependencies
|
||||
)) {
|
||||
const versionRange = (() => {
|
||||
const versionRange_current =
|
||||
extensionModulesPerDependencies[peerDependencyName];
|
||||
uiModulesPerDependencies[peerDependencyName];
|
||||
|
||||
if (versionRange_current === undefined) {
|
||||
return versionRange_candidate;
|
||||
@ -76,11 +76,11 @@ export async function installExtensionModulesPeerDependencies(params: {
|
||||
return versionRange;
|
||||
})();
|
||||
|
||||
extensionModulesPerDependencies[peerDependencyName] = versionRange;
|
||||
uiModulesPerDependencies[peerDependencyName] = versionRange;
|
||||
}
|
||||
}
|
||||
|
||||
return { extensionModulesPerDependencies };
|
||||
return { uiModulesPerDependencies };
|
||||
})();
|
||||
|
||||
const parsedPackageJson = await (async () => {
|
||||
@ -117,9 +117,7 @@ export async function installExtensionModulesPeerDependencies(params: {
|
||||
|
||||
const parsedPackageJson_before = JSON.parse(JSON.stringify(parsedPackageJson));
|
||||
|
||||
for (const [moduleName, versionRange] of Object.entries(
|
||||
extensionModulesPerDependencies
|
||||
)) {
|
||||
for (const [moduleName, versionRange] of Object.entries(uiModulesPerDependencies)) {
|
||||
if (moduleName.startsWith("@types/")) {
|
||||
(parsedPackageJson.devDependencies ??= {})[moduleName] = versionRange;
|
||||
continue;
|
||||
@ -151,7 +149,7 @@ export async function installExtensionModulesPeerDependencies(params: {
|
||||
|
||||
await fsPr.writeFile(buildContext.packageJsonFilePath, packageJsonContentStr);
|
||||
|
||||
await npmInstall({
|
||||
npmInstall({
|
||||
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||
});
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
} from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import type { ExtensionModuleMeta } from "./extensionModuleMeta";
|
||||
import type { UiModuleMeta } from "./uiModuleMeta";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||
|
||||
@ -17,17 +17,17 @@ export type BuildContextLike = {
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
const DELIMITER_START = `# === Owned files start ===`;
|
||||
const DELIMITER_END = `# === Owned files end =====`;
|
||||
const DELIMITER_START = `# === Ejected files start ===`;
|
||||
const DELIMITER_END = `# === Ejected files end =====`;
|
||||
|
||||
export async function writeManagedGitignoreFile(params: {
|
||||
buildContext: BuildContextLike;
|
||||
extensionModuleMetas: ExtensionModuleMeta[];
|
||||
ownedFilesRelativePaths: string[];
|
||||
uiModuleMetas: UiModuleMeta[];
|
||||
ejectedFilesRelativePaths: string[];
|
||||
}): Promise<void> {
|
||||
const { buildContext, extensionModuleMetas, ownedFilesRelativePaths } = params;
|
||||
const { buildContext, uiModuleMetas, ejectedFilesRelativePaths } = params;
|
||||
|
||||
if (extensionModuleMetas.length === 0) {
|
||||
if (uiModuleMetas.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -38,19 +38,19 @@ export async function writeManagedGitignoreFile(params: {
|
||||
`# This file is managed by Keycloakify, do not edit it manually.`,
|
||||
``,
|
||||
DELIMITER_START,
|
||||
...ownedFilesRelativePaths
|
||||
...ejectedFilesRelativePaths
|
||||
.map(fileRelativePath => fileRelativePath.split(pathSep).join("/"))
|
||||
.map(line => `# ${line}`),
|
||||
DELIMITER_END,
|
||||
``,
|
||||
...extensionModuleMetas
|
||||
.map(extensionModuleMeta => [
|
||||
`# === ${extensionModuleMeta.moduleName} v${extensionModuleMeta.version} ===`,
|
||||
...extensionModuleMeta.files
|
||||
...uiModuleMetas
|
||||
.map(uiModuleMeta => [
|
||||
`# === ${uiModuleMeta.moduleName} v${uiModuleMeta.version} ===`,
|
||||
...uiModuleMeta.files
|
||||
.map(({ fileRelativePath }) => fileRelativePath)
|
||||
.filter(
|
||||
fileRelativePath =>
|
||||
!ownedFilesRelativePaths.includes(fileRelativePath)
|
||||
!ejectedFilesRelativePaths.includes(fileRelativePath)
|
||||
)
|
||||
.map(
|
||||
fileRelativePath =>
|
||||
@ -92,14 +92,14 @@ export async function writeManagedGitignoreFile(params: {
|
||||
export async function readManagedGitignoreFile(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{
|
||||
ownedFilesRelativePaths: string[];
|
||||
ejectedFilesRelativePaths: string[];
|
||||
}> {
|
||||
const { buildContext } = params;
|
||||
|
||||
const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
|
||||
|
||||
if (!(await existsAsync(filePath))) {
|
||||
return { ownedFilesRelativePaths: [] };
|
||||
return { ejectedFilesRelativePaths: [] };
|
||||
}
|
||||
|
||||
const contentStr = (await fsPr.readFile(filePath)).toString("utf8");
|
||||
@ -116,10 +116,10 @@ export async function readManagedGitignoreFile(params: {
|
||||
})();
|
||||
|
||||
if (payload === undefined) {
|
||||
return { ownedFilesRelativePaths: [] };
|
||||
return { ejectedFilesRelativePaths: [] };
|
||||
}
|
||||
|
||||
const ownedFilesRelativePaths = payload
|
||||
const ejectedFilesRelativePaths = payload
|
||||
.split("\n")
|
||||
.map(line => line.trim())
|
||||
.map(line => line.replace(/^# /, ""))
|
||||
@ -132,5 +132,5 @@ export async function readManagedGitignoreFile(params: {
|
||||
)
|
||||
.map(filePath => pathRelative(buildContext.themeSrcDirPath, filePath));
|
||||
|
||||
return { ownedFilesRelativePaths };
|
||||
return { ejectedFilesRelativePaths };
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import { getExtensionModuleMetas, computeHash } from "./extensionModuleMeta";
|
||||
import { installExtensionModulesPeerDependencies } from "./installExtensionModulesPeerDependencies";
|
||||
import { getUiModuleMetas, computeHash } from "./uiModuleMeta";
|
||||
import { installUiModulesPeerDependencies } from "./installUiModulesPeerDependencies";
|
||||
import {
|
||||
readManagedGitignoreFile,
|
||||
writeManagedGitignoreFile
|
||||
@ -9,36 +9,36 @@ import { dirname as pathDirname } from "path";
|
||||
import { join as pathJoin } from "path";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
import * as fsPr from "fs/promises";
|
||||
import { getIsKnownByGit } from "../tools/isKnownByGit";
|
||||
import { getIsTrackedByGit } from "../tools/isTrackedByGit";
|
||||
import { untrackFromGit } from "../tools/untrackFromGit";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const extensionModuleMetas = await getExtensionModuleMetas({ buildContext });
|
||||
const uiModuleMetas = await getUiModuleMetas({ buildContext });
|
||||
|
||||
await installExtensionModulesPeerDependencies({
|
||||
await installUiModulesPeerDependencies({
|
||||
buildContext,
|
||||
extensionModuleMetas
|
||||
uiModuleMetas
|
||||
});
|
||||
|
||||
const { ownedFilesRelativePaths } = await readManagedGitignoreFile({
|
||||
const { ejectedFilesRelativePaths } = await readManagedGitignoreFile({
|
||||
buildContext
|
||||
});
|
||||
|
||||
await writeManagedGitignoreFile({
|
||||
buildContext,
|
||||
ownedFilesRelativePaths,
|
||||
extensionModuleMetas
|
||||
ejectedFilesRelativePaths,
|
||||
uiModuleMetas
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
extensionModuleMetas
|
||||
.map(extensionModuleMeta =>
|
||||
uiModuleMetas
|
||||
.map(uiModuleMeta =>
|
||||
Promise.all(
|
||||
extensionModuleMeta.files.map(
|
||||
uiModuleMeta.files.map(
|
||||
async ({ fileRelativePath, copyableFilePath, hash }) => {
|
||||
if (ownedFilesRelativePaths.includes(fileRelativePath)) {
|
||||
if (ejectedFilesRelativePaths.includes(fileRelativePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -65,7 +65,19 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await getIsKnownByGit({ filePath: destFilePath })) {
|
||||
git_untrack: {
|
||||
if (!doesFileExist) {
|
||||
break git_untrack;
|
||||
}
|
||||
|
||||
const isTracked = await getIsTrackedByGit({
|
||||
filePath: destFilePath
|
||||
});
|
||||
|
||||
if (!isTracked) {
|
||||
break git_untrack;
|
||||
}
|
||||
|
||||
await untrackFromGit({
|
||||
filePath: destFilePath
|
||||
});
|
@ -10,15 +10,14 @@ import { crawlAsync } from "../tools/crawlAsync";
|
||||
import { getIsPrettierAvailable, getPrettier } from "../tools/runPrettier";
|
||||
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||
import {
|
||||
getExtensionModuleFileSourceCodeReadyToBeCopied,
|
||||
type BuildContextLike as BuildContextLike_getExtensionModuleFileSourceCodeReadyToBeCopied
|
||||
} from "./getExtensionModuleFileSourceCodeReadyToBeCopied";
|
||||
getUiModuleFileSourceCodeReadyToBeCopied,
|
||||
type BuildContextLike as BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied
|
||||
} from "./getUiModuleFileSourceCodeReadyToBeCopied";
|
||||
import * as crypto from "crypto";
|
||||
import { KEYCLOAK_THEME } from "../shared/constants";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
import { isAmong } from "tsafe/isAmong";
|
||||
|
||||
export type ExtensionModuleMeta = {
|
||||
export type UiModuleMeta = {
|
||||
moduleName: string;
|
||||
version: string;
|
||||
files: {
|
||||
@ -29,8 +28,8 @@ export type ExtensionModuleMeta = {
|
||||
peerDependencies: Record<string, string>;
|
||||
};
|
||||
|
||||
const zExtensionModuleMeta = (() => {
|
||||
type ExpectedType = ExtensionModuleMeta;
|
||||
const zUiModuleMeta = (() => {
|
||||
type ExpectedType = UiModuleMeta;
|
||||
|
||||
const zTargetType = z.object({
|
||||
moduleName: z.string(),
|
||||
@ -56,7 +55,7 @@ type ParsedCacheFile = {
|
||||
keycloakifyVersion: string;
|
||||
prettierConfigHash: string | null;
|
||||
thisFilePath: string;
|
||||
extensionModuleMetas: ExtensionModuleMeta[];
|
||||
uiModuleMetas: UiModuleMeta[];
|
||||
};
|
||||
|
||||
const zParsedCacheFile = (() => {
|
||||
@ -66,7 +65,7 @@ const zParsedCacheFile = (() => {
|
||||
keycloakifyVersion: z.string(),
|
||||
prettierConfigHash: z.union([z.string(), z.null()]),
|
||||
thisFilePath: z.string(),
|
||||
extensionModuleMetas: z.array(zExtensionModuleMeta)
|
||||
uiModuleMetas: z.array(zUiModuleMeta)
|
||||
});
|
||||
|
||||
type InferredType = z.infer<typeof zTargetType>;
|
||||
@ -76,10 +75,10 @@ const zParsedCacheFile = (() => {
|
||||
return id<z.ZodType<ExpectedType>>(zTargetType);
|
||||
})();
|
||||
|
||||
const CACHE_FILE_RELATIVE_PATH = pathJoin("extension-modules", "cache.json");
|
||||
const CACHE_FILE_RELATIVE_PATH = pathJoin("ui-modules", "cache.json");
|
||||
|
||||
export type BuildContextLike =
|
||||
BuildContextLike_getExtensionModuleFileSourceCodeReadyToBeCopied & {
|
||||
BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied & {
|
||||
cacheDirPath: string;
|
||||
packageJsonFilePath: string;
|
||||
projectDirPath: string;
|
||||
@ -87,9 +86,9 @@ export type BuildContextLike =
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function getExtensionModuleMetas(params: {
|
||||
export async function getUiModuleMetas(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<ExtensionModuleMeta[]> {
|
||||
}): Promise<UiModuleMeta[]> {
|
||||
const { buildContext } = params;
|
||||
|
||||
const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH);
|
||||
@ -106,9 +105,10 @@ export async function getExtensionModuleMetas(params: {
|
||||
return configHash;
|
||||
})();
|
||||
|
||||
const installedExtensionModules = await (async () => {
|
||||
const installedUiModules = await (async () => {
|
||||
const installedModulesWithKeycloakifyInTheName = await listInstalledModules({
|
||||
packageJsonFilePath: buildContext.packageJsonFilePath,
|
||||
projectDirPath: buildContext.packageJsonFilePath,
|
||||
filter: ({ moduleName }) =>
|
||||
moduleName.includes("keycloakify") && moduleName !== "keycloakify"
|
||||
});
|
||||
@ -133,7 +133,7 @@ export async function getExtensionModuleMetas(params: {
|
||||
return await fsPr.readFile(cacheFilePath);
|
||||
})();
|
||||
|
||||
const extensionModuleMetas_cacheUpToDate: ExtensionModuleMeta[] = await (async () => {
|
||||
const uiModuleMetas_cacheUpToDate: UiModuleMeta[] = await (async () => {
|
||||
const parsedCacheFile: ParsedCacheFile | undefined = await (async () => {
|
||||
if (cacheContent === undefined) {
|
||||
return undefined;
|
||||
@ -176,51 +176,45 @@ export async function getExtensionModuleMetas(params: {
|
||||
return [];
|
||||
}
|
||||
|
||||
const extensionModuleMetas_cacheUpToDate =
|
||||
parsedCacheFile.extensionModuleMetas.filter(extensionModuleMeta => {
|
||||
const correspondingInstalledExtensionModule =
|
||||
installedExtensionModules.find(
|
||||
installedExtensionModule =>
|
||||
installedExtensionModule.moduleName ===
|
||||
extensionModuleMeta.moduleName
|
||||
);
|
||||
const uiModuleMetas_cacheUpToDate = parsedCacheFile.uiModuleMetas.filter(
|
||||
uiModuleMeta => {
|
||||
const correspondingInstalledUiModule = installedUiModules.find(
|
||||
installedUiModule =>
|
||||
installedUiModule.moduleName === uiModuleMeta.moduleName
|
||||
);
|
||||
|
||||
if (correspondingInstalledExtensionModule === undefined) {
|
||||
if (correspondingInstalledUiModule === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
correspondingInstalledExtensionModule.version ===
|
||||
extensionModuleMeta.version
|
||||
);
|
||||
});
|
||||
return correspondingInstalledUiModule.version === uiModuleMeta.version;
|
||||
}
|
||||
);
|
||||
|
||||
return extensionModuleMetas_cacheUpToDate;
|
||||
return uiModuleMetas_cacheUpToDate;
|
||||
})();
|
||||
|
||||
const extensionModuleMetas = await Promise.all(
|
||||
installedExtensionModules.map(
|
||||
const uiModuleMetas = await Promise.all(
|
||||
installedUiModules.map(
|
||||
async ({
|
||||
moduleName,
|
||||
version,
|
||||
peerDependencies,
|
||||
dirPath
|
||||
}): Promise<ExtensionModuleMeta> => {
|
||||
}): Promise<UiModuleMeta> => {
|
||||
use_cache: {
|
||||
const extensionModuleMeta_cache =
|
||||
extensionModuleMetas_cacheUpToDate.find(
|
||||
extensionModuleMeta =>
|
||||
extensionModuleMeta.moduleName === moduleName
|
||||
);
|
||||
const uiModuleMeta_cache = uiModuleMetas_cacheUpToDate.find(
|
||||
uiModuleMeta => uiModuleMeta.moduleName === moduleName
|
||||
);
|
||||
|
||||
if (extensionModuleMeta_cache === undefined) {
|
||||
if (uiModuleMeta_cache === undefined) {
|
||||
break use_cache;
|
||||
}
|
||||
|
||||
return extensionModuleMeta_cache;
|
||||
return uiModuleMeta_cache;
|
||||
}
|
||||
|
||||
const files: ExtensionModuleMeta["files"] = [];
|
||||
const files: UiModuleMeta["files"] = [];
|
||||
|
||||
{
|
||||
const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME);
|
||||
@ -230,13 +224,13 @@ export async function getExtensionModuleMetas(params: {
|
||||
returnedPathsType: "relative to dirPath",
|
||||
onFileFound: async fileRelativePath => {
|
||||
const sourceCode =
|
||||
await getExtensionModuleFileSourceCodeReadyToBeCopied({
|
||||
await getUiModuleFileSourceCodeReadyToBeCopied({
|
||||
buildContext,
|
||||
fileRelativePath,
|
||||
isOwnershipAction: false,
|
||||
extensionModuleDirPath: dirPath,
|
||||
extensionModuleName: moduleName,
|
||||
extensionModuleVersion: version
|
||||
isForEjection: false,
|
||||
uiModuleDirPath: dirPath,
|
||||
uiModuleName: moduleName,
|
||||
uiModuleVersion: version
|
||||
});
|
||||
|
||||
const hash = computeHash(sourceCode);
|
||||
@ -266,16 +260,11 @@ export async function getExtensionModuleMetas(params: {
|
||||
});
|
||||
}
|
||||
|
||||
return id<ExtensionModuleMeta>({
|
||||
return id<UiModuleMeta>({
|
||||
moduleName,
|
||||
version,
|
||||
files,
|
||||
peerDependencies: Object.fromEntries(
|
||||
Object.entries(peerDependencies).filter(
|
||||
([moduleName]) =>
|
||||
!isAmong(["react", "@types/react"], moduleName)
|
||||
)
|
||||
)
|
||||
peerDependencies
|
||||
});
|
||||
}
|
||||
)
|
||||
@ -286,7 +275,7 @@ export async function getExtensionModuleMetas(params: {
|
||||
keycloakifyVersion,
|
||||
prettierConfigHash,
|
||||
thisFilePath: cacheFilePath,
|
||||
extensionModuleMetas
|
||||
uiModuleMetas
|
||||
});
|
||||
|
||||
const cacheContent_new = Buffer.from(
|
||||
@ -311,7 +300,7 @@ export async function getExtensionModuleMetas(params: {
|
||||
await fsPr.writeFile(cacheFilePath, cacheContent_new);
|
||||
}
|
||||
|
||||
return extensionModuleMetas;
|
||||
return uiModuleMetas;
|
||||
}
|
||||
|
||||
export function computeHash(data: Buffer) {
|
@ -1,70 +0,0 @@
|
||||
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,16 +45,12 @@ export type BuildContext = {
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
themeSrcDirPath: string;
|
||||
implementedThemeTypes: {
|
||||
login:
|
||||
| { isImplemented: true }
|
||||
| { isImplemented: false; isImplemented_native: boolean };
|
||||
email: { isImplemented: false; isImplemented_native: boolean };
|
||||
login: { isImplemented: boolean };
|
||||
email: { isImplemented: boolean };
|
||||
account:
|
||||
| { isImplemented: false; isImplemented_native: boolean }
|
||||
| { isImplemented: false }
|
||||
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
|
||||
admin:
|
||||
| { isImplemented: true }
|
||||
| { isImplemented: false; isImplemented_native: boolean };
|
||||
admin: { isImplemented: boolean };
|
||||
};
|
||||
packageJsonFilePath: string;
|
||||
bundler: "vite" | "webpack";
|
||||
@ -438,68 +434,27 @@ export function getBuildContext(params: {
|
||||
assert<Equals<typeof bundler, never>>(false);
|
||||
})();
|
||||
|
||||
const implementedThemeTypes: BuildContext["implementedThemeTypes"] = (() => {
|
||||
const getIsNative = (dirPath: string) =>
|
||||
fs.existsSync(pathJoin(dirPath, "theme.properties"));
|
||||
const implementedThemeTypes: BuildContext["implementedThemeTypes"] = {
|
||||
login: {
|
||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "login"))
|
||||
},
|
||||
email: {
|
||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "email"))
|
||||
},
|
||||
account: (() => {
|
||||
if (buildOptions.accountThemeImplementation === "none") {
|
||||
return { isImplemented: false };
|
||||
}
|
||||
|
||||
return {
|
||||
login: (() => {
|
||||
const dirPath = pathJoin(themeSrcDirPath, "login");
|
||||
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
return { isImplemented: false, isImplemented_native: false };
|
||||
}
|
||||
|
||||
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 };
|
||||
})()
|
||||
};
|
||||
})();
|
||||
return {
|
||||
isImplemented: true,
|
||||
type: buildOptions.accountThemeImplementation
|
||||
};
|
||||
})(),
|
||||
admin: {
|
||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "admin"))
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
implementedThemeTypes.account.isImplemented &&
|
||||
|
@ -12,7 +12,6 @@ export type CommandName =
|
||||
| "add-story"
|
||||
| "initialize-account-theme"
|
||||
| "initialize-admin-theme"
|
||||
| "initialize-admin-theme"
|
||||
| "initialize-email-theme"
|
||||
| "copy-keycloak-resources-to-public";
|
||||
|
||||
|
@ -13,15 +13,13 @@ import * as fs from "fs";
|
||||
|
||||
assert<Equals<ApiVersion, "v1">>();
|
||||
|
||||
export async function maybeDelegateCommandToCustomHandler(params: {
|
||||
export function maybeDelegateCommandToCustomHandler(params: {
|
||||
commandName: CommandName;
|
||||
buildContext: BuildContext;
|
||||
}): Promise<{ hasBeenHandled: boolean }> {
|
||||
}): { hasBeenHandled: boolean } {
|
||||
const { commandName, buildContext } = params;
|
||||
|
||||
const nodeModulesBinDirPath = await getNodeModulesBinDirPath({
|
||||
packageJsonFilePath: buildContext.packageJsonFilePath
|
||||
});
|
||||
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
|
||||
|
||||
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
|
||||
return { hasBeenHandled: false };
|
||||
|
201
src/bin/shared/getLatestsSemVersionedTag.ts
Normal file
201
src/bin/shared/getLatestsSemVersionedTag.ts
Normal file
@ -0,0 +1,201 @@
|
||||
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;
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
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)
|
||||
});
|
||||
}
|
40
src/bin/shared/metaInfKeycloakThemes.ts
Normal file
40
src/bin/shared/metaInfKeycloakThemes.ts
Normal file
@ -0,0 +1,40 @@
|
||||
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")
|
||||
);
|
||||
}
|
72
src/bin/shared/promptKeycloakVersion.ts
Normal file
72
src/bin/shared/promptKeycloakVersion.ts
Normal file
@ -0,0 +1,72 @@
|
||||
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,7 +10,6 @@ import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import * as fs from "fs/promises";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||
import type { ReturnType } from "tsafe";
|
||||
|
||||
export type BuildContextLike = {
|
||||
fetchOptions: BuildContext["fetchOptions"];
|
||||
@ -21,10 +20,7 @@ assert<BuildContext extends BuildContextLike ? true : false>;
|
||||
|
||||
export async function getSupportedDockerImageTags(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{
|
||||
allSupportedTags: string[];
|
||||
latestMajorTags: string[];
|
||||
}> {
|
||||
}) {
|
||||
const { buildContext } = params;
|
||||
|
||||
{
|
||||
@ -35,14 +31,14 @@ export async function getSupportedDockerImageTags(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const tags_queryResponse: string[] = [];
|
||||
const tags: string[] = [];
|
||||
|
||||
await (async function callee(url: string) {
|
||||
const r = await fetch(url, buildContext.fetchOptions);
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
tags_queryResponse.push(
|
||||
tags.push(
|
||||
...z
|
||||
.object({
|
||||
tags: z.array(z.string())
|
||||
@ -74,9 +70,7 @@ export async function getSupportedDockerImageTags(params: {
|
||||
]);
|
||||
})("https://quay.io/v2/keycloak/keycloak/tags/list");
|
||||
|
||||
const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions();
|
||||
|
||||
const allSupportedTags_withVersion = tags_queryResponse
|
||||
const arr = tags
|
||||
.map(tag => ({
|
||||
tag,
|
||||
version: (() => {
|
||||
@ -92,35 +86,28 @@ export async function getSupportedDockerImageTags(params: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (tag.split(".").length !== 3) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!supportedKeycloakMajorVersions.includes(version.major)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return version;
|
||||
})()
|
||||
}))
|
||||
.map(({ tag, version }) => (version === undefined ? undefined : { tag, version }))
|
||||
.filter(exclude(undefined))
|
||||
.sort(({ version: a }, { version: b }) => SemVer.compare(b, a));
|
||||
.filter(exclude(undefined));
|
||||
|
||||
const latestTagByMajor: Record<number, SemVer | undefined> = {};
|
||||
const versionByMajor: Record<number, SemVer | undefined> = {};
|
||||
|
||||
for (const { version } of allSupportedTags_withVersion) {
|
||||
const version_current = latestTagByMajor[version.major];
|
||||
for (const { version } of arr) {
|
||||
const version_current = versionByMajor[version.major];
|
||||
|
||||
if (
|
||||
version_current === undefined ||
|
||||
SemVer.compare(version_current, version) === -1
|
||||
) {
|
||||
latestTagByMajor[version.major] = version;
|
||||
versionByMajor[version.major] = version;
|
||||
}
|
||||
}
|
||||
|
||||
const latestMajorTags = Object.entries(latestTagByMajor)
|
||||
const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions();
|
||||
|
||||
const result = Object.entries(versionByMajor)
|
||||
.sort(([a], [b]) => parseInt(b) - parseInt(a))
|
||||
.map(([, version]) => version)
|
||||
.map(version => {
|
||||
@ -134,40 +121,16 @@ export async function getSupportedDockerImageTags(params: {
|
||||
})
|
||||
.filter(exclude(undefined));
|
||||
|
||||
const allSupportedTags = allSupportedTags_withVersion.map(({ tag }) => tag);
|
||||
|
||||
const result = {
|
||||
latestMajorTags,
|
||||
allSupportedTags
|
||||
};
|
||||
|
||||
await setCachedValue({ cacheDirPath: buildContext.cacheDirPath, result });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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 = {
|
||||
keycloakifyVersion: string;
|
||||
time: number;
|
||||
result: Result;
|
||||
result: string[];
|
||||
};
|
||||
|
||||
const zCache = (() => {
|
||||
@ -176,7 +139,7 @@ const { getCachedValue, setCachedValue } = (() => {
|
||||
const zTargetType = z.object({
|
||||
keycloakifyVersion: z.string(),
|
||||
time: z.number(),
|
||||
result: zResult
|
||||
result: z.array(z.string())
|
||||
});
|
||||
|
||||
type InferredType = z.infer<typeof zTargetType>;
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import { id } from "tsafe/id";
|
||||
import * as fs from "fs";
|
||||
|
||||
export type ParsedRealmJson = {
|
||||
realm: string;
|
||||
@ -48,7 +50,7 @@ export type ParsedRealmJson = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export const zParsedRealmJson = (() => {
|
||||
const zParsedRealmJson = (() => {
|
||||
type TargetType = ParsedRealmJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
@ -116,3 +118,19 @@ export const zParsedRealmJson = (() => {
|
||||
|
||||
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;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export type { ParsedRealmJson } from "./ParsedRealmJson";
|
||||
export { readRealmJsonFile } from "./readRealmJsonFile";
|
||||
export { writeRealmJsonFile } from "./writeRealmJsonFile";
|
@ -1,20 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
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,10 +3,11 @@ import { getThisCodebaseRootDirPath } from "../../../tools/getThisCodebaseRootDi
|
||||
import * as fs from "fs";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { readRealmJsonFile } from "../ParsedRealmJson/readRealmJsonFile";
|
||||
import type { ParsedRealmJson } from "../ParsedRealmJson/ParsedRealmJson";
|
||||
import { type ParsedRealmJson, readRealmJsonFile } from "../ParsedRealmJson";
|
||||
|
||||
function getDefaultRealmJsonFilePath(params: { keycloakMajorVersionNumber: number }) {
|
||||
export function getDefaultRealmJsonFilePath(params: {
|
||||
keycloakMajorVersionNumber: number;
|
||||
}) {
|
||||
const { keycloakMajorVersionNumber } = params;
|
||||
|
||||
return pathJoin(
|
||||
|
@ -756,24 +756,6 @@
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"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",
|
||||
"name": "locale",
|
||||
@ -1354,13 +1336,13 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"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",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@ -1411,13 +1393,13 @@
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-address-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"
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-attribute-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1535,7 +1517,7 @@
|
||||
"defaultLocale": "en",
|
||||
"authenticationFlows": [
|
||||
{
|
||||
"id": "f664efe4-102d-4ec1-bf11-11af67e3f178",
|
||||
"id": "223ce532-2038-4f24-a606-2a5c73f7bd65",
|
||||
"alias": "Account verification options",
|
||||
"description": "Method with which to verity the existing account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1561,7 +1543,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "8a5630c5-eca1-4b6a-8e59-459cb6c84535",
|
||||
"id": "57e47732-79cc-4d60-bee7-4f0b8fd44540",
|
||||
"alias": "Authentication Options",
|
||||
"description": "Authentication options.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1595,7 +1577,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "c1a3eed3-25ce-44ae-93d1-f0b8148a0f8c",
|
||||
"id": "c2735d89-60c0-45a4-9b3c-ae5df17df395",
|
||||
"alias": "Browser - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1621,7 +1603,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "6eb188ad-1041-44dd-bf8f-37cae0d98bf1",
|
||||
"id": "11a5a507-2b9a-443f-961b-dffd66f4318d",
|
||||
"alias": "Direct Grant - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1647,7 +1629,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4ee215ac-f4e5-4edb-bf76-65dc9e211543",
|
||||
"id": "963bd753-6ea7-4d93-ab56-30f9ab59d597",
|
||||
"alias": "First broker login - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1673,7 +1655,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "5a1eac7e-06a0-46d8-b9ae-1f2c934331f9",
|
||||
"id": "1db6a489-a3b4-44c4-b480-1d1e8c123d20",
|
||||
"alias": "Handle Existing Account",
|
||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||
"providerId": "basic-flow",
|
||||
@ -1699,7 +1681,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ed165166-4521-4a62-b185-c4b51643cbb1",
|
||||
"id": "7a38f32d-4f34-450f-8f03-64802d7cb8f1",
|
||||
"alias": "Reset - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1725,7 +1707,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4788fb1f-fd81-4f5d-9abe-4199dd641c1e",
|
||||
"id": "0df88739-3739-4d70-8893-47c546f19003",
|
||||
"alias": "User creation or linking",
|
||||
"description": "Flow for the existing/non-existing user alternatives",
|
||||
"providerId": "basic-flow",
|
||||
@ -1752,7 +1734,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d778a70f-f472-4dd3-ac40-cb5612ddc171",
|
||||
"id": "35025424-e291-4c54-8a29-70aadba549ce",
|
||||
"alias": "Verify Existing Account by Re-authentication",
|
||||
"description": "Reauthentication of existing account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1778,7 +1760,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "9c1ea8ea-7c23-4e60-b02d-1900d9dc4109",
|
||||
"id": "1813b7f2-c3c2-4b92-8ffc-9ff2d12186c6",
|
||||
"alias": "browser",
|
||||
"description": "browser based authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1820,7 +1802,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "0ebdf418-d57d-4318-9359-7bd0cb2381f2",
|
||||
"id": "954283ac-f1c2-40b6-a39f-bf23ff9f3ce8",
|
||||
"alias": "clients",
|
||||
"description": "Base authentication for clients",
|
||||
"providerId": "client-flow",
|
||||
@ -1862,7 +1844,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "5cc89293-c72e-4c5e-b31c-15558588a60d",
|
||||
"id": "52a789ce-2cad-4f0f-93b2-295b7fd519f0",
|
||||
"alias": "direct grant",
|
||||
"description": "OpenID Connect Resource Owner Grant",
|
||||
"providerId": "basic-flow",
|
||||
@ -1896,7 +1878,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "5ae5a321-ccac-449e-9c19-d6dc22ab8085",
|
||||
"id": "5a6a71e1-9105-45b6-b5f0-52538461357b",
|
||||
"alias": "docker auth",
|
||||
"description": "Used by Docker clients to authenticate against the IDP",
|
||||
"providerId": "basic-flow",
|
||||
@ -1914,7 +1896,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "7737fdd1-0875-47e6-977b-12561cddfdc3",
|
||||
"id": "8392b6e7-bdbf-4d7f-97b6-885761c200db",
|
||||
"alias": "first broker login",
|
||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1941,7 +1923,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "90f975c3-9826-461f-88ca-27c697aff86b",
|
||||
"id": "52136d70-8d08-42ea-b04b-cf40ea2807aa",
|
||||
"alias": "forms",
|
||||
"description": "Username, password, otp and other auth forms.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1967,7 +1949,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ce2722d5-9f4f-41a2-8f81-e01f7b6cee57",
|
||||
"id": "26bbc7e6-ef01-4cdb-9dba-520e2f3f8993",
|
||||
"alias": "http challenge",
|
||||
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||
"providerId": "basic-flow",
|
||||
@ -1993,7 +1975,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "31b5bfa7-98ad-47a2-b8e6-0669022cd8cb",
|
||||
"id": "f0887979-04eb-4033-8f19-0ffd8c8b7f6a",
|
||||
"alias": "registration",
|
||||
"description": "registration flow",
|
||||
"providerId": "basic-flow",
|
||||
@ -2012,7 +1994,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bf8a950b-be3b-4e44-8602-64e0bba492eb",
|
||||
"id": "a3b7b94b-bfbf-4760-a8c9-7d9cd98d262e",
|
||||
"alias": "registration form",
|
||||
"description": "registration form",
|
||||
"providerId": "form-flow",
|
||||
@ -2054,7 +2036,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "e3519800-971b-4b1d-b64e-3983ccd02dea",
|
||||
"id": "dc68a665-2e51-4a22-aaad-bd693ddc77cc",
|
||||
"alias": "reset credentials",
|
||||
"description": "Reset credentials for a user if they forgot their password or something",
|
||||
"providerId": "basic-flow",
|
||||
@ -2096,7 +2078,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "9d5a33a2-e777-4beb-95de-b84812f69c56",
|
||||
"id": "ae6b73aa-1318-4ae8-a3d9-d01b5e7d957e",
|
||||
"alias": "saml ecp",
|
||||
"description": "SAML ECP Profile Authentication Flow",
|
||||
"providerId": "basic-flow",
|
||||
@ -2116,14 +2098,14 @@
|
||||
],
|
||||
"authenticatorConfig": [
|
||||
{
|
||||
"id": "4901c91d-59bd-4727-b585-8e4e44828d0a",
|
||||
"id": "0c18de7f-0714-41f4-9a3f-ed4edd53ae9c",
|
||||
"alias": "create unique user config",
|
||||
"config": {
|
||||
"require.password.update.after.registration": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5062a078-83a7-4933-b0d5-3f75cc2a5003",
|
||||
"id": "65b3c8bb-34a4-4d19-b578-245dc8ff53ea",
|
||||
"alias": "review profile config",
|
||||
"config": {
|
||||
"update.profile.on.first.login": "missing"
|
||||
|
@ -764,24 +764,6 @@
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"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",
|
||||
"name": "locale",
|
||||
@ -1362,14 +1344,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"saml-user-property-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
"oidc-sha256-pairwise-sub-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1418,14 +1400,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"saml-user-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-role-list-mapper"
|
||||
"saml-user-attribute-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1543,7 +1525,7 @@
|
||||
"defaultLocale": "en",
|
||||
"authenticationFlows": [
|
||||
{
|
||||
"id": "8ccfe057-5ce6-499b-9fae-3cd89b62bf01",
|
||||
"id": "1f4d4e13-1591-4751-8985-17886a8c98a9",
|
||||
"alias": "Account verification options",
|
||||
"description": "Method with which to verity the existing account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1569,7 +1551,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "f3b9ab2e-41c2-4e73-876b-e2c275d6d14e",
|
||||
"id": "126f07c3-1bcb-4a02-bf16-bb44674bf55d",
|
||||
"alias": "Authentication Options",
|
||||
"description": "Authentication options.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1603,7 +1585,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "df1329cc-777c-42d8-aa2f-c5d5ddaaf5a4",
|
||||
"id": "eb3a08c8-5f99-49b6-b02b-16b62571f273",
|
||||
"alias": "Browser - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1629,7 +1611,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "f78a4cbc-66ff-4caa-8066-67aff94946f4",
|
||||
"id": "3dc19838-5025-4bbb-b569-b574bd5a8d90",
|
||||
"alias": "Direct Grant - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1655,7 +1637,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4b20995b-5553-45db-86b0-05c3fe14edb1",
|
||||
"id": "70d6fd40-d740-4dae-b0e6-350f8e9d4a1c",
|
||||
"alias": "First broker login - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1681,7 +1663,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "0a7cc6b7-e427-4f72-b44e-a02133241bad",
|
||||
"id": "6e24dcb3-5818-483c-8e44-883858171901",
|
||||
"alias": "Handle Existing Account",
|
||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||
"providerId": "basic-flow",
|
||||
@ -1707,7 +1689,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "e24e73c0-dd51-4fdc-a916-284f11f38487",
|
||||
"id": "ac6254cd-403b-457b-b308-22a2a0e4f99d",
|
||||
"alias": "Reset - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1733,7 +1715,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "37ee5a12-01c2-41b0-aafa-e9c6661ff544",
|
||||
"id": "485e74e6-9b3e-4b2c-a9b9-927802dc4f06",
|
||||
"alias": "User creation or linking",
|
||||
"description": "Flow for the existing/non-existing user alternatives",
|
||||
"providerId": "basic-flow",
|
||||
@ -1760,7 +1742,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "8902a1a7-c2ee-4648-869f-dd5ef89184fc",
|
||||
"id": "ff9bb879-1d6a-4d1c-9836-1e4fab6f8997",
|
||||
"alias": "Verify Existing Account by Re-authentication",
|
||||
"description": "Reauthentication of existing account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1786,7 +1768,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "77c78eed-4bcd-4779-b39f-10135be84946",
|
||||
"id": "af8b2470-d581-401c-9984-762b966ebcc2",
|
||||
"alias": "browser",
|
||||
"description": "browser based authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1828,7 +1810,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "c6398883-01e6-47a1-bb97-c09f2983155d",
|
||||
"id": "414dbda4-eb3f-4baa-b23a-d3423af1eae6",
|
||||
"alias": "clients",
|
||||
"description": "Base authentication for clients",
|
||||
"providerId": "client-flow",
|
||||
@ -1870,7 +1852,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "78ab5fb8-f35b-4053-b264-94b208000b13",
|
||||
"id": "1cae0c4b-8dfb-4f5d-a781-e74d0a13c940",
|
||||
"alias": "direct grant",
|
||||
"description": "OpenID Connect Resource Owner Grant",
|
||||
"providerId": "basic-flow",
|
||||
@ -1904,7 +1886,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "959e154b-034e-413d-9b19-211e7d9ba33d",
|
||||
"id": "e798b655-7d85-4b6b-aee7-1448a3e1e0ea",
|
||||
"alias": "docker auth",
|
||||
"description": "Used by Docker clients to authenticate against the IDP",
|
||||
"providerId": "basic-flow",
|
||||
@ -1922,7 +1904,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "001e253d-bdbd-41e2-81c7-1c7b239feeb1",
|
||||
"id": "eb94b723-1041-426a-87bf-f7b4bd2f485d",
|
||||
"alias": "first broker login",
|
||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1949,7 +1931,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "45481bb0-18fe-4a26-a77c-35a5afe58436",
|
||||
"id": "452d1d5f-7632-44d7-bc89-77ff2b209b3e",
|
||||
"alias": "forms",
|
||||
"description": "Username, password, otp and other auth forms.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1975,7 +1957,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bb47b847-5a55-4c08-909e-9f6f8d8a0636",
|
||||
"id": "7c1b9e8f-6b57-49d1-a9a7-494862f93c0f",
|
||||
"alias": "http challenge",
|
||||
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||
"providerId": "basic-flow",
|
||||
@ -2001,7 +1983,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "77e6e169-05b7-4b89-af00-09cfe1604eed",
|
||||
"id": "2b38f34a-1739-499e-bb24-1dff96f32009",
|
||||
"alias": "registration",
|
||||
"description": "registration flow",
|
||||
"providerId": "basic-flow",
|
||||
@ -2020,7 +2002,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "aef03fe8-1a70-40c3-879f-25588f75c119",
|
||||
"id": "d26ae72b-a933-44dc-9927-1c82757004b2",
|
||||
"alias": "registration form",
|
||||
"description": "registration form",
|
||||
"providerId": "form-flow",
|
||||
@ -2062,7 +2044,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "990abff7-e2ba-4217-984e-8890cbc2b3a9",
|
||||
"id": "222ee8d6-1892-4768-9ada-720274b6bf9a",
|
||||
"alias": "reset credentials",
|
||||
"description": "Reset credentials for a user if they forgot their password or something",
|
||||
"providerId": "basic-flow",
|
||||
@ -2104,7 +2086,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d9894cf6-2f99-493e-ac47-853f54bfc9c6",
|
||||
"id": "e8b4d92c-27c1-4a9b-9b16-7ceb810fa230",
|
||||
"alias": "saml ecp",
|
||||
"description": "SAML ECP Profile Authentication Flow",
|
||||
"providerId": "basic-flow",
|
||||
@ -2124,14 +2106,14 @@
|
||||
],
|
||||
"authenticatorConfig": [
|
||||
{
|
||||
"id": "101ed8ff-4383-4539-aa52-2d1e69698b78",
|
||||
"id": "e5847a0b-855d-4d93-85fd-94714be3ed92",
|
||||
"alias": "create unique user config",
|
||||
"config": {
|
||||
"require.password.update.after.registration": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "049042a5-3551-4c16-81a1-64d86f5aa1e5",
|
||||
"id": "a2a18aa4-bd4c-4c2a-9286-e9d6c64f4812",
|
||||
"alias": "review profile config",
|
||||
"config": {
|
||||
"update.profile.on.first.login": "missing"
|
||||
|
@ -775,24 +775,6 @@
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"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",
|
||||
"name": "locale",
|
||||
@ -1373,14 +1355,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-property-mapper"
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"saml-user-attribute-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1429,14 +1411,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-property-mapper"
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1554,7 +1536,7 @@
|
||||
"defaultLocale": "en",
|
||||
"authenticationFlows": [
|
||||
{
|
||||
"id": "30a878f0-57aa-4d20-bab0-6cf1d7317a5c",
|
||||
"id": "c40791b4-4d59-4df2-bebd-2b71e793704f",
|
||||
"alias": "Account verification options",
|
||||
"description": "Method with which to verity the existing account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1580,7 +1562,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d386affe-d1fe-472a-bee6-54105d0101f5",
|
||||
"id": "8813b6d1-8b88-4672-b29b-8420ce3f3975",
|
||||
"alias": "Authentication Options",
|
||||
"description": "Authentication options.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1614,7 +1596,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "77b95bc0-bd0c-46b7-8240-3182023e9d50",
|
||||
"id": "a9937c40-a1ee-4c57-adf7-ede0a9983953",
|
||||
"alias": "Browser - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1640,7 +1622,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bc96d3d6-29a1-42af-a63e-bb67a8c6d78f",
|
||||
"id": "2d494b5a-eb73-40d0-94d3-a8d8024a7db4",
|
||||
"alias": "Direct Grant - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1666,7 +1648,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "7697ca74-5c2b-45ab-9335-e0f6dec59b5c",
|
||||
"id": "2e977f5a-8110-412b-b704-3e15164dbb1b",
|
||||
"alias": "First broker login - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1692,7 +1674,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "534cb120-f600-4f40-9707-7b781bdbce48",
|
||||
"id": "6f171b4b-8723-4e6d-bb1e-6b4293a7bb3f",
|
||||
"alias": "Handle Existing Account",
|
||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||
"providerId": "basic-flow",
|
||||
@ -1718,7 +1700,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "f884b048-b223-4ed6-ae16-e49a4255131e",
|
||||
"id": "2dbb7f27-757d-4178-8217-4a24fdb0163c",
|
||||
"alias": "Reset - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1744,7 +1726,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "61c7966c-ad72-49f5-84dd-376152348092",
|
||||
"id": "7295aaf7-acf4-4b78-8186-d2415ea4ede0",
|
||||
"alias": "User creation or linking",
|
||||
"description": "Flow for the existing/non-existing user alternatives",
|
||||
"providerId": "basic-flow",
|
||||
@ -1771,7 +1753,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "72412d0f-dd1b-49fe-bb0b-9dad99eb0491",
|
||||
"id": "e0d34d7c-7bbb-4847-8864-fbd97a1f3e89",
|
||||
"alias": "Verify Existing Account by Re-authentication",
|
||||
"description": "Reauthentication of existing account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1797,7 +1779,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "6b76613e-0d39-440d-aab4-98eaffb1e96a",
|
||||
"id": "5f3d0fb0-d95e-4841-89d3-a27d0cdbbcb4",
|
||||
"alias": "browser",
|
||||
"description": "browser based authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1839,7 +1821,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "0ff60395-fa89-41be-ad22-fab339e67c49",
|
||||
"id": "c246380d-af25-4151-ab19-1f1e5b553008",
|
||||
"alias": "clients",
|
||||
"description": "Base authentication for clients",
|
||||
"providerId": "client-flow",
|
||||
@ -1881,7 +1863,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bbb3ece7-7dbf-4aba-80c3-dde4b9cdd0b6",
|
||||
"id": "abacf398-0f1f-4f28-a310-8d306d588048",
|
||||
"alias": "direct grant",
|
||||
"description": "OpenID Connect Resource Owner Grant",
|
||||
"providerId": "basic-flow",
|
||||
@ -1915,7 +1897,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "f5f2c0f6-7dbf-4978-845e-6cacac23aa13",
|
||||
"id": "a0f87683-619a-44d4-8b4f-4b053bba2346",
|
||||
"alias": "docker auth",
|
||||
"description": "Used by Docker clients to authenticate against the IDP",
|
||||
"providerId": "basic-flow",
|
||||
@ -1933,7 +1915,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cf463104-19e2-41a8-8a53-d3dd30b75344",
|
||||
"id": "e8820c7c-22a7-4618-beb7-3e09be72c00c",
|
||||
"alias": "first broker login",
|
||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1960,7 +1942,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "b99b60dc-41ad-487d-be69-a2eefa954a9d",
|
||||
"id": "cac00c38-ee44-44c9-b95e-cc755bab36ef",
|
||||
"alias": "forms",
|
||||
"description": "Username, password, otp and other auth forms.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1986,7 +1968,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "18731296-2c96-4f98-a884-027e629e4f9d",
|
||||
"id": "688cde36-507e-4a68-afdf-18ec4ad626a7",
|
||||
"alias": "http challenge",
|
||||
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||
"providerId": "basic-flow",
|
||||
@ -2012,7 +1994,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "9a9dce17-5425-4fd5-b3b8-81410e1dbce4",
|
||||
"id": "e058697c-f450-4f14-ae64-04e9299fa24f",
|
||||
"alias": "registration",
|
||||
"description": "registration flow",
|
||||
"providerId": "basic-flow",
|
||||
@ -2031,7 +2013,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d0a24e08-cb69-4949-9518-50ae7a96ee49",
|
||||
"id": "ad768088-32c9-4979-90dd-61bf111fd72e",
|
||||
"alias": "registration form",
|
||||
"description": "registration form",
|
||||
"providerId": "form-flow",
|
||||
@ -2073,7 +2055,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "6a9aa554-afba-487f-9c82-e94c81c15b3b",
|
||||
"id": "47d4b090-f965-4588-b5bc-029ccb59876f",
|
||||
"alias": "reset credentials",
|
||||
"description": "Reset credentials for a user if they forgot their password or something",
|
||||
"providerId": "basic-flow",
|
||||
@ -2115,7 +2097,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "e0361d46-eab4-41a6-bb2e-1dc6a5a6b073",
|
||||
"id": "1f68feec-7f99-4c49-afe6-45d46684ca21",
|
||||
"alias": "saml ecp",
|
||||
"description": "SAML ECP Profile Authentication Flow",
|
||||
"providerId": "basic-flow",
|
||||
@ -2135,14 +2117,14 @@
|
||||
],
|
||||
"authenticatorConfig": [
|
||||
{
|
||||
"id": "053d6017-e54c-418a-abe7-44dd4752eacb",
|
||||
"id": "bd7365c7-842b-4bc6-a4ca-498cf025c210",
|
||||
"alias": "create unique user config",
|
||||
"config": {
|
||||
"require.password.update.after.registration": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8b545cf4-ab9e-4226-b3c0-d7ac773eae2f",
|
||||
"id": "b929192d-f650-4a09-9701-3d3216547552",
|
||||
"alias": "review profile config",
|
||||
"config": {
|
||||
"update.profile.on.first.login": "missing"
|
||||
|
@ -408,9 +408,9 @@
|
||||
"otpPolicyPeriod": 30,
|
||||
"otpPolicyCodeReusable": false,
|
||||
"otpSupportedApplications": [
|
||||
"totpAppGoogleName",
|
||||
"totpAppFreeOTPName",
|
||||
"totpAppMicrosoftAuthenticatorName",
|
||||
"totpAppGoogleName"
|
||||
"totpAppMicrosoftAuthenticatorName"
|
||||
],
|
||||
"webAuthnPolicyRpEntityName": "keycloak",
|
||||
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
||||
@ -779,24 +779,6 @@
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"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",
|
||||
"name": "locale",
|
||||
@ -1377,13 +1359,13 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-usermodel-attribute-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",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-address-mapper"
|
||||
]
|
||||
}
|
||||
@ -1433,14 +1415,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper"
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-attribute-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -789,24 +789,6 @@
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"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",
|
||||
"name": "locale",
|
||||
@ -1419,14 +1401,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"saml-role-list-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"saml-role-list-mapper"
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1495,14 +1477,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper"
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -919,24 +919,6 @@
|
||||
"claim.name": "locale",
|
||||
"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"],
|
||||
@ -1563,14 +1545,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-full-name-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper"
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-full-name-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1602,14 +1584,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper"
|
||||
"oidc-address-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -964,24 +964,6 @@
|
||||
"claim.name": "locale",
|
||||
"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": [
|
||||
@ -1636,14 +1618,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"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-address-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-full-name-mapper"
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1675,12 +1657,12 @@
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-address-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-full-name-mapper"
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -997,7 +997,7 @@
|
||||
"claim.value": "[\"*\"]",
|
||||
"userinfo.token.claim": "true",
|
||||
"id.token.claim": "false",
|
||||
"lightweight.claim": "true",
|
||||
"lightweight.claim": "false",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "allowed-origins",
|
||||
"jsonType.label": "JSON",
|
||||
@ -1628,7 +1628,7 @@
|
||||
"smtpServer": {},
|
||||
"loginTheme": "keycloakify-starter",
|
||||
"accountTheme": "",
|
||||
"adminTheme": "keycloakify-starter",
|
||||
"adminTheme": "",
|
||||
"emailTheme": "",
|
||||
"eventsEnabled": false,
|
||||
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||
@ -1657,13 +1657,13 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-full-name-mapper"
|
||||
]
|
||||
}
|
||||
@ -1694,14 +1694,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-user-attribute-mapper"
|
||||
"saml-role-list-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-usermodel-attribute-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -67,7 +67,7 @@ export async function dumpContainerConfig(params: {
|
||||
...["--db", "dev-file"],
|
||||
...[
|
||||
"--db-url",
|
||||
'"jdbc:h2:file:/tmp/h2/keycloakdb;NON_KEYWORDS=VALUE"'
|
||||
"'jdbc:h2:file:/tmp/h2/keycloakdb;NON_KEYWORDS=VALUE'"
|
||||
]
|
||||
])
|
||||
],
|
||||
|
@ -1,20 +1,28 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { ParsedRealmJson } from "./ParsedRealmJson";
|
||||
import { getDefaultConfig } from "./defaultConfig";
|
||||
import { TEST_APP_URL, type ThemeType, THEME_TYPES } from "../../shared/constants";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { objectKeys } from "tsafe/objectKeys";
|
||||
import { TEST_APP_URL } from "../../shared/constants";
|
||||
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: {
|
||||
parsedRealmJson: ParsedRealmJson;
|
||||
keycloakMajorVersionNumber: number;
|
||||
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||
buildContext: BuildContextLike;
|
||||
}): {
|
||||
realmName: string;
|
||||
clientName: string;
|
||||
username: string;
|
||||
} {
|
||||
const { parsedRealmJson, keycloakMajorVersionNumber, parsedKeycloakThemesJsonEntry } =
|
||||
params;
|
||||
const { parsedRealmJson, keycloakMajorVersionNumber, buildContext } = params;
|
||||
|
||||
const { username } = addOrEditTestUser({
|
||||
parsedRealmJson,
|
||||
@ -30,7 +38,8 @@ export function prepareRealmConfig(params: {
|
||||
|
||||
enableCustomThemes({
|
||||
parsedRealmJson,
|
||||
parsedKeycloakThemesJsonEntry
|
||||
themeName: buildContext.themeNames[0],
|
||||
implementedThemeTypes: buildContext.implementedThemeTypes
|
||||
});
|
||||
|
||||
enable_custom_events_listeners: {
|
||||
@ -54,15 +63,17 @@ export function prepareRealmConfig(params: {
|
||||
|
||||
function enableCustomThemes(params: {
|
||||
parsedRealmJson: ParsedRealmJson;
|
||||
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||
themeName: string;
|
||||
implementedThemeTypes: BuildContextLike["implementedThemeTypes"];
|
||||
}) {
|
||||
const { parsedRealmJson, parsedKeycloakThemesJsonEntry } = params;
|
||||
const { parsedRealmJson, themeName, implementedThemeTypes } = params;
|
||||
|
||||
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||
parsedRealmJson[`${themeType}Theme` as const] =
|
||||
!parsedKeycloakThemesJsonEntry.types.includes(themeType)
|
||||
? ""
|
||||
: parsedKeycloakThemesJsonEntry.name;
|
||||
for (const themeType of objectKeys(implementedThemeTypes)) {
|
||||
if (!implementedThemeTypes[themeType].isImplemented) {
|
||||
continue;
|
||||
}
|
||||
|
||||
parsedRealmJson[`${themeType}Theme` as const] = themeName;
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,6 +112,7 @@ function addOrEditTestUser(params: {
|
||||
);
|
||||
|
||||
newUser.username = defaultUser_default.username;
|
||||
newUser.email = defaultUser_default.email;
|
||||
|
||||
delete_existing_password_credential_if_any: {
|
||||
const i = newUser.credentials.findIndex(
|
||||
@ -321,7 +333,7 @@ function editAccountConsoleAndSecurityAdminConsole(params: {
|
||||
"claim.value": '["*"]',
|
||||
"userinfo.token.claim": "true",
|
||||
"id.token.claim": "false",
|
||||
"lightweight.claim": "true",
|
||||
"lightweight.claim": "false",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "allowed-origins",
|
||||
"jsonType.label": "JSON",
|
||||
|
@ -1,7 +1,11 @@
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { runPrettier, getIsPrettierAvailable } from "../../tools/runPrettier";
|
||||
import { getDefaultConfig } from "./defaultConfig";
|
||||
import { prepareRealmConfig } from "./prepareRealmConfig";
|
||||
import {
|
||||
prepareRealmConfig,
|
||||
type BuildContextLike as BuildContextLike_prepareRealmConfig
|
||||
} from "./prepareRealmConfig";
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
join as pathJoin,
|
||||
@ -10,30 +14,25 @@ import {
|
||||
sep as pathSep
|
||||
} from "path";
|
||||
import { existsAsync } from "../../tools/fs.existsAsync";
|
||||
import {
|
||||
readRealmJsonFile,
|
||||
writeRealmJsonFile,
|
||||
type ParsedRealmJson
|
||||
} from "./ParsedRealmJson";
|
||||
import { readRealmJsonFile, type ParsedRealmJson } from "./ParsedRealmJson";
|
||||
import {
|
||||
dumpContainerConfig,
|
||||
type BuildContextLike as BuildContextLike_dumpContainerConfig
|
||||
} from "./dumpContainerConfig";
|
||||
import * as runExclusive from "run-exclusive";
|
||||
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
|
||||
import type { ThemeType } from "../../shared/constants";
|
||||
import chalk from "chalk";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_dumpContainerConfig & {
|
||||
projectDirPath: string;
|
||||
};
|
||||
export type BuildContextLike = BuildContextLike_dumpContainerConfig &
|
||||
BuildContextLike_prepareRealmConfig & {
|
||||
projectDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>;
|
||||
|
||||
export async function getRealmConfig(params: {
|
||||
keycloakMajorVersionNumber: number;
|
||||
realmJsonFilePath_userProvided: string | undefined;
|
||||
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{
|
||||
realmJsonFilePath: string;
|
||||
@ -42,12 +41,8 @@ export async function getRealmConfig(params: {
|
||||
username: string;
|
||||
onRealmConfigChange: () => Promise<void>;
|
||||
}> {
|
||||
const {
|
||||
keycloakMajorVersionNumber,
|
||||
realmJsonFilePath_userProvided,
|
||||
parsedKeycloakThemesJsonEntry,
|
||||
buildContext
|
||||
} = params;
|
||||
const { keycloakMajorVersionNumber, realmJsonFilePath_userProvided, buildContext } =
|
||||
params;
|
||||
|
||||
const realmJsonFilePath = pathJoin(
|
||||
buildContext.projectDirPath,
|
||||
@ -73,8 +68,8 @@ export async function getRealmConfig(params: {
|
||||
|
||||
const { clientName, realmName, username } = prepareRealmConfig({
|
||||
parsedRealmJson,
|
||||
keycloakMajorVersionNumber,
|
||||
parsedKeycloakThemesJsonEntry
|
||||
buildContext,
|
||||
keycloakMajorVersionNumber
|
||||
});
|
||||
|
||||
{
|
||||
@ -85,11 +80,22 @@ export async function getRealmConfig(params: {
|
||||
}
|
||||
}
|
||||
|
||||
await writeRealmJsonFile({
|
||||
realmJsonFilePath,
|
||||
parsedRealmJson,
|
||||
keycloakMajorVersionNumber
|
||||
});
|
||||
const writeRealmJsonFile = async (params: { parsedRealmJson: ParsedRealmJson }) => {
|
||||
const { parsedRealmJson } = params;
|
||||
|
||||
let sourceCode = JSON.stringify(parsedRealmJson, null, 2);
|
||||
|
||||
if (await getIsPrettierAvailable()) {
|
||||
sourceCode = await runPrettier({
|
||||
sourceCode,
|
||||
filePath: realmJsonFilePath
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(realmJsonFilePath, sourceCode);
|
||||
};
|
||||
|
||||
await writeRealmJsonFile({ parsedRealmJson });
|
||||
|
||||
const { onRealmConfigChange } = (() => {
|
||||
const run = runExclusive.build(async () => {
|
||||
@ -113,11 +119,7 @@ export async function getRealmConfig(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
await writeRealmJsonFile({
|
||||
realmJsonFilePath,
|
||||
parsedRealmJson,
|
||||
keycloakMajorVersionNumber
|
||||
});
|
||||
await writeRealmJsonFile({ parsedRealmJson });
|
||||
|
||||
console.log(
|
||||
[
|
||||
|
@ -4,8 +4,7 @@ import {
|
||||
CONTAINER_NAME,
|
||||
KEYCLOAKIFY_SPA_DEV_SERVER_PORT,
|
||||
KEYCLOAKIFY_LOGIN_JAR_BASENAME,
|
||||
TEST_APP_URL,
|
||||
ThemeType
|
||||
TEST_APP_URL
|
||||
} from "../shared/constants";
|
||||
import { SemVer } from "../tools/SemVer";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
@ -35,7 +34,6 @@ import { startViteDevServer } from "./startViteDevServer";
|
||||
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
|
||||
import { getSupportedDockerImageTags } from "./getSupportedDockerImageTags";
|
||||
import { getRealmConfig } from "./realmConfig";
|
||||
import { id } from "tsafe/id";
|
||||
|
||||
export async function command(params: {
|
||||
buildContext: BuildContext;
|
||||
@ -53,17 +51,11 @@ export async function command(params: {
|
||||
.execSync("docker --version", {
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
})
|
||||
.toString("utf8");
|
||||
} catch {
|
||||
commandOutput = "";
|
||||
}
|
||||
?.toString("utf8");
|
||||
} catch {}
|
||||
|
||||
commandOutput = commandOutput.trim().toLowerCase();
|
||||
|
||||
for (const term of ["docker", "podman"]) {
|
||||
if (commandOutput.includes(term)) {
|
||||
break exit_if_docker_not_installed;
|
||||
}
|
||||
if (commandOutput?.includes("Docker") || commandOutput?.includes("podman")) {
|
||||
break exit_if_docker_not_installed;
|
||||
}
|
||||
|
||||
console.log(
|
||||
@ -105,7 +97,7 @@ export async function command(params: {
|
||||
|
||||
const { cliCommandOptions, buildContext } = params;
|
||||
|
||||
const { allSupportedTags, latestMajorTags } = await getSupportedDockerImageTags({
|
||||
const availableTags = await getSupportedDockerImageTags({
|
||||
buildContext
|
||||
});
|
||||
|
||||
@ -113,7 +105,7 @@ export async function command(params: {
|
||||
if (cliCommandOptions.keycloakVersion !== undefined) {
|
||||
const cliCommandOptions_keycloakVersion = cliCommandOptions.keycloakVersion;
|
||||
|
||||
const tag = allSupportedTags.find(tag =>
|
||||
const tag = availableTags.find(tag =>
|
||||
tag.startsWith(cliCommandOptions_keycloakVersion)
|
||||
);
|
||||
|
||||
@ -150,96 +142,15 @@ export async function command(params: {
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
const tag_userSelected = await (async () => {
|
||||
let tag: string;
|
||||
const { value: tag } = await cliSelect<string>({
|
||||
values: availableTags
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
let latestMajorTags_copy = [...latestMajorTags];
|
||||
console.log(`→ ${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 };
|
||||
return { dockerImageTag: tag };
|
||||
})();
|
||||
|
||||
const keycloakMajorVersionNumber = (() => {
|
||||
@ -278,6 +189,32 @@ export async function command(params: {
|
||||
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({
|
||||
buildContext
|
||||
@ -358,24 +295,10 @@ export async function command(params: {
|
||||
))
|
||||
];
|
||||
|
||||
let parsedKeycloakThemesJson = id<
|
||||
{ themes: { name: string; types: (ThemeType | "email")[] }[] } | undefined
|
||||
>(undefined);
|
||||
|
||||
async function extractThemeResourcesFromJar() {
|
||||
await extractArchive({
|
||||
archiveFilePath: jarFilePath,
|
||||
onArchiveFile: async ({ relativeFilePathInArchive, writeFile, readFile }) => {
|
||||
if (
|
||||
relativeFilePathInArchive ===
|
||||
pathJoin("META-INF", "keycloak-themes.json") &&
|
||||
parsedKeycloakThemesJson === undefined
|
||||
) {
|
||||
parsedKeycloakThemesJson = JSON.parse(
|
||||
(await readFile()).toString("utf8")
|
||||
);
|
||||
}
|
||||
|
||||
onArchiveFile: async ({ relativeFilePathInArchive, writeFile }) => {
|
||||
if (isInside({ dirPath: "theme", filePath: relativeFilePathInArchive })) {
|
||||
await writeFile({
|
||||
filePath: pathJoin(
|
||||
@ -397,43 +320,6 @@ export async function command(params: {
|
||||
|
||||
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(
|
||||
buildContext.cacheDirPath,
|
||||
pathBasename(jarFilePath)
|
||||
@ -737,90 +623,42 @@ export async function command(params: {
|
||||
}
|
||||
)
|
||||
.on("all", async (...[, filePath]) => {
|
||||
ignore_path_covered_by_hmr: {
|
||||
if (filePath.endsWith(".properties")) {
|
||||
break ignore_path_covered_by_hmr;
|
||||
ignore_account_spa: {
|
||||
const doImplementAccountSpa =
|
||||
buildContext.implementedThemeTypes.account.isImplemented &&
|
||||
buildContext.implementedThemeTypes.account.type === "Single-Page";
|
||||
|
||||
if (!doImplementAccountSpa) {
|
||||
break ignore_account_spa;
|
||||
}
|
||||
|
||||
if (!doStartDevServer) {
|
||||
break ignore_path_covered_by_hmr;
|
||||
if (
|
||||
!isInside({
|
||||
dirPath: pathJoin(buildContext.themeSrcDirPath, "account"),
|
||||
filePath
|
||||
})
|
||||
) {
|
||||
break ignore_account_spa;
|
||||
}
|
||||
|
||||
ignore_account_spa: {
|
||||
const doImplementAccountSpa =
|
||||
buildContext.implementedThemeTypes.account.isImplemented &&
|
||||
buildContext.implementedThemeTypes.account.type ===
|
||||
"Single-Page";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doImplementAccountSpa) {
|
||||
break ignore_account_spa;
|
||||
}
|
||||
|
||||
if (
|
||||
!isInside({
|
||||
dirPath: pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
"account"
|
||||
),
|
||||
filePath
|
||||
})
|
||||
) {
|
||||
break ignore_account_spa;
|
||||
}
|
||||
|
||||
return;
|
||||
ignore_admin: {
|
||||
if (!buildContext.implementedThemeTypes.admin.isImplemented) {
|
||||
break ignore_admin;
|
||||
}
|
||||
|
||||
ignore_admin: {
|
||||
if (!buildContext.implementedThemeTypes.admin.isImplemented) {
|
||||
break ignore_admin;
|
||||
}
|
||||
|
||||
if (
|
||||
!isInside({
|
||||
dirPath: pathJoin(buildContext.themeSrcDirPath, "admin"),
|
||||
filePath
|
||||
})
|
||||
) {
|
||||
break ignore_admin;
|
||||
}
|
||||
|
||||
return;
|
||||
if (
|
||||
!isInside({
|
||||
dirPath: pathJoin(buildContext.themeSrcDirPath, "admin"),
|
||||
filePath
|
||||
})
|
||||
) {
|
||||
break ignore_admin;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Detected changes in ${filePath}`);
|
||||
|
@ -1,151 +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 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 +0,0 @@
|
||||
export * from "./sync-extension";
|
12
src/bin/tools/OptionalIfCanBeUndefined.ts
Normal file
12
src/bin/tools/OptionalIfCanBeUndefined.ts
Normal file
@ -0,0 +1,12 @@
|
||||
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] };
|
@ -1,99 +0,0 @@
|
||||
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 };
|
||||
})();
|
@ -1,164 +0,0 @@
|
||||
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
|
||||
})
|
||||
);
|
||||
}
|
73
src/bin/tools/crc32.ts
Normal file
73
src/bin/tools/crc32.ts
Normal file
@ -0,0 +1,73 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
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;
|
||||
}
|
61
src/bin/tools/deflate.ts
Normal file
61
src/bin/tools/deflate.ts
Normal file
@ -0,0 +1,61 @@
|
||||
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,8 +14,6 @@ export function getAbsoluteAndInOsFormatPath(params: {
|
||||
|
||||
let pathOut = pathIsh;
|
||||
|
||||
pathOut = pathOut.replace(/^['"]/, "").replace(/['"]$/, "");
|
||||
|
||||
pathOut = pathOut.replace(/\//g, pathSep);
|
||||
|
||||
if (pathOut.startsWith("~")) {
|
||||
|
@ -2,42 +2,40 @@ import { join as pathJoin } from "path";
|
||||
import { existsAsync } from "./fs.existsAsync";
|
||||
import * as child_process from "child_process";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { getIsRootPath } from "../tools/isRootPath";
|
||||
|
||||
export async function getInstalledModuleDirPath(params: {
|
||||
moduleName: string;
|
||||
packageJsonDirPath: string;
|
||||
projectDirPath: string;
|
||||
}) {
|
||||
const { moduleName, packageJsonDirPath } = params;
|
||||
const { moduleName, packageJsonDirPath, projectDirPath } = params;
|
||||
|
||||
{
|
||||
let dirPath = packageJsonDirPath;
|
||||
common_case: {
|
||||
const dirPath = pathJoin(
|
||||
...[packageJsonDirPath, "node_modules", ...moduleName.split("/")]
|
||||
);
|
||||
|
||||
while (true) {
|
||||
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, "..");
|
||||
if (!(await existsAsync(dirPath))) {
|
||||
break common_case;
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -1,45 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
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;
|
||||
}
|
29
src/bin/tools/isTrackedByGit.ts
Normal file
29
src/bin/tools/isTrackedByGit.ts
Normal file
@ -0,0 +1,29 @@
|
||||
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,6 +8,7 @@ import { exclude } from "tsafe/exclude";
|
||||
|
||||
export async function listInstalledModules(params: {
|
||||
packageJsonFilePath: string;
|
||||
projectDirPath: string;
|
||||
filter: (params: { moduleName: string }) => boolean;
|
||||
}): Promise<
|
||||
{
|
||||
@ -17,13 +18,13 @@ export async function listInstalledModules(params: {
|
||||
peerDependencies: Record<string, string>;
|
||||
}[]
|
||||
> {
|
||||
const { packageJsonFilePath, filter } = params;
|
||||
const { packageJsonFilePath, projectDirPath, filter } = params;
|
||||
|
||||
const parsedPackageJson = await readPackageJsonDependencies({
|
||||
packageJsonFilePath
|
||||
});
|
||||
|
||||
const extensionModuleNames = (
|
||||
const uiModuleNames = (
|
||||
[parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const
|
||||
)
|
||||
.filter(exclude(undefined))
|
||||
@ -32,10 +33,11 @@ export async function listInstalledModules(params: {
|
||||
.filter(moduleName => filter({ moduleName }));
|
||||
|
||||
const result = await Promise.all(
|
||||
extensionModuleNames.map(async moduleName => {
|
||||
uiModuleNames.map(async moduleName => {
|
||||
const dirPath = await getInstalledModuleDirPath({
|
||||
moduleName,
|
||||
packageJsonDirPath: pathDirname(packageJsonFilePath)
|
||||
packageJsonDirPath: pathDirname(packageJsonFilePath),
|
||||
projectDirPath
|
||||
});
|
||||
|
||||
const { version, peerDependencies } =
|
||||
|
@ -1,29 +1,10 @@
|
||||
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";
|
||||
import { sep as pathSep } from "path";
|
||||
|
||||
let cache_bestEffort: string | undefined = undefined;
|
||||
let cache: string | undefined = undefined;
|
||||
|
||||
/** NOTE: Careful, this function can fail when the binary
|
||||
* Used is not in the node_modules directory of the project
|
||||
* (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;
|
||||
export function getNodeModulesBinDirPath() {
|
||||
if (cache !== undefined) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
const binPath = process.argv[1];
|
||||
@ -49,122 +30,9 @@ function getNodeModulesBinDirPath_bestEffort() {
|
||||
segments.unshift(segment);
|
||||
}
|
||||
|
||||
if (!foundNodeModules) {
|
||||
throw new Error(`Could not find node_modules in path ${binPath}`);
|
||||
}
|
||||
|
||||
const nodeModulesBinDirPath = segments.join(pathSep);
|
||||
|
||||
cache_bestEffort = nodeModulesBinDirPath;
|
||||
cache = 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,9 +9,8 @@ import { objectKeys } from "tsafe/objectKeys";
|
||||
import { getAbsoluteAndInOsFormatPath } from "./getAbsoluteAndInOsFormatPath";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
import { rmSync } from "./fs.rmSync";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
|
||||
export async function npmInstall(params: { packageJsonDirPath: string }) {
|
||||
export function npmInstall(params: { packageJsonDirPath: string }) {
|
||||
const { packageJsonDirPath } = params;
|
||||
|
||||
const packageManagerBinName = (() => {
|
||||
@ -69,7 +68,7 @@ export async function npmInstall(params: { packageJsonDirPath: string }) {
|
||||
|
||||
console.log(chalk.green("Installing in a way that won't break the links..."));
|
||||
|
||||
await installWithoutBreakingLinks({
|
||||
installWithoutBreakingLinks({
|
||||
packageJsonDirPath,
|
||||
garronejLinkInfos
|
||||
});
|
||||
@ -78,9 +77,9 @@ export async function npmInstall(params: { packageJsonDirPath: string }) {
|
||||
}
|
||||
|
||||
try {
|
||||
await runPackageManagerInstall({
|
||||
packageManagerBinName,
|
||||
cwd: packageJsonDirPath
|
||||
child_process.execSync(`${packageManagerBinName} install`, {
|
||||
cwd: packageJsonDirPath,
|
||||
stdio: "inherit"
|
||||
});
|
||||
} catch {
|
||||
console.log(
|
||||
@ -91,42 +90,6 @@ export async 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: {
|
||||
packageJsonDirPath: string;
|
||||
}): { linkedModuleNames: string[]; yarnHomeDirPath: string } | undefined {
|
||||
@ -217,7 +180,7 @@ function getGarronejLinkInfos(params: {
|
||||
return { linkedModuleNames, yarnHomeDirPath };
|
||||
}
|
||||
|
||||
async function installWithoutBreakingLinks(params: {
|
||||
function installWithoutBreakingLinks(params: {
|
||||
packageJsonDirPath: string;
|
||||
garronejLinkInfos: Exclude<ReturnType<typeof getGarronejLinkInfos>, undefined>;
|
||||
}) {
|
||||
@ -298,9 +261,9 @@ async function installWithoutBreakingLinks(params: {
|
||||
pathJoin(tmpProjectDirPath, YARN_LOCK)
|
||||
);
|
||||
|
||||
await runPackageManagerInstall({
|
||||
packageManagerBinName: "yarn",
|
||||
cwd: tmpProjectDirPath
|
||||
child_process.execSync(`yarn install`, {
|
||||
cwd: tmpProjectDirPath,
|
||||
stdio: "inherit"
|
||||
});
|
||||
|
||||
// NOTE: Moving the modules from the tmp project to the actual project
|
||||
|
47
src/bin/tools/octokit-addons/getLatestsSemVersionedTag.ts
Normal file
47
src/bin/tools/octokit-addons/getLatestsSemVersionedTag.ts
Normal file
@ -0,0 +1,47 @@
|
||||
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 };
|
||||
}
|
60
src/bin/tools/octokit-addons/listTags.ts
Normal file
60
src/bin/tools/octokit-addons/listTags.ts
Normal file
@ -0,0 +1,60 @@
|
||||
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,9 +15,7 @@ export async function getIsPrettierAvailable(): Promise<boolean> {
|
||||
return getIsPrettierAvailable.cache;
|
||||
}
|
||||
|
||||
const nodeModulesBinDirPath = getNodeModulesBinDirPath({
|
||||
packageJsonFilePath: undefined
|
||||
});
|
||||
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
|
||||
|
||||
const prettierBinPath = pathJoin(nodeModulesBinDirPath, "prettier");
|
||||
|
||||
@ -52,26 +50,10 @@ export async function getPrettier(): Promise<PrettierAndConfigHash> {
|
||||
// So we do a sketchy eval to bypass ncc.
|
||||
// We make sure to only do that when linking, otherwise we import properly.
|
||||
if (readThisNpmPackageVersion().startsWith("0.0.0")) {
|
||||
const prettierDirPath = pathResolve(
|
||||
pathJoin(
|
||||
getNodeModulesBinDirPath({ packageJsonFilePath: undefined }),
|
||||
"..",
|
||||
"prettier"
|
||||
)
|
||||
eval(
|
||||
`${symToStr({ prettier })} = require("${pathResolve(pathJoin(getNodeModulesBinDirPath(), "..", "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));
|
||||
|
||||
break import_prettier;
|
||||
@ -82,7 +64,7 @@ export async function getPrettier(): Promise<PrettierAndConfigHash> {
|
||||
|
||||
const configHash = await (async () => {
|
||||
const configFilePath = await prettier.resolveConfigFile(
|
||||
pathJoin(getNodeModulesBinDirPath({ packageJsonFilePath: undefined }), "..")
|
||||
pathJoin(getNodeModulesBinDirPath(), "..")
|
||||
);
|
||||
|
||||
if (configFilePath === null) {
|
||||
|
39
src/bin/tools/tee.ts
Normal file
39
src/bin/tools/tee.ts
Normal file
@ -0,0 +1,39 @@
|
||||
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;
|
||||
}
|
49
src/bin/tools/trimIndent.ts
Normal file
49
src/bin/tools/trimIndent.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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,31 +1,15 @@
|
||||
import * as child_process from "child_process";
|
||||
import {
|
||||
dirname as pathDirname,
|
||||
basename as pathBasename,
|
||||
join as pathJoin,
|
||||
sep as pathSep
|
||||
} from "path";
|
||||
import { dirname as pathDirname, basename as pathBasename } from "path";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import { existsAsync } from "./fs.existsAsync";
|
||||
|
||||
export async function untrackFromGit(params: { filePath: string }): Promise<void> {
|
||||
const { filePath } = params;
|
||||
|
||||
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(
|
||||
`git rm --cached '${relativePath.split(pathSep).join("/")}'`,
|
||||
{ cwd: dirPath },
|
||||
`git rm --cached ${pathBasename(filePath)}`,
|
||||
{ cwd: pathDirname(filePath) },
|
||||
error => {
|
||||
if (error !== null) {
|
||||
dDone.reject(error);
|
||||
|
@ -10,5 +10,5 @@
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["initialize-account-theme/multi-page-boilerplate"]
|
||||
"exclude": ["initialize-account-theme/src"]
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
await command({ buildContext });
|
||||
}
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "update-kc-gen",
|
||||
buildContext
|
||||
});
|
||||
|
@ -769,8 +769,6 @@ export declare namespace Validators {
|
||||
export type PasswordPolicies = {
|
||||
/** The minimum length of the password */
|
||||
length?: number;
|
||||
/** The maximum length of the password */
|
||||
maxLength?: number;
|
||||
/** The minimum number of digits required in the password */
|
||||
digits?: number;
|
||||
/** The minimum number of lowercase characters required in the password */
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { JSX } from "keycloakify/tools/JSX";
|
||||
import { useEffect, Fragment } from "react";
|
||||
import { useEffect, useReducer, Fragment } from "react";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||
import type { KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import {
|
||||
useUserProfileForm,
|
||||
@ -90,6 +89,7 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
|
||||
{advancedMsg(attribute.annotations.inputHelperTextAfter)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{AfterField !== undefined && (
|
||||
<AfterField
|
||||
attribute={attribute}
|
||||
@ -106,10 +106,6 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
|
||||
</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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -253,7 +249,15 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
||||
|
||||
const { msgStr } = i18n;
|
||||
|
||||
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
||||
|
||||
useEffect(() => {
|
||||
const passwordInputElement = document.getElementById(passwordInputId);
|
||||
|
||||
assert(passwordInputElement instanceof HTMLInputElement);
|
||||
|
||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
||||
}, [isPasswordRevealed]);
|
||||
|
||||
return (
|
||||
<div className={kcClsx("kcInputGroup")}>
|
||||
|
@ -509,8 +509,6 @@ function formStateSelector(params: { state: internal.State }): FormState {
|
||||
switch (error.source.name) {
|
||||
case "length":
|
||||
return hasLostFocusAtLeastOnce;
|
||||
case "maxLength":
|
||||
return hasLostFocusAtLeastOnce;
|
||||
case "digits":
|
||||
return hasLostFocusAtLeastOnce;
|
||||
case "lowerCase":
|
||||
@ -969,34 +967,6 @@ 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: {
|
||||
const policyName = "digits";
|
||||
|
||||
|
@ -31,10 +31,10 @@ export default function Info(props: PageProps<Extract<KcContext, { pageId: "info
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: kcSanitize(
|
||||
(() => {
|
||||
let html = message.summary?.trim();
|
||||
let html = message.summary;
|
||||
|
||||
if (requiredActions) {
|
||||
html += " <b>";
|
||||
html += "<b>";
|
||||
|
||||
html += requiredActions.map(requiredAction => advancedMsgStr(`requiredAction.${requiredAction}`)).join(", ");
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { JSX } from "keycloakify/tools/JSX";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect, useReducer } from "react";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
@ -200,7 +200,15 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
||||
|
||||
const { msgStr } = i18n;
|
||||
|
||||
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
||||
|
||||
useEffect(() => {
|
||||
const passwordInputElement = document.getElementById(passwordInputId);
|
||||
|
||||
assert(passwordInputElement instanceof HTMLInputElement);
|
||||
|
||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
||||
}, [isPasswordRevealed]);
|
||||
|
||||
return (
|
||||
<div className={kcClsx("kcInputGroup")}>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { Fragment } from "react";
|
||||
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
@ -17,8 +17,6 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
@ -28,16 +26,7 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
|
||||
displayMessage={!messagesPerField.existsError("totp")}
|
||||
headerNode={msg("doLogIn")}
|
||||
>
|
||||
<form
|
||||
id="kc-otp-login-form"
|
||||
className={kcClsx("kcFormClass")}
|
||||
action={url.loginAction}
|
||||
onSubmit={() => {
|
||||
setIsSubmitting(true);
|
||||
return true;
|
||||
}}
|
||||
method="post"
|
||||
>
|
||||
<form id="kc-otp-login-form" className={kcClsx("kcFormClass")} action={url.loginAction} method="post">
|
||||
{otpLogin.userOtpCredentials.length > 1 && (
|
||||
<div className={kcClsx("kcFormGroupClass")}>
|
||||
<div className={kcClsx("kcInputWrapperClass")}>
|
||||
@ -105,7 +94,6 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
|
||||
id="kc-login"
|
||||
type="submit"
|
||||
value={msgStr("doLogIn")}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import type { JSX } from "keycloakify/tools/JSX";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect, useReducer } from "react";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
@ -107,7 +107,15 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
||||
|
||||
const { msgStr } = i18n;
|
||||
|
||||
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
||||
|
||||
useEffect(() => {
|
||||
const passwordInputElement = document.getElementById(passwordInputId);
|
||||
|
||||
assert(passwordInputElement instanceof HTMLInputElement);
|
||||
|
||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
||||
}, [isPasswordRevealed]);
|
||||
|
||||
return (
|
||||
<div className={kcClsx("kcInputGroup")}>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { JSX } from "keycloakify/tools/JSX";
|
||||
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||
import { useEffect, useReducer } from "react";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
@ -145,7 +146,15 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
||||
|
||||
const { msgStr } = i18n;
|
||||
|
||||
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
||||
|
||||
useEffect(() => {
|
||||
const passwordInputElement = document.getElementById(passwordInputId);
|
||||
|
||||
assert(passwordInputElement instanceof HTMLInputElement);
|
||||
|
||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
||||
}, [isPasswordRevealed]);
|
||||
|
||||
return (
|
||||
<div className={kcClsx("kcInputGroup")}>
|
||||
|
@ -98,7 +98,7 @@ export default function LoginUsername(props: PageProps<Extract<KcContext, { page
|
||||
defaultValue={login.username ?? ""}
|
||||
type="text"
|
||||
autoFocus
|
||||
autoComplete="username"
|
||||
autoComplete="off"
|
||||
aria-invalid={messagesPerField.existsError("username")}
|
||||
/>
|
||||
{messagesPerField.existsError("username") && (
|
||||
|
@ -1,45 +0,0 @@
|
||||
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,9 +155,8 @@ export function keycloakify(params: keycloakify.Params) {
|
||||
{
|
||||
const isJavascriptFile = id.endsWith(".js") || id.endsWith(".jsx");
|
||||
const isTypeScriptFile = id.endsWith(".ts") || id.endsWith(".tsx");
|
||||
const isSvelteFile = id.endsWith(".svelte");
|
||||
|
||||
if (!isTypeScriptFile && !isJavascriptFile && !isSvelteFile) {
|
||||
if (!isTypeScriptFile && !isJavascriptFile) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ export const WithRequiredActions: Story = {
|
||||
kcContext={{
|
||||
messageHeader: "Message header",
|
||||
message: {
|
||||
summary: "Required actions:"
|
||||
summary: "Required actions: "
|
||||
},
|
||||
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PROFILE", "VERIFY_EMAIL", "CUSTOM_ACTION"],
|
||||
"x-keycloakify": {
|
||||
|
@ -62,22 +62,3 @@ 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
|
||||
},
|
||||
"include": ["../src", "."],
|
||||
"exclude": ["../src/bin/initialize-account-theme/multi-page-boilerplate"]
|
||||
"exclude": ["../src/bin/initialize-account-theme/src"]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user