Rely on @keycloakify/email-native for email initialization

This commit is contained in:
Joseph Garrone 2025-01-05 02:07:54 +01:00
parent 4845d7c32d
commit fb2d651a6f
6 changed files with 142 additions and 85 deletions

View File

@ -23,22 +23,6 @@ export async function command(params: { buildContext: BuildContext }) {
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account"); const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
if (
fs.existsSync(accountThemeSrcDirPath) &&
fs.readdirSync(accountThemeSrcDirPath).length > 0
) {
console.warn(
chalk.red(
`There is already a ${pathRelative(
process.cwd(),
accountThemeSrcDirPath
)} directory in your project. Aborting.`
)
);
process.exit(-1);
}
exitIfUncommittedChanges({ exitIfUncommittedChanges({
projectDirPath: buildContext.projectDirPath projectDirPath: buildContext.projectDirPath
}); });
@ -51,17 +35,35 @@ export async function command(params: { buildContext: BuildContext }) {
switch (accountThemeType) { switch (accountThemeType) {
case "Multi-Page": case "Multi-Page":
fs.cpSync( {
pathJoin( if (
getThisCodebaseRootDirPath(), fs.existsSync(accountThemeSrcDirPath) &&
"src", fs.readdirSync(accountThemeSrcDirPath).length > 0
"bin", ) {
"initialize-account-theme", console.warn(
"multi-page-boilerplate" chalk.red(
), `There is already a ${pathRelative(
accountThemeSrcDirPath, process.cwd(),
{ recursive: true } accountThemeSrcDirPath
); )} directory in your project. Aborting.`
)
);
process.exit(-1);
}
fs.cpSync(
pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"initialize-account-theme",
"multi-page-boilerplate"
),
accountThemeSrcDirPath,
{ recursive: true }
);
}
break; break;
case "Single-Page": case "Single-Page":
{ {

View File

@ -1,17 +1,24 @@
import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import type { BuildContext } from "./shared/buildContext"; import type { BuildContext } from "./shared/buildContext";
import * as fs from "fs"; import cliSelect from "cli-select";
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate"; import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { assert } from "tsafe/assert"; import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges";
import { getSupportedDockerImageTags } from "./start-keycloak/getSupportedDockerImageTags";
import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path";
import * as fs from "fs";
import { assert, is, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import { addSyncExtensionsToPostinstallScript } from "./shared/addSyncExtensionsToPostinstallScript";
import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier";
import { npmInstall } from "./tools/npmInstall";
import * as child_process from "child_process";
import { z } from "zod";
import chalk from "chalk";
export async function command(params: { buildContext: BuildContext }) { export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params; const { buildContext } = params;
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({ const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "initialize-email-theme", commandName: "initialize-account-theme",
buildContext buildContext
}); });
@ -19,6 +26,10 @@ export async function command(params: { buildContext: BuildContext }) {
return; return;
} }
exitIfUncommittedChanges({
projectDirPath: buildContext.projectDirPath
});
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email"); const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
if ( if (
@ -26,71 +37,110 @@ export async function command(params: { buildContext: BuildContext }) {
fs.readdirSync(emailThemeSrcDirPath).length > 0 fs.readdirSync(emailThemeSrcDirPath).length > 0
) { ) {
console.warn( console.warn(
`There is already a non empty ${pathRelative( chalk.red(
process.cwd(), `There is already a ${pathRelative(
emailThemeSrcDirPath process.cwd(),
)} directory in your project. Aborting.` emailThemeSrcDirPath
)} directory in your project. Aborting.`
)
); );
process.exit(-1); process.exit(-1);
} }
console.log("Initialize with the base email theme from which version of Keycloak?"); const { value: emailThemeType } = await cliSelect({
values: ["native (FreeMarker)" as const, "jsx-email (React)" as const]
}).catch(() => {
process.exit(-1);
});
const { extractedDirPath } = await downloadAndExtractArchive({ if (emailThemeType === "jsx-email (React)") {
url: await (async () => { console.log(
const { latestMajorTags } = await getSupportedDockerImageTags({ [
buildContext "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()
}); });
const keycloakVersion = latestMajorTags[0]; assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
assert(keycloakVersion !== undefined); return id<z.ZodType<TargetType>>(zTargetType);
})();
const parsedPackageJson = JSON.parse(
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
);
return `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`; zParsedPackageJson.parse(parsedPackageJson);
})(),
cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme",
onArchiveFile: async ({ fileRelativePath, writeFile }) => {
const fileRelativePath_target = pathRelative(
pathJoin("theme", "base", "email"),
fileRelativePath
);
if (fileRelativePath_target.startsWith("..")) { assert(is<ParsedPackageJson>(parsedPackageJson));
return;
}
await writeFile({ fileRelativePath: fileRelativePath_target }); return parsedPackageJson;
} })();
addSyncExtensionsToPostinstallScript({
parsedPackageJson,
buildContext
}); });
transformCodebase({ const moduleName = `@keycloakify/email-native`;
srcDirPath: extractedDirPath,
destDirPath: emailThemeSrcDirPath const [version] = (
}); JSON.parse(
child_process
.execSync(`npm show ${moduleName} versions --json`)
.toString("utf8")
.trim()
) as string[]
)
.reverse()
.filter(version => !version.includes("-"));
assert(version !== undefined);
(parsedPackageJson.dependencies ??= {})[moduleName] = `~${version}`;
if (parsedPackageJson.devDependencies !== undefined) {
delete parsedPackageJson.devDependencies[moduleName];
}
{ {
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties"); let sourceCode = JSON.stringify(parsedPackageJson, undefined, 2);
if (await getIsPrettierAvailable()) {
sourceCode = await runPrettier({
sourceCode,
filePath: buildContext.packageJsonFilePath
});
}
fs.writeFileSync( fs.writeFileSync(
themePropertyFilePath, buildContext.packageJsonFilePath,
Buffer.from( Buffer.from(sourceCode, "utf8")
[
`parent=base`,
fs.readFileSync(themePropertyFilePath).toString("utf8")
].join("\n"),
"utf8"
)
); );
} }
console.log( await npmInstall({
`The \`${pathJoin( packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
".", });
pathRelative(process.cwd(), emailThemeSrcDirPath)
)}\` directory have been created.` console.log(chalk.green("Email theme initialized."));
);
console.log("You can delete any file you don't modify.");
} }

View File

@ -1,6 +1,6 @@
import { dirname as pathDirname, relative as pathRelative, sep as pathSep } from "path"; import { dirname as pathDirname, relative as pathRelative, sep as pathSep } from "path";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { BuildContext } from "../buildContext"; import type { BuildContext } from "./buildContext";
export type BuildContextLike = { export type BuildContextLike = {
projectDirPath: string; projectDirPath: string;

View File

@ -1,5 +1,5 @@
import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path"; import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path";
import type { BuildContext } from "../buildContext"; import type { BuildContext } from "./buildContext";
import * as fs from "fs"; import * as fs from "fs";
import { assert, is, type Equals } from "tsafe/assert"; import { assert, is, type Equals } from "tsafe/assert";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
@ -7,8 +7,8 @@ import {
addSyncExtensionsToPostinstallScript, addSyncExtensionsToPostinstallScript,
type BuildContextLike as BuildContextLike_addSyncExtensionsToPostinstallScript type BuildContextLike as BuildContextLike_addSyncExtensionsToPostinstallScript
} from "./addSyncExtensionsToPostinstallScript"; } from "./addSyncExtensionsToPostinstallScript";
import { getIsPrettierAvailable, runPrettier } from "../../tools/runPrettier"; import { getIsPrettierAvailable, runPrettier } from "../tools/runPrettier";
import { npmInstall } from "../../tools/npmInstall"; import { npmInstall } from "../tools/npmInstall";
import * as child_process from "child_process"; import * as child_process from "child_process";
import { z } from "zod"; import { z } from "zod";
import chalk from "chalk"; import chalk from "chalk";

View File

@ -1 +0,0 @@
export * from "./initializeSpa";

View File

@ -99,6 +99,12 @@ function addCommentToSourceCode(params: {
return toResult(commentLines.map(line => `# ${line}`).join("\n")); return toResult(commentLines.map(line => `# ${line}`).join("\n"));
} }
if (fileRelativePath.endsWith(".ftl")) {
return toResult(
[`<#--`, ...commentLines.map(line => ` ${line}`), `-->`].join("\n")
);
}
if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) { if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) {
const comment = [ const comment = [
`<!--`, `<!--`,