keycloakify 9.3.0 → 9.4.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/account/kcContext/getKcContextFromWindow.js +2 -2
- package/account/kcContext/getKcContextFromWindow.js.map +1 -1
- package/bin/constants.d.ts +4 -1
- package/bin/constants.js +5 -2
- package/bin/constants.js.map +1 -1
- package/bin/copy-keycloak-resources-to-public.js +3 -4
- package/bin/copy-keycloak-resources-to-public.js.map +1 -1
- package/bin/download-builtin-keycloak-theme.js +98 -31
- package/bin/download-builtin-keycloak-theme.js.map +1 -1
- package/bin/eject-keycloak-page.js +2 -2
- package/bin/eject-keycloak-page.js.map +1 -1
- package/bin/{getSrcDirPath.js → getThemeSrcDirPath.js} +1 -1
- package/bin/getThemeSrcDirPath.js.map +1 -0
- package/bin/initialize-email-theme.js +6 -5
- package/bin/initialize-email-theme.js.map +1 -1
- package/bin/keycloakify/{BuildOptions.d.ts → buildOptions/buildOptions.d.ts} +2 -1
- package/bin/keycloakify/buildOptions/buildOptions.js +136 -0
- package/bin/keycloakify/buildOptions/buildOptions.js.map +1 -0
- package/bin/keycloakify/buildOptions/getKeycloakifyBuildDirPath.d.ts +7 -0
- package/bin/keycloakify/buildOptions/getKeycloakifyBuildDirPath.js +27 -0
- package/bin/keycloakify/buildOptions/getKeycloakifyBuildDirPath.js.map +1 -0
- package/bin/keycloakify/buildOptions/index.d.ts +1 -0
- package/bin/keycloakify/{generateJavaStackFiles → buildOptions}/index.js +1 -1
- package/bin/keycloakify/buildOptions/index.js.map +1 -0
- package/bin/keycloakify/buildOptions/parsedPackageJson.d.ts +19 -0
- package/bin/keycloakify/{parsedPackageJson.js → buildOptions/parsedPackageJson.js} +6 -7
- package/bin/keycloakify/buildOptions/parsedPackageJson.js.map +1 -0
- package/bin/keycloakify/buildOptions/resolvedViteConfig.d.ts +12 -0
- package/bin/keycloakify/buildOptions/resolvedViteConfig.js +82 -0
- package/bin/keycloakify/buildOptions/resolvedViteConfig.js.map +1 -0
- package/bin/keycloakify/generateFtl/generateFtl.d.ts +4 -1
- package/bin/keycloakify/generateFtl/generateFtl.js +5 -5
- package/bin/keycloakify/generateFtl/generateFtl.js.map +1 -1
- package/bin/keycloakify/generatePom.d.ts +12 -0
- package/bin/keycloakify/generatePom.js +59 -0
- package/bin/keycloakify/generatePom.js.map +1 -0
- package/bin/keycloakify/{generateJavaStackFiles → generateTheme}/bringInAccountV1.d.ts +2 -1
- package/bin/keycloakify/{generateJavaStackFiles → generateTheme}/bringInAccountV1.js +21 -67
- package/bin/keycloakify/generateTheme/bringInAccountV1.js.map +1 -0
- package/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.d.ts +0 -3
- package/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.js +5 -34
- package/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.js.map +1 -1
- package/bin/keycloakify/generateTheme/generateTheme.d.ts +4 -1
- package/bin/keycloakify/generateTheme/generateTheme.js +123 -70
- package/bin/keycloakify/generateTheme/generateTheme.js.map +1 -1
- package/bin/keycloakify/keycloakify.js +13 -72
- package/bin/keycloakify/keycloakify.js.map +1 -1
- package/bin/keycloakify/replacers/replaceImportsInCssCode.js +2 -1
- package/bin/keycloakify/replacers/replaceImportsInCssCode.js.map +1 -1
- package/bin/keycloakify/replacers/replaceImportsInInlineCssCode.js +2 -1
- package/bin/keycloakify/replacers/replaceImportsInInlineCssCode.js.map +1 -1
- package/bin/keycloakify/replacers/replaceImportsInJsCode/index.d.ts +1 -0
- package/bin/keycloakify/replacers/replaceImportsInJsCode/index.js +18 -0
- package/bin/keycloakify/replacers/replaceImportsInJsCode/index.js.map +1 -0
- package/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.d.ts +12 -0
- package/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.js +70 -0
- package/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.js.map +1 -0
- package/bin/keycloakify/replacers/replaceImportsInJsCode/vite.d.ts +13 -0
- package/bin/keycloakify/replacers/replaceImportsInJsCode/vite.js +95 -0
- package/bin/keycloakify/replacers/replaceImportsInJsCode/vite.js.map +1 -0
- package/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.d.ts +12 -0
- package/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.js +108 -0
- package/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.js.map +1 -0
- package/bin/promptKeycloakVersion.js +0 -1
- package/bin/promptKeycloakVersion.js.map +1 -1
- package/bin/tools/OptionalIfCanBeUndefined.d.ts +14 -0
- package/bin/tools/OptionalIfCanBeUndefined.js +3 -0
- package/bin/tools/OptionalIfCanBeUndefined.js.map +1 -0
- package/bin/tools/SemVer.d.ts +26 -0
- package/bin/tools/{NpmModuleVersion.js → SemVer.js} +39 -23
- package/bin/tools/SemVer.js.map +1 -0
- package/bin/tools/String.prototype.replaceAll.d.ts +1 -0
- package/bin/tools/String.prototype.replaceAll.js +29 -0
- package/bin/tools/String.prototype.replaceAll.js.map +1 -0
- package/bin/tools/crawl.js +9 -9
- package/bin/tools/crawl.js.map +1 -1
- package/bin/tools/downloadAndUnzip.js +30 -19
- package/bin/tools/downloadAndUnzip.js.map +1 -1
- package/bin/tools/fs.rm.d.ts +8 -0
- package/bin/tools/fs.rm.js +151 -0
- package/bin/tools/fs.rm.js.map +1 -0
- package/bin/tools/fs.rmSync.d.ts +8 -0
- package/bin/tools/fs.rmSync.js +58 -0
- package/bin/tools/fs.rmSync.js.map +1 -0
- package/bin/tools/getAbsoluteAndInOsFormatPath.js +1 -0
- package/bin/tools/getAbsoluteAndInOsFormatPath.js.map +1 -1
- package/bin/tools/octokit-addons/getLatestsSemVersionedTag.d.ts +2 -3
- package/bin/tools/octokit-addons/getLatestsSemVersionedTag.js +6 -6
- package/bin/tools/octokit-addons/getLatestsSemVersionedTag.js.map +1 -1
- package/bin/tools/transformCodebase.d.ts +5 -1
- package/bin/tools/transformCodebase.js +31 -13
- package/bin/tools/transformCodebase.js.map +1 -1
- package/login/kcContext/getKcContextFromWindow.js +2 -2
- package/login/kcContext/getKcContextFromWindow.js.map +1 -1
- package/login/lib/useGetClassName.js +1 -1
- package/login/lib/useGetClassName.js.map +1 -1
- package/login/pages/LoginOtp.js +5 -43
- package/login/pages/LoginOtp.js.map +1 -1
- package/package.json +104 -53
- package/src/account/kcContext/getKcContextFromWindow.ts +2 -2
- package/src/bin/constants.ts +4 -1
- package/src/bin/copy-keycloak-resources-to-public.ts +2 -3
- package/src/bin/download-builtin-keycloak-theme.ts +96 -48
- package/src/bin/eject-keycloak-page.ts +1 -1
- package/src/bin/initialize-email-theme.ts +4 -3
- package/src/bin/keycloakify/buildOptions/buildOptions.ts +185 -0
- package/src/bin/keycloakify/buildOptions/getKeycloakifyBuildDirPath.ts +33 -0
- package/src/bin/keycloakify/buildOptions/index.ts +1 -0
- package/src/bin/keycloakify/{parsedPackageJson.ts → buildOptions/parsedPackageJson.ts} +4 -6
- package/src/bin/keycloakify/buildOptions/resolvedViteConfig.ts +85 -0
- package/src/bin/keycloakify/generateFtl/generateFtl.ts +12 -8
- package/src/bin/keycloakify/generatePom.ts +70 -0
- package/src/bin/keycloakify/generateStartKeycloakTestingContainer.ts +1 -1
- package/src/bin/keycloakify/generateTheme/bringInAccountV1.ts +83 -0
- package/src/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.ts +11 -52
- package/src/bin/keycloakify/generateTheme/generateTheme.ts +141 -51
- package/src/bin/keycloakify/keycloakify.ts +11 -58
- package/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts +3 -2
- package/src/bin/keycloakify/replacers/replaceImportsInInlineCssCode.ts +3 -2
- package/src/bin/keycloakify/replacers/replaceImportsInJsCode/index.ts +1 -0
- package/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts +66 -0
- package/src/bin/keycloakify/replacers/replaceImportsInJsCode/vite.ts +85 -0
- package/src/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.ts +90 -0
- package/src/bin/promptKeycloakVersion.ts +0 -1
- package/src/bin/tools/OptionalIfCanBeUndefined.ts +12 -0
- package/src/bin/tools/SemVer.ts +99 -0
- package/src/bin/tools/String.prototype.replaceAll.ts +30 -0
- package/src/bin/tools/crawl.ts +8 -8
- package/src/bin/tools/downloadAndUnzip.ts +11 -4
- package/src/bin/tools/fs.rm.ts +43 -0
- package/src/bin/tools/fs.rmSync.ts +34 -0
- package/src/bin/tools/getAbsoluteAndInOsFormatPath.ts +2 -0
- package/src/bin/tools/octokit-addons/getLatestsSemVersionedTag.ts +9 -9
- package/src/bin/tools/transformCodebase.ts +35 -14
- package/src/login/kcContext/getKcContextFromWindow.ts +2 -2
- package/src/login/lib/useGetClassName.ts +1 -1
- package/src/login/pages/LoginOtp.tsx +64 -94
- package/src/tsconfig.json +1 -1
- package/src/vite-plugin/config.json +232 -0
- package/src/vite-plugin/index.ts +1 -0
- package/src/vite-plugin/tsconfig.json +18 -0
- package/src/vite-plugin/vite-plugin.ts +128 -0
- package/vite-plugin/index.d.ts +1 -0
- package/vite-plugin/index.js +18 -0
- package/vite-plugin/index.js.map +1 -0
- package/vite-plugin/tsconfig.tsbuildinfo +1 -0
- package/vite-plugin/vite-plugin.d.ts +2 -0
- package/vite-plugin/vite-plugin.js +118 -0
- package/vite-plugin/vite-plugin.js.map +1 -0
- package/bin/getSrcDirPath.js.map +0 -1
- package/bin/keycloakify/BuildOptions.js +0 -113
- package/bin/keycloakify/BuildOptions.js.map +0 -1
- package/bin/keycloakify/ftlValuesGlobalName.d.ts +0 -1
- package/bin/keycloakify/ftlValuesGlobalName.js +0 -5
- package/bin/keycloakify/ftlValuesGlobalName.js.map +0 -1
- package/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.js.map +0 -1
- package/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.d.ts +0 -16
- package/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.js +0 -205
- package/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.js.map +0 -1
- package/bin/keycloakify/generateJavaStackFiles/index.d.ts +0 -1
- package/bin/keycloakify/generateJavaStackFiles/index.js.map +0 -1
- package/bin/keycloakify/generateTheme/readStaticResourcesUsage.d.ts +0 -15
- package/bin/keycloakify/generateTheme/readStaticResourcesUsage.js +0 -158
- package/bin/keycloakify/generateTheme/readStaticResourcesUsage.js.map +0 -1
- package/bin/keycloakify/parsedPackageJson.d.ts +0 -108
- package/bin/keycloakify/parsedPackageJson.js.map +0 -1
- package/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.d.ts +0 -5
- package/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.js +0 -74
- package/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.js.map +0 -1
- package/bin/tools/NpmModuleVersion.d.ts +0 -22
- package/bin/tools/NpmModuleVersion.js.map +0 -1
- package/src/bin/keycloakify/BuildOptions.ts +0 -157
- package/src/bin/keycloakify/ftlValuesGlobalName.ts +0 -1
- package/src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts +0 -101
- package/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts +0 -141
- package/src/bin/keycloakify/generateJavaStackFiles/index.ts +0 -1
- package/src/bin/keycloakify/generateTheme/readStaticResourcesUsage.ts +0 -76
- package/src/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.ts +0 -51
- package/src/bin/tools/NpmModuleVersion.ts +0 -73
- /package/bin/{getSrcDirPath.d.ts → getThemeSrcDirPath.d.ts} +0 -0
- /package/src/bin/{getSrcDirPath.ts → getThemeSrcDirPath.ts} +0 -0
@@ -0,0 +1,185 @@
|
|
1
|
+
import { parse as urlParse } from "url";
|
2
|
+
import { readParsedPackageJson } from "./parsedPackageJson";
|
3
|
+
import { join as pathJoin } from "path";
|
4
|
+
import parseArgv from "minimist";
|
5
|
+
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
|
6
|
+
import { readResolvedViteConfig } from "./resolvedViteConfig";
|
7
|
+
import { getKeycloakifyBuildDirPath } from "./getKeycloakifyBuildDirPath";
|
8
|
+
|
9
|
+
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
10
|
+
export type BuildOptions = {
|
11
|
+
bundler: "vite" | "webpack";
|
12
|
+
isSilent: boolean;
|
13
|
+
themeVersion: string;
|
14
|
+
themeNames: string[];
|
15
|
+
extraThemeProperties: string[] | undefined;
|
16
|
+
groupId: string;
|
17
|
+
artifactId: string;
|
18
|
+
doCreateJar: boolean;
|
19
|
+
loginThemeResourcesFromKeycloakVersion: string;
|
20
|
+
reactAppRootDirPath: string;
|
21
|
+
reactAppBuildDirPath: string;
|
22
|
+
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
|
23
|
+
keycloakifyBuildDirPath: string;
|
24
|
+
publicDirPath: string;
|
25
|
+
cacheDirPath: string;
|
26
|
+
/** If your app is hosted under a subpath, it's the case in CRA if you have "homepage": "https://example.com/my-app" in your package.json
|
27
|
+
* In this case the urlPathname will be "/my-app/" */
|
28
|
+
urlPathname: string | undefined;
|
29
|
+
assetsDirPath: string;
|
30
|
+
doBuildRetrocompatAccountTheme: boolean;
|
31
|
+
};
|
32
|
+
|
33
|
+
export function readBuildOptions(params: { reactAppRootDirPath: string; processArgv: string[] }): BuildOptions {
|
34
|
+
const { reactAppRootDirPath, processArgv } = params;
|
35
|
+
|
36
|
+
const parsedPackageJson = readParsedPackageJson({ reactAppRootDirPath });
|
37
|
+
|
38
|
+
const { resolvedViteConfig } =
|
39
|
+
readResolvedViteConfig({
|
40
|
+
"parsedPackageJson_keycloakify_keycloakifyBuildDirPath": parsedPackageJson.keycloakify?.keycloakifyBuildDirPath,
|
41
|
+
reactAppRootDirPath
|
42
|
+
}) ?? {};
|
43
|
+
|
44
|
+
const themeNames = (() => {
|
45
|
+
if (parsedPackageJson.keycloakify?.themeName === undefined) {
|
46
|
+
return [
|
47
|
+
parsedPackageJson.name
|
48
|
+
.replace(/^@(.*)/, "$1")
|
49
|
+
.split("/")
|
50
|
+
.join("-")
|
51
|
+
];
|
52
|
+
}
|
53
|
+
|
54
|
+
if (typeof parsedPackageJson.keycloakify.themeName === "string") {
|
55
|
+
return [parsedPackageJson.keycloakify.themeName];
|
56
|
+
}
|
57
|
+
|
58
|
+
return parsedPackageJson.keycloakify.themeName;
|
59
|
+
})();
|
60
|
+
|
61
|
+
const { keycloakifyBuildDirPath } = getKeycloakifyBuildDirPath({
|
62
|
+
"parsedPackageJson_keycloakify_keycloakifyBuildDirPath": parsedPackageJson.keycloakify?.keycloakifyBuildDirPath,
|
63
|
+
reactAppRootDirPath,
|
64
|
+
"bundler": resolvedViteConfig !== undefined ? "vite" : "webpack"
|
65
|
+
});
|
66
|
+
|
67
|
+
const reactAppBuildDirPath = (() => {
|
68
|
+
webpack: {
|
69
|
+
if (resolvedViteConfig !== undefined) {
|
70
|
+
break webpack;
|
71
|
+
}
|
72
|
+
|
73
|
+
if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) {
|
74
|
+
return getAbsoluteAndInOsFormatPath({
|
75
|
+
"pathIsh": parsedPackageJson.keycloakify?.reactAppBuildDirPath,
|
76
|
+
"cwd": reactAppRootDirPath
|
77
|
+
});
|
78
|
+
}
|
79
|
+
|
80
|
+
return pathJoin(reactAppRootDirPath, "build");
|
81
|
+
}
|
82
|
+
|
83
|
+
return pathJoin(reactAppRootDirPath, resolvedViteConfig.buildDir);
|
84
|
+
})();
|
85
|
+
|
86
|
+
return {
|
87
|
+
"bundler": resolvedViteConfig !== undefined ? "vite" : "webpack",
|
88
|
+
"isSilent": (() => {
|
89
|
+
const argv = parseArgv(processArgv);
|
90
|
+
|
91
|
+
return typeof argv["silent"] === "boolean" ? argv["silent"] : false;
|
92
|
+
})(),
|
93
|
+
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
|
94
|
+
themeNames,
|
95
|
+
"extraThemeProperties": parsedPackageJson.keycloakify?.extraThemeProperties,
|
96
|
+
"groupId": (() => {
|
97
|
+
const fallbackGroupId = `${themeNames[0]}.keycloak`;
|
98
|
+
|
99
|
+
return (
|
100
|
+
process.env.KEYCLOAKIFY_GROUP_ID ??
|
101
|
+
parsedPackageJson.keycloakify?.groupId ??
|
102
|
+
(parsedPackageJson.homepage === undefined
|
103
|
+
? fallbackGroupId
|
104
|
+
: urlParse(parsedPackageJson.homepage)
|
105
|
+
.host?.replace(/:[0-9]+$/, "")
|
106
|
+
?.split(".")
|
107
|
+
.reverse()
|
108
|
+
.join(".") ?? fallbackGroupId) + ".keycloak"
|
109
|
+
);
|
110
|
+
})(),
|
111
|
+
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? parsedPackageJson.keycloakify?.artifactId ?? `${themeNames[0]}-keycloak-theme`,
|
112
|
+
"doCreateJar": parsedPackageJson.keycloakify?.doCreateJar ?? true,
|
113
|
+
"loginThemeResourcesFromKeycloakVersion": parsedPackageJson.keycloakify?.loginThemeResourcesFromKeycloakVersion ?? "11.0.3",
|
114
|
+
reactAppRootDirPath,
|
115
|
+
reactAppBuildDirPath,
|
116
|
+
keycloakifyBuildDirPath,
|
117
|
+
"publicDirPath": (() => {
|
118
|
+
webpack: {
|
119
|
+
if (resolvedViteConfig !== undefined) {
|
120
|
+
break webpack;
|
121
|
+
}
|
122
|
+
|
123
|
+
if (process.env.PUBLIC_DIR_PATH !== undefined) {
|
124
|
+
return getAbsoluteAndInOsFormatPath({
|
125
|
+
"pathIsh": process.env.PUBLIC_DIR_PATH,
|
126
|
+
"cwd": reactAppRootDirPath
|
127
|
+
});
|
128
|
+
}
|
129
|
+
|
130
|
+
return pathJoin(reactAppRootDirPath, "public");
|
131
|
+
}
|
132
|
+
|
133
|
+
return pathJoin(reactAppRootDirPath, resolvedViteConfig.publicDir);
|
134
|
+
})(),
|
135
|
+
"cacheDirPath": pathJoin(
|
136
|
+
(() => {
|
137
|
+
if (process.env.XDG_CACHE_HOME !== undefined) {
|
138
|
+
return getAbsoluteAndInOsFormatPath({
|
139
|
+
"pathIsh": process.env.XDG_CACHE_HOME,
|
140
|
+
"cwd": reactAppRootDirPath
|
141
|
+
});
|
142
|
+
}
|
143
|
+
|
144
|
+
return pathJoin(reactAppRootDirPath, "node_modules", ".cache");
|
145
|
+
})(),
|
146
|
+
"keycloakify"
|
147
|
+
),
|
148
|
+
"urlPathname": (() => {
|
149
|
+
webpack: {
|
150
|
+
if (resolvedViteConfig !== undefined) {
|
151
|
+
break webpack;
|
152
|
+
}
|
153
|
+
|
154
|
+
const { homepage } = parsedPackageJson;
|
155
|
+
|
156
|
+
let url: URL | undefined = undefined;
|
157
|
+
|
158
|
+
if (homepage !== undefined) {
|
159
|
+
url = new URL(homepage);
|
160
|
+
}
|
161
|
+
|
162
|
+
if (url === undefined) {
|
163
|
+
return undefined;
|
164
|
+
}
|
165
|
+
|
166
|
+
const out = url.pathname.replace(/([^/])$/, "$1/");
|
167
|
+
return out === "/" ? undefined : out;
|
168
|
+
}
|
169
|
+
|
170
|
+
return resolvedViteConfig.urlPathname;
|
171
|
+
})(),
|
172
|
+
"assetsDirPath": (() => {
|
173
|
+
webpack: {
|
174
|
+
if (resolvedViteConfig !== undefined) {
|
175
|
+
break webpack;
|
176
|
+
}
|
177
|
+
|
178
|
+
return pathJoin(reactAppBuildDirPath, "static");
|
179
|
+
}
|
180
|
+
|
181
|
+
return pathJoin(reactAppBuildDirPath, resolvedViteConfig.assetsDir);
|
182
|
+
})(),
|
183
|
+
"doBuildRetrocompatAccountTheme": parsedPackageJson.keycloakify?.doBuildRetrocompatAccountTheme ?? true
|
184
|
+
};
|
185
|
+
}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
|
2
|
+
import { join as pathJoin } from "path";
|
3
|
+
|
4
|
+
export function getKeycloakifyBuildDirPath(params: {
|
5
|
+
reactAppRootDirPath: string;
|
6
|
+
parsedPackageJson_keycloakify_keycloakifyBuildDirPath: string | undefined;
|
7
|
+
bundler: "vite" | "webpack";
|
8
|
+
}) {
|
9
|
+
const { reactAppRootDirPath, parsedPackageJson_keycloakify_keycloakifyBuildDirPath, bundler } = params;
|
10
|
+
|
11
|
+
const keycloakifyBuildDirPath = (() => {
|
12
|
+
if (parsedPackageJson_keycloakify_keycloakifyBuildDirPath !== undefined) {
|
13
|
+
getAbsoluteAndInOsFormatPath({
|
14
|
+
"pathIsh": parsedPackageJson_keycloakify_keycloakifyBuildDirPath,
|
15
|
+
"cwd": reactAppRootDirPath
|
16
|
+
});
|
17
|
+
}
|
18
|
+
|
19
|
+
return pathJoin(
|
20
|
+
reactAppRootDirPath,
|
21
|
+
`${(() => {
|
22
|
+
switch (bundler) {
|
23
|
+
case "vite":
|
24
|
+
return "dist";
|
25
|
+
case "webpack":
|
26
|
+
return "build";
|
27
|
+
}
|
28
|
+
})()}_keycloak`
|
29
|
+
);
|
30
|
+
})();
|
31
|
+
|
32
|
+
return { keycloakifyBuildDirPath };
|
33
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from "./buildOptions";
|
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
|
2
2
|
import { assert } from "tsafe";
|
3
3
|
import type { Equals } from "tsafe";
|
4
4
|
import { z } from "zod";
|
5
|
-
import { pathJoin } from "
|
5
|
+
import { join as pathJoin } from "path";
|
6
6
|
|
7
7
|
export type ParsedPackageJson = {
|
8
8
|
name: string;
|
@@ -10,7 +10,6 @@ export type ParsedPackageJson = {
|
|
10
10
|
homepage?: string;
|
11
11
|
keycloakify?: {
|
12
12
|
extraThemeProperties?: string[];
|
13
|
-
areAppAndKeycloakServerSharingSameDomain?: boolean;
|
14
13
|
artifactId?: string;
|
15
14
|
groupId?: string;
|
16
15
|
doCreateJar?: boolean;
|
@@ -22,14 +21,13 @@ export type ParsedPackageJson = {
|
|
22
21
|
};
|
23
22
|
};
|
24
23
|
|
25
|
-
|
24
|
+
const zParsedPackageJson = z.object({
|
26
25
|
"name": z.string(),
|
27
26
|
"version": z.string().optional(),
|
28
27
|
"homepage": z.string().optional(),
|
29
28
|
"keycloakify": z
|
30
29
|
.object({
|
31
30
|
"extraThemeProperties": z.array(z.string()).optional(),
|
32
|
-
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
|
33
31
|
"artifactId": z.string().optional(),
|
34
32
|
"groupId": z.string().optional(),
|
35
33
|
"doCreateJar": z.boolean().optional(),
|
@@ -44,8 +42,8 @@ export const zParsedPackageJson = z.object({
|
|
44
42
|
|
45
43
|
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
|
46
44
|
|
47
|
-
let parsedPackageJson: undefined |
|
48
|
-
export function
|
45
|
+
let parsedPackageJson: undefined | ParsedPackageJson;
|
46
|
+
export function readParsedPackageJson(params: { reactAppRootDirPath: string }) {
|
49
47
|
const { reactAppRootDirPath } = params;
|
50
48
|
if (parsedPackageJson) {
|
51
49
|
return parsedPackageJson;
|
@@ -0,0 +1,85 @@
|
|
1
|
+
import * as fs from "fs";
|
2
|
+
import { assert } from "tsafe";
|
3
|
+
import type { Equals } from "tsafe";
|
4
|
+
import { z } from "zod";
|
5
|
+
import { join as pathJoin } from "path";
|
6
|
+
import { resolvedViteConfigJsonBasename } from "../../constants";
|
7
|
+
import type { OptionalIfCanBeUndefined } from "../../tools/OptionalIfCanBeUndefined";
|
8
|
+
import { getKeycloakifyBuildDirPath } from "./getKeycloakifyBuildDirPath";
|
9
|
+
|
10
|
+
export type ResolvedViteConfig = {
|
11
|
+
buildDir: string;
|
12
|
+
publicDir: string;
|
13
|
+
assetsDir: string;
|
14
|
+
urlPathname: string | undefined;
|
15
|
+
};
|
16
|
+
|
17
|
+
const zResolvedViteConfig = z.object({
|
18
|
+
"buildDir": z.string(),
|
19
|
+
"publicDir": z.string(),
|
20
|
+
"assetsDir": z.string(),
|
21
|
+
"urlPathname": z.string().optional()
|
22
|
+
});
|
23
|
+
|
24
|
+
{
|
25
|
+
type Got = ReturnType<(typeof zResolvedViteConfig)["parse"]>;
|
26
|
+
type Expected = OptionalIfCanBeUndefined<ResolvedViteConfig>;
|
27
|
+
|
28
|
+
assert<Equals<Got, Expected>>();
|
29
|
+
}
|
30
|
+
|
31
|
+
export function readResolvedViteConfig(params: {
|
32
|
+
reactAppRootDirPath: string;
|
33
|
+
parsedPackageJson_keycloakify_keycloakifyBuildDirPath: string | undefined;
|
34
|
+
}):
|
35
|
+
| {
|
36
|
+
resolvedViteConfig: ResolvedViteConfig;
|
37
|
+
}
|
38
|
+
| undefined {
|
39
|
+
const { reactAppRootDirPath, parsedPackageJson_keycloakify_keycloakifyBuildDirPath } = params;
|
40
|
+
|
41
|
+
const viteConfigTsFilePath = pathJoin(reactAppRootDirPath, "vite.config.ts");
|
42
|
+
|
43
|
+
if (!fs.existsSync(viteConfigTsFilePath)) {
|
44
|
+
return undefined;
|
45
|
+
}
|
46
|
+
|
47
|
+
const { keycloakifyBuildDirPath } = getKeycloakifyBuildDirPath({
|
48
|
+
reactAppRootDirPath,
|
49
|
+
parsedPackageJson_keycloakify_keycloakifyBuildDirPath,
|
50
|
+
"bundler": "vite"
|
51
|
+
});
|
52
|
+
|
53
|
+
const resolvedViteConfig = (() => {
|
54
|
+
const resolvedViteConfigJsonFilePath = pathJoin(keycloakifyBuildDirPath, resolvedViteConfigJsonBasename);
|
55
|
+
|
56
|
+
if (!fs.existsSync(resolvedViteConfigJsonFilePath)) {
|
57
|
+
throw new Error("Missing Keycloakify Vite plugin output.");
|
58
|
+
}
|
59
|
+
|
60
|
+
let out: ResolvedViteConfig;
|
61
|
+
|
62
|
+
try {
|
63
|
+
out = JSON.parse(fs.readFileSync(resolvedViteConfigJsonFilePath).toString("utf8"));
|
64
|
+
} catch {
|
65
|
+
throw new Error("The output of the Keycloakify Vite plugin is not a valid JSON.");
|
66
|
+
}
|
67
|
+
|
68
|
+
try {
|
69
|
+
const zodParseReturn = zResolvedViteConfig.parse(out);
|
70
|
+
|
71
|
+
// So that objectKeys from tsafe return the expected result no matter what.
|
72
|
+
Object.keys(zodParseReturn)
|
73
|
+
.filter(key => !(key in out))
|
74
|
+
.forEach(key => {
|
75
|
+
delete (out as any)[key];
|
76
|
+
});
|
77
|
+
} catch {
|
78
|
+
throw new Error("The output of the Keycloakify Vite plugin do not match the expected schema.");
|
79
|
+
}
|
80
|
+
|
81
|
+
return out;
|
82
|
+
})();
|
83
|
+
|
84
|
+
return { resolvedViteConfig };
|
85
|
+
}
|
@@ -1,18 +1,20 @@
|
|
1
1
|
import cheerio from "cheerio";
|
2
|
-
import {
|
2
|
+
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
3
3
|
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode";
|
4
4
|
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
|
5
5
|
import * as fs from "fs";
|
6
6
|
import { join as pathJoin } from "path";
|
7
7
|
import { objectKeys } from "tsafe/objectKeys";
|
8
|
-
import {
|
9
|
-
import type { BuildOptions } from "../BuildOptions";
|
8
|
+
import type { BuildOptions } from "../buildOptions";
|
10
9
|
import { assert } from "tsafe/assert";
|
11
|
-
import type
|
10
|
+
import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../constants";
|
12
11
|
|
13
12
|
export type BuildOptionsLike = {
|
13
|
+
bundler: "vite" | "webpack";
|
14
14
|
themeVersion: string;
|
15
15
|
urlPathname: string | undefined;
|
16
|
+
reactAppBuildDirPath: string;
|
17
|
+
assetsDirPath: string;
|
16
18
|
};
|
17
19
|
|
18
20
|
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
@@ -20,7 +22,6 @@ assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
|
20
22
|
export function generateFtlFilesCodeFactory(params: {
|
21
23
|
themeName: string;
|
22
24
|
indexHtmlCode: string;
|
23
|
-
//NOTE: Expected to be an empty object if external assets mode is enabled.
|
24
25
|
cssGlobalsToDefine: Record<string, string>;
|
25
26
|
buildOptions: BuildOptionsLike;
|
26
27
|
keycloakifyVersion: string;
|
@@ -37,7 +38,7 @@ export function generateFtlFilesCodeFactory(params: {
|
|
37
38
|
|
38
39
|
assert(jsCode !== null);
|
39
40
|
|
40
|
-
const { fixedJsCode } =
|
41
|
+
const { fixedJsCode } = replaceImportsInJsCode({ jsCode, buildOptions });
|
41
42
|
|
42
43
|
$(element).text(fixedJsCode);
|
43
44
|
});
|
@@ -70,7 +71,10 @@ export function generateFtlFilesCodeFactory(params: {
|
|
70
71
|
|
71
72
|
$(element).attr(
|
72
73
|
attrName,
|
73
|
-
href.replace(
|
74
|
+
href.replace(
|
75
|
+
new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`),
|
76
|
+
`\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
|
77
|
+
)
|
74
78
|
);
|
75
79
|
})
|
76
80
|
);
|
@@ -114,7 +118,7 @@ export function generateFtlFilesCodeFactory(params: {
|
|
114
118
|
$("head").prepend(
|
115
119
|
[
|
116
120
|
"<script>",
|
117
|
-
` window.${
|
121
|
+
` window.${nameOfTheGlobal}= ${objectKeys(replaceValueBySearchValue)[0]};`,
|
118
122
|
"</script>",
|
119
123
|
"",
|
120
124
|
objectKeys(replaceValueBySearchValue)[1]
|
@@ -0,0 +1,70 @@
|
|
1
|
+
import { assert } from "tsafe/assert";
|
2
|
+
import { Reflect } from "tsafe/Reflect";
|
3
|
+
import type { BuildOptions } from "./buildOptions";
|
4
|
+
|
5
|
+
type BuildOptionsLike = {
|
6
|
+
groupId: string;
|
7
|
+
artifactId: string;
|
8
|
+
themeVersion: string;
|
9
|
+
keycloakifyBuildDirPath: string;
|
10
|
+
};
|
11
|
+
|
12
|
+
{
|
13
|
+
const buildOptions = Reflect<BuildOptions>();
|
14
|
+
|
15
|
+
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
16
|
+
}
|
17
|
+
|
18
|
+
export function generatePom(params: { buildOptions: BuildOptionsLike }) {
|
19
|
+
const { buildOptions } = params;
|
20
|
+
|
21
|
+
const { pomFileCode } = (function generatePomFileCode(): {
|
22
|
+
pomFileCode: string;
|
23
|
+
} {
|
24
|
+
const pomFileCode = [
|
25
|
+
`<?xml version="1.0"?>`,
|
26
|
+
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
|
27
|
+
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
|
28
|
+
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
|
29
|
+
` <modelVersion>4.0.0</modelVersion>`,
|
30
|
+
` <groupId>${buildOptions.groupId}</groupId>`,
|
31
|
+
` <artifactId>${buildOptions.artifactId}</artifactId>`,
|
32
|
+
` <version>${buildOptions.themeVersion}</version>`,
|
33
|
+
` <name>${buildOptions.artifactId}</name>`,
|
34
|
+
` <description />`,
|
35
|
+
` <packaging>jar</packaging>`,
|
36
|
+
` <properties>`,
|
37
|
+
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
|
38
|
+
` </properties>`,
|
39
|
+
` <build>`,
|
40
|
+
` <plugins>`,
|
41
|
+
` <plugin>`,
|
42
|
+
` <groupId>org.apache.maven.plugins</groupId>`,
|
43
|
+
` <artifactId>maven-shade-plugin</artifactId>`,
|
44
|
+
` <version>3.5.1</version>`,
|
45
|
+
` <executions>`,
|
46
|
+
` <execution>`,
|
47
|
+
` <phase>package</phase>`,
|
48
|
+
` <goals>`,
|
49
|
+
` <goal>shade</goal>`,
|
50
|
+
` </goals>`,
|
51
|
+
` </execution>`,
|
52
|
+
` </executions>`,
|
53
|
+
` </plugin>`,
|
54
|
+
` </plugins>`,
|
55
|
+
` </build>`,
|
56
|
+
` <dependencies>`,
|
57
|
+
` <dependency>`,
|
58
|
+
` <groupId>io.phasetwo.keycloak</groupId>`,
|
59
|
+
` <artifactId>keycloak-account-v1</artifactId>`,
|
60
|
+
` <version>0.1</version>`,
|
61
|
+
` </dependency>`,
|
62
|
+
` </dependencies>`,
|
63
|
+
`</project>`
|
64
|
+
].join("\n");
|
65
|
+
|
66
|
+
return { pomFileCode };
|
67
|
+
})();
|
68
|
+
|
69
|
+
return { pomFileCode };
|
70
|
+
}
|
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
|
2
2
|
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
3
3
|
import { assert } from "tsafe/assert";
|
4
4
|
import { Reflect } from "tsafe/Reflect";
|
5
|
-
import type { BuildOptions } from "./
|
5
|
+
import type { BuildOptions } from "./buildOptions";
|
6
6
|
|
7
7
|
export type BuildOptionsLike = {
|
8
8
|
keycloakifyBuildDirPath: string;
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import * as fs from "fs";
|
2
|
+
import { join as pathJoin } from "path";
|
3
|
+
import { assert } from "tsafe/assert";
|
4
|
+
import { Reflect } from "tsafe/Reflect";
|
5
|
+
import type { BuildOptions } from "../buildOptions";
|
6
|
+
import { resources_common, lastKeycloakVersionWithAccountV1, accountV1ThemeName } from "../../constants";
|
7
|
+
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
|
8
|
+
import { transformCodebase } from "../../tools/transformCodebase";
|
9
|
+
import { rmSync } from "../../tools/fs.rmSync";
|
10
|
+
|
11
|
+
type BuildOptionsLike = {
|
12
|
+
keycloakifyBuildDirPath: string;
|
13
|
+
cacheDirPath: string;
|
14
|
+
};
|
15
|
+
|
16
|
+
{
|
17
|
+
const buildOptions = Reflect<BuildOptions>();
|
18
|
+
|
19
|
+
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
20
|
+
}
|
21
|
+
|
22
|
+
export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike }) {
|
23
|
+
const { buildOptions } = params;
|
24
|
+
|
25
|
+
const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "..", "tmp_yxdE2_builtin_keycloak_theme");
|
26
|
+
|
27
|
+
await downloadBuiltinKeycloakTheme({
|
28
|
+
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
29
|
+
"keycloakVersion": lastKeycloakVersionWithAccountV1,
|
30
|
+
buildOptions
|
31
|
+
});
|
32
|
+
|
33
|
+
const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1ThemeName, "account");
|
34
|
+
|
35
|
+
transformCodebase({
|
36
|
+
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"),
|
37
|
+
"destDirPath": accountV1DirPath
|
38
|
+
});
|
39
|
+
|
40
|
+
transformCodebase({
|
41
|
+
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources"),
|
42
|
+
"destDirPath": pathJoin(accountV1DirPath, "resources")
|
43
|
+
});
|
44
|
+
|
45
|
+
transformCodebase({
|
46
|
+
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources"),
|
47
|
+
"destDirPath": pathJoin(accountV1DirPath, "resources", resources_common)
|
48
|
+
});
|
49
|
+
|
50
|
+
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
|
51
|
+
|
52
|
+
fs.writeFileSync(
|
53
|
+
pathJoin(accountV1DirPath, "theme.properties"),
|
54
|
+
Buffer.from(
|
55
|
+
[
|
56
|
+
"accountResourceProvider=account-v1",
|
57
|
+
"",
|
58
|
+
"locales=ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
|
59
|
+
"",
|
60
|
+
"styles=" +
|
61
|
+
[
|
62
|
+
"css/account.css",
|
63
|
+
"img/icon-sidebar-active.png",
|
64
|
+
"img/logo.png",
|
65
|
+
...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(
|
66
|
+
fileBasename => `${resources_common}/node_modules/patternfly/dist/css/${fileBasename}`
|
67
|
+
)
|
68
|
+
].join(" "),
|
69
|
+
"",
|
70
|
+
"##### css classes for form buttons",
|
71
|
+
"# main class used for all buttons",
|
72
|
+
"kcButtonClass=btn",
|
73
|
+
"# classes defining priority of the button - primary or default (there is typically only one priority button for the form)",
|
74
|
+
"kcButtonPrimaryClass=btn-primary",
|
75
|
+
"kcButtonDefaultClass=btn-default",
|
76
|
+
"# classes defining size of the button",
|
77
|
+
"kcButtonLargeClass=btn-lg",
|
78
|
+
""
|
79
|
+
].join("\n"),
|
80
|
+
"utf8"
|
81
|
+
)
|
82
|
+
);
|
83
|
+
}
|
@@ -1,11 +1,11 @@
|
|
1
1
|
import { transformCodebase } from "../../tools/transformCodebase";
|
2
|
-
import
|
3
|
-
import { join as pathJoin, dirname as pathDirname } from "path";
|
2
|
+
import { join as pathJoin } from "path";
|
4
3
|
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
|
5
4
|
import { resources_common, type ThemeType } from "../../constants";
|
6
|
-
import { BuildOptions } from "../
|
5
|
+
import { BuildOptions } from "../buildOptions";
|
7
6
|
import { assert } from "tsafe/assert";
|
8
7
|
import * as crypto from "crypto";
|
8
|
+
import { rmSync } from "../../tools/fs.rmSync";
|
9
9
|
|
10
10
|
export type BuildOptionsLike = {
|
11
11
|
cacheDirPath: string;
|
@@ -13,45 +13,14 @@ export type BuildOptionsLike = {
|
|
13
13
|
|
14
14
|
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
15
15
|
|
16
|
-
export async function downloadKeycloakStaticResources(
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
usedResources: {
|
23
|
-
resourcesCommonFilePaths: string[];
|
24
|
-
} | undefined;
|
25
|
-
buildOptions: BuildOptionsLike;
|
26
|
-
}
|
27
|
-
) {
|
16
|
+
export async function downloadKeycloakStaticResources(params: {
|
17
|
+
themeType: ThemeType;
|
18
|
+
themeDirPath: string;
|
19
|
+
keycloakVersion: string;
|
20
|
+
buildOptions: BuildOptionsLike;
|
21
|
+
}) {
|
28
22
|
const { themeType, themeDirPath, keycloakVersion, buildOptions } = params;
|
29
23
|
|
30
|
-
// NOTE: Hack for 427
|
31
|
-
const usedResources = (() => {
|
32
|
-
const { usedResources } = params;
|
33
|
-
|
34
|
-
if (usedResources === undefined) {
|
35
|
-
return undefined;
|
36
|
-
}
|
37
|
-
|
38
|
-
assert(usedResources !== undefined);
|
39
|
-
|
40
|
-
return {
|
41
|
-
"resourcesCommonDirPaths": usedResources.resourcesCommonFilePaths.map(filePath => {
|
42
|
-
{
|
43
|
-
const splitArg = "/dist/";
|
44
|
-
|
45
|
-
if (filePath.includes(splitArg)) {
|
46
|
-
return filePath.split(splitArg)[0] + splitArg;
|
47
|
-
}
|
48
|
-
}
|
49
|
-
|
50
|
-
return pathDirname(filePath);
|
51
|
-
})
|
52
|
-
};
|
53
|
-
})();
|
54
|
-
|
55
24
|
const tmpDirPath = pathJoin(
|
56
25
|
themeDirPath,
|
57
26
|
`tmp_suLeKsxId_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}`
|
@@ -72,18 +41,8 @@ export async function downloadKeycloakStaticResources(
|
|
72
41
|
|
73
42
|
transformCodebase({
|
74
43
|
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
75
|
-
"destDirPath": pathJoin(resourcesPath, resources_common)
|
76
|
-
"transformSourceCode":
|
77
|
-
usedResources === undefined
|
78
|
-
? undefined
|
79
|
-
: ({ fileRelativePath, sourceCode }) => {
|
80
|
-
if (usedResources.resourcesCommonDirPaths.find(dirPath => fileRelativePath.startsWith(dirPath)) === undefined) {
|
81
|
-
return undefined;
|
82
|
-
}
|
83
|
-
|
84
|
-
return { "modifiedSourceCode": sourceCode };
|
85
|
-
}
|
44
|
+
"destDirPath": pathJoin(resourcesPath, resources_common)
|
86
45
|
});
|
87
46
|
|
88
|
-
|
47
|
+
rmSync(tmpDirPath, { "recursive": true, "force": true });
|
89
48
|
}
|