initial version of the kc-create tool (#31314)
* initial version of the kc-create tool Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * Update js/apps/kc-create/package.json Co-authored-by: Jon Koops <jonkoops@gmail.com> Signed-off-by: Erik Jan de Wit <edewit@redhat.com> * renamed and addressed review Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> --------- Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> Signed-off-by: Erik Jan de Wit <edewit@redhat.com> Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
parent
557cf1e60e
commit
afec4401fc
6 changed files with 410 additions and 9 deletions
38
js/apps/create-keycloak-theme/README.md
Normal file
38
js/apps/create-keycloak-theme/README.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
=== kc-create
|
||||
|
||||
Create a new Keycloak ui project based on a template
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
npm create keycloak-theme <name> [options]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
* `-t, --type <name>` the type of ui to be created either account or admin (currently only account is supported)
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
npm create keycloak-theme my-project -t account
|
||||
```
|
||||
|
||||
This will create a new project called `my-project` with an account ui based on the template from the quickstarts repo.
|
||||
After the project is created, the following commands can be used to start the server and open the ui in a browser:
|
||||
|
||||
```bash
|
||||
cd my-project
|
||||
npm run dev
|
||||
```
|
||||
And then run keycloak in the background:
|
||||
|
||||
```bash
|
||||
npm run start-keycloak
|
||||
```
|
||||
|
||||
Then open the ui in a browser:
|
||||
|
||||
```bash
|
||||
open http://localhost:8080
|
||||
```
|
127
js/apps/create-keycloak-theme/create.js
Executable file
127
js/apps/create-keycloak-theme/create.js
Executable file
|
@ -0,0 +1,127 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import chalk from "chalk";
|
||||
import { Command } from "commander";
|
||||
import fs from "fs-extra";
|
||||
import Mustache from "mustache";
|
||||
import { mkdtemp } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve, dirname } from "node:path";
|
||||
import { simpleGit } from "simple-git";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf8"));
|
||||
|
||||
function main() {
|
||||
new Command(packageJson.name)
|
||||
.version(packageJson.version)
|
||||
.description(packageJson.description)
|
||||
.arguments("<name>")
|
||||
.usage(`${chalk.green("<name>")} [options]`)
|
||||
.option(
|
||||
"-t, --type <name>",
|
||||
"the type of ui to be created either `account` or `admin` ",
|
||||
"account",
|
||||
)
|
||||
.action(async (name, options) => {
|
||||
console.log(`Creating a new ${chalk.green(name)} project`);
|
||||
|
||||
await createProject(name, options.type);
|
||||
done(name);
|
||||
})
|
||||
.on("--help", () => {
|
||||
console.log();
|
||||
console.log(` Only ${chalk.green("<name>")} is required.`);
|
||||
console.log();
|
||||
console.log(` Type ${chalk.blue("--type")} can be one of:`);
|
||||
console.log(
|
||||
` - ${chalk.green("account")} for an account ui based ui`,
|
||||
);
|
||||
console.log(` - ${chalk.green("admin")} for an admin ui based ui`);
|
||||
console.log();
|
||||
})
|
||||
.parse(process.argv);
|
||||
}
|
||||
|
||||
function cloneQuickstart() {
|
||||
return new Promise((resolve, reject) => {
|
||||
mkdtemp(join(tmpdir(), "template-"), async (err, dir) => {
|
||||
if (err) return reject(err);
|
||||
simpleGit()
|
||||
.clone("https://github.com/keycloak/keycloak-quickstarts", dir, {
|
||||
"--single-branch": undefined,
|
||||
"--branch": "main",
|
||||
})
|
||||
.then(() => resolve(join(dir, "extension/extend-admin-console-node")));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createProject(name, type) {
|
||||
const templateProjectDir = await cloneQuickstart();
|
||||
const projectDir = join(resolve(), name);
|
||||
await fs.mkdir(projectDir);
|
||||
await fs.copy(templateProjectDir, projectDir);
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const templateDir = join(dirname(filename), "templates");
|
||||
const templateFiles = await searchFile(templateDir, "mu");
|
||||
templateFiles.forEach(async (file) => {
|
||||
const dest = file.substring(templateDir.length, file.length - 3);
|
||||
const destPath = join(projectDir, dest);
|
||||
const contents = await fs.readFile(file, "utf8");
|
||||
const data = Mustache.render(contents, {
|
||||
name,
|
||||
type,
|
||||
version: packageJson.version,
|
||||
});
|
||||
await fs.writeFile(destPath, data);
|
||||
});
|
||||
}
|
||||
|
||||
async function searchFile(dir, fileName) {
|
||||
const result = [];
|
||||
const files = await fs.readdir(dir);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(dir, file);
|
||||
const fileStat = await fs.stat(filePath);
|
||||
|
||||
if (fileStat.isDirectory()) {
|
||||
result.push(...(await searchFile(filePath, fileName)));
|
||||
} else if (file.endsWith(fileName)) {
|
||||
result.push(filePath);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function done(appName) {
|
||||
console.log();
|
||||
console.log(`Success! Created ${appName} at ./${appName}`);
|
||||
console.log("Inside that directory, you can run several commands:");
|
||||
console.log();
|
||||
console.log(chalk.cyan(` npm run start-keycloak`));
|
||||
console.log(" Downloads and starts a keycloak server.");
|
||||
console.log();
|
||||
console.log(chalk.cyan(` npm run dev`));
|
||||
console.log(" Starts development server.");
|
||||
console.log();
|
||||
console.log(chalk.cyan(` mvn install`));
|
||||
console.log(
|
||||
" Bundles the app into a jar file that can be deployed to a keycloak ",
|
||||
);
|
||||
console.log(
|
||||
` server by putting it in the ${chalk.green("providers")} directory.`,
|
||||
);
|
||||
console.log();
|
||||
console.log("We suggest that you begin by typing:");
|
||||
console.log();
|
||||
console.log(chalk.cyan(" cd"), appName);
|
||||
console.log(` ${chalk.cyan(`npm run start-keycloak &`)}`);
|
||||
console.log();
|
||||
console.log(` ${chalk.cyan(`npm run dev`)}`);
|
||||
console.log();
|
||||
console.log("👾 🚀 Happy hacking!");
|
||||
}
|
||||
|
||||
main();
|
20
js/apps/create-keycloak-theme/package.json
Normal file
20
js/apps/create-keycloak-theme/package.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "create-keycloak-theme",
|
||||
"version": "999.0.0-SNAPSHOT",
|
||||
"description": "Create a new Keycloak ui project based on a template",
|
||||
"main": "create.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node create.js"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.1.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"mustache": "^4.2.0",
|
||||
"simple-git": "^3.25.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
<!doctype html>
|
||||
<html lang="${locale}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<base href="${resourceUrl}/">
|
||||
<link rel="icon" type="${properties.favIconType!'image/svg+xml'}" href="${resourceUrl}${properties.favIcon!'/favicon.svg'}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="${properties.description!'The {{type}} ui is a web-based management interface.'}">
|
||||
<title>${properties.title!'{{type}} Management'}</title>
|
||||
<style>
|
||||
.keycloak__loading-container {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#loading-text {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 5px solid #FFF;
|
||||
border-bottom-color: #06c;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react": "${resourceCommonUrl}/vendor/react/react.production.min.js",
|
||||
"react/jsx-runtime": "${resourceCommonUrl}/vendor/react/react-jsx-runtime.production.min.js",
|
||||
"react-dom": "${resourceCommonUrl}/vendor/react-dom/react-dom.production.min.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<#if devServerUrl?has_content>
|
||||
<script type="module">
|
||||
import { injectIntoGlobalHook } from "${devServerUrl}/@react-refresh";
|
||||
|
||||
injectIntoGlobalHook(window);
|
||||
window.$RefreshReg$ = () => {};
|
||||
window.$RefreshSig$ = () => (type) => type;
|
||||
</script>
|
||||
<script type="module">
|
||||
import { inject } from "${devServerUrl}/@vite-plugin-checker-runtime";
|
||||
|
||||
inject({
|
||||
overlayConfig: {},
|
||||
base: "/",
|
||||
});
|
||||
</script>
|
||||
<script type="module" src="${devServerUrl}/@vite/client"></script>
|
||||
<script type="module" src="${devServerUrl}/src/main.tsx"></script>
|
||||
</#if>
|
||||
<#if entryStyles?has_content>
|
||||
<#list entryStyles as style>
|
||||
<link rel="stylesheet" href="${resourceUrl}/${style}">
|
||||
</#list>
|
||||
</#if>
|
||||
<#if properties.styles?has_content>
|
||||
<#list properties.styles?split(' ') as style>
|
||||
<link rel="stylesheet" href="${resourceUrl}/${style}">
|
||||
</#list>
|
||||
</#if>
|
||||
<#if entryScript?has_content>
|
||||
<script type="module" src="${resourceUrl}/${entryScript}"></script>
|
||||
</#if>
|
||||
<#if properties.scripts?has_content>
|
||||
<#list properties.scripts?split(' ') as script>
|
||||
<script type="module" src="${resourceUrl}/${script}"></script>
|
||||
</#list>
|
||||
</#if>
|
||||
<#if entryImports?has_content>
|
||||
<#list entryImports as import>
|
||||
<link rel="modulepreload" href="${resourceUrl}/${import}">
|
||||
</#list>
|
||||
</#if>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="keycloak__loading-container">
|
||||
<span class="loader" role="progressbar" aria-valuetext="Loading...">
|
||||
</span>
|
||||
<div>
|
||||
<p id="loading-text">Loading the {{type}} ui</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<noscript>JavaScript is required to use the {{type}} ui.</noscript>
|
||||
<script id="environment" type="application/json">
|
||||
{
|
||||
"authUrl": "${authUrl}",
|
||||
"authServerUrl": "${authServerUrl}",
|
||||
"realm": "${realm.name}",
|
||||
"clientId": "${clientId}",
|
||||
"resourceUrl": "${resourceUrl}",
|
||||
"logo": "${properties.logo!""}",
|
||||
"logoUrl": "${properties.logoUrl!""}",
|
||||
"baseUrl": "${baseUrl}",
|
||||
"locale": "${locale}",
|
||||
"referrerName": "${referrerName!""}",
|
||||
"referrerUrl": "${referrer_uri!""}",
|
||||
"features": {
|
||||
"isRegistrationEmailAsUsername": ${realm.registrationEmailAsUsername?c},
|
||||
"isEditUserNameAllowed": ${realm.editUsernameAllowed?c},
|
||||
"isInternationalizationEnabled": ${realm.isInternationalizationEnabled()?c},
|
||||
"isLinkedAccountsEnabled": ${realm.identityFederationEnabled?c},
|
||||
"isMyResourcesEnabled": ${(realm.userManagedAccessAllowed && isAuthorizationEnabled)?c},
|
||||
"deleteAccountAllowed": ${deleteAccountAllowed?c},
|
||||
"updateEmailFeatureEnabled": ${updateEmailFeatureEnabled?c},
|
||||
"updateEmailActionEnabled": ${updateEmailActionEnabled?c},
|
||||
"isViewGroupsEnabled": ${isViewGroupsEnabled?c},
|
||||
"isOid4VciEnabled": ${isOid4VciEnabled?c}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
35
js/apps/create-keycloak-theme/templates/package.json.mu
Normal file
35
js/apps/create-keycloak-theme/templates/package.json.mu
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "{{name}}",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"start-keycloak": "node ./start-server.js --account-dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@keycloak/keycloak-account-ui": "{{version}}",
|
||||
"@keycloak/keycloak-ui-shared": "{{version}}",
|
||||
"@patternfly/react-core": "5.0.0",
|
||||
"i18next": "^23.10.1",
|
||||
"i18next-http-backend": "^2.5.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-router-dom": "^6.23.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@keycloak/keycloak-admin-client": "{{version}}",
|
||||
"@octokit/rest": "^20.1.1",
|
||||
"@types/react": "^18.2.67",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"gunzip-maybe": "^1.4.2",
|
||||
"tar-fs": "^3.0.6",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.2.2",
|
||||
"vite-plugin-checker": "^0.6.4"
|
||||
}
|
||||
}
|
|
@ -299,6 +299,24 @@ importers:
|
|||
specifier: ^2.0.4
|
||||
version: 2.0.4(@types/node@20.14.12)(jsdom@24.1.1)(lightningcss@1.25.1)(terser@5.31.0)
|
||||
|
||||
js/apps/create-keycloak-theme:
|
||||
dependencies:
|
||||
chalk:
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
commander:
|
||||
specifier: ^12.1.0
|
||||
version: 12.1.0
|
||||
fs-extra:
|
||||
specifier: ^11.2.0
|
||||
version: 11.2.0
|
||||
mustache:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
simple-git:
|
||||
specifier: ^3.25.0
|
||||
version: 3.25.0
|
||||
|
||||
js/apps/keycloak-server:
|
||||
dependencies:
|
||||
'@octokit/rest':
|
||||
|
@ -1054,6 +1072,12 @@ packages:
|
|||
'@keycloak/keycloak-admin-ui@file:js/apps/admin-ui':
|
||||
resolution: {directory: js/apps/admin-ui, type: directory}
|
||||
|
||||
'@kwsites/file-exists@1.1.1':
|
||||
resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==}
|
||||
|
||||
'@kwsites/promise-deferred@1.1.1':
|
||||
resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==}
|
||||
|
||||
'@microsoft/api-extractor-model@7.28.13':
|
||||
resolution: {integrity: sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==}
|
||||
|
||||
|
@ -1727,9 +1751,6 @@ packages:
|
|||
'@types/mocha@10.0.7':
|
||||
resolution: {integrity: sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==}
|
||||
|
||||
'@types/node@20.14.11':
|
||||
resolution: {integrity: sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==}
|
||||
|
||||
'@types/node@20.14.12':
|
||||
resolution: {integrity: sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==}
|
||||
|
||||
|
@ -3736,6 +3757,10 @@ packages:
|
|||
muggle-string@0.3.1:
|
||||
resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==}
|
||||
|
||||
mustache@4.2.0:
|
||||
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||
hasBin: true
|
||||
|
||||
nanoid@3.3.7:
|
||||
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
|
@ -4310,6 +4335,9 @@ packages:
|
|||
resolution: {integrity: sha512-0LxHn+P1lF5r2WwVB/za3hLRIsYoLaNq1CXqjbrs3ZvLuvlWnRKrUjEWzV7umZL7hpQ7xULiQMV+0iXdRa5iFg==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
simple-git@3.25.0:
|
||||
resolution: {integrity: sha512-KIY5sBnzc4yEcJXW7Tdv4viEz8KyG+nU0hay+DWZasvdFOYKeUZ6Xc25LUHHjw0tinPT7O1eY6pzX7pRT1K8rw==}
|
||||
|
||||
slash@3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -5576,6 +5604,14 @@ snapshots:
|
|||
- immer
|
||||
- react-native
|
||||
|
||||
'@kwsites/file-exists@1.1.1':
|
||||
dependencies:
|
||||
debug: 4.3.5(supports-color@8.1.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@kwsites/promise-deferred@1.1.1': {}
|
||||
|
||||
'@microsoft/api-extractor-model@7.28.13(@types/node@20.14.12)':
|
||||
dependencies:
|
||||
'@microsoft/tsdoc': 0.14.2
|
||||
|
@ -6256,7 +6292,7 @@ snapshots:
|
|||
|
||||
'@types/gunzip-maybe@1.4.2':
|
||||
dependencies:
|
||||
'@types/node': 20.14.11
|
||||
'@types/node': 20.14.12
|
||||
|
||||
'@types/lodash-es@4.17.12':
|
||||
dependencies:
|
||||
|
@ -6266,10 +6302,6 @@ snapshots:
|
|||
|
||||
'@types/mocha@10.0.7': {}
|
||||
|
||||
'@types/node@20.14.11':
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/node@20.14.12':
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
@ -6293,7 +6325,7 @@ snapshots:
|
|||
|
||||
'@types/tar-fs@2.0.4':
|
||||
dependencies:
|
||||
'@types/node': 20.14.11
|
||||
'@types/node': 20.14.12
|
||||
'@types/tar-stream': 3.1.3
|
||||
|
||||
'@types/tar-stream@3.1.3':
|
||||
|
@ -8577,6 +8609,8 @@ snapshots:
|
|||
|
||||
muggle-string@0.3.1: {}
|
||||
|
||||
mustache@4.2.0: {}
|
||||
|
||||
nanoid@3.3.7: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
@ -9178,6 +9212,14 @@ snapshots:
|
|||
|
||||
simple-bin-help@1.8.0: {}
|
||||
|
||||
simple-git@3.25.0:
|
||||
dependencies:
|
||||
'@kwsites/file-exists': 1.1.1
|
||||
'@kwsites/promise-deferred': 1.1.1
|
||||
debug: 4.3.5(supports-color@8.1.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
slice-ansi@3.0.0:
|
||||
|
|
Loading…
Reference in a new issue