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:
Erik Jan de Wit 2024-07-29 13:47:32 +02:00 committed by GitHub
parent 557cf1e60e
commit afec4401fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 410 additions and 9 deletions

View 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
```

View 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();

View 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"
}
}

View file

@ -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>

View 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"
}
}

View file

@ -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: