Better naming convention 'uiModules' -> 'extensionModules'

This commit is contained in:
Joseph Garrone 2024-12-24 16:43:42 +01:00
parent d2da43c617
commit c1dc899bc1
12 changed files with 191 additions and 142 deletions

View File

@ -4,7 +4,7 @@ import * as fs from "fs";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate"; import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
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";
import { addPostinstallScriptIfNotPresent } from "./shared/addPostinstallScriptIfNotPresent"; import { addSyncExtensionsToPostinstallScript } from "./shared/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";
@ -74,7 +74,7 @@ export async function command(params: { buildContext: BuildContext }) {
return parsedPackageJson; return parsedPackageJson;
})(); })();
addPostinstallScriptIfNotPresent({ addSyncExtensionsToPostinstallScript({
parsedPackageJson, parsedPackageJson,
buildContext buildContext
}); });

View File

@ -248,13 +248,30 @@ program
program program
.command({ .command({
name: "postinstall", name: "sync-extensions",
description: "Initialize all the Keycloakify UI modules installed in the project." 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")
}) })
.task({ .task({
skip, skip,
handler: async ({ projectDirPath }) => { handler: async ({ projectDirPath }) => {
const { command } = await import("./postinstall"); const { command } = await import("./sync-extensions");
await command({ buildContext: getBuildContext({ projectDirPath }) }); await command({ buildContext: getBuildContext({ projectDirPath }) });
} }
@ -267,9 +284,20 @@ program
}>({ }>({
name: "own", name: "own",
description: [ description: [
"WARNING: Not usable yet, will be used for future features", "Manages ownership of auto-generated files provided by Keycloakify extensions.",
"Take ownership over a given file" "",
].join(" ") "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({ .option({
key: "path", key: "path",
@ -282,9 +310,9 @@ program
return { long, short }; return { long, short };
})(), })(),
description: [ description: [
"Relative path of the file or the directory that you want to take ownership over.", "Specifies the relative path of the file or directory to take ownership of.",
"The path is relative to your theme directory.", "This path should be relative to your theme directory.",
"Example `--path admin/page/Login.tsx`" "Example: `--path 'admin/KcPage.tsx'`"
].join(" ") ].join(" ")
}) })
.option({ .option({
@ -296,7 +324,10 @@ program
return name; return name;
})(), })(),
description: "Revert ownership claim over a given file or directory.", description: [
"Restores a file or directory to its original auto-generated state,",
"removing your ownership claim and reverting any modifications."
].join(" "),
defaultValue: false defaultValue: false
}) })
.task({ .task({

View File

@ -1,18 +1,18 @@
import type { BuildContext } from "./shared/buildContext"; import type { BuildContext } from "./shared/buildContext";
import { getUiModuleFileSourceCodeReadyToBeCopied } from "./postinstall/getUiModuleFileSourceCodeReadyToBeCopied"; import { getExtensionModuleFileSourceCodeReadyToBeCopied } from "./sync-extensions/getExtensionModuleFileSourceCodeReadyToBeCopied";
import { getAbsoluteAndInOsFormatPath } from "./tools/getAbsoluteAndInOsFormatPath"; import type { ExtensionModuleMeta } from "./sync-extensions/extensionModuleMeta";
import { relative as pathRelative, dirname as pathDirname, join as pathJoin } from "path"; import { command as command_syncExtensions } from "./sync-extensions/sync-extension";
import { getUiModuleMetas } from "./postinstall/uiModuleMeta";
import { getInstalledModuleDirPath } from "./tools/getInstalledModuleDirPath";
import * as fsPr from "fs/promises";
import { import {
readManagedGitignoreFile, readManagedGitignoreFile,
writeManagedGitignoreFile writeManagedGitignoreFile
} from "./postinstall/managedGitignoreFile"; } 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 { isInside } from "./tools/isInside";
import chalk from "chalk"; import chalk from "chalk";
import type { UiModuleMeta } from "./postinstall/uiModuleMeta";
import { command as command_postinstall } from "./postinstall";
export async function command(params: { export async function command(params: {
buildContext: BuildContext; buildContext: BuildContext;
@ -23,9 +23,9 @@ export async function command(params: {
}) { }) {
const { buildContext, cliCommandOptions } = params; const { buildContext, cliCommandOptions } = params;
const uiModuleMetas = await getUiModuleMetas({ buildContext }); const extensionModuleMetas = await getExtensionModuleMetas({ buildContext });
const { targetFileRelativePathsByUiModuleMeta } = await (async () => { const { targetFileRelativePathsByExtensionModuleMeta } = await (async () => {
const fileOrDirectoryRelativePath = pathRelative( const fileOrDirectoryRelativePath = pathRelative(
buildContext.themeSrcDirPath, buildContext.themeSrcDirPath,
getAbsoluteAndInOsFormatPath({ getAbsoluteAndInOsFormatPath({
@ -34,10 +34,10 @@ export async function command(params: {
}) })
); );
const arr = uiModuleMetas const arr = extensionModuleMetas
.map(uiModuleMeta => ({ .map(extensionModuleMeta => ({
uiModuleMeta, extensionModuleMeta,
fileRelativePaths: uiModuleMeta.files fileRelativePaths: extensionModuleMeta.files
.map(({ fileRelativePath }) => fileRelativePath) .map(({ fileRelativePath }) => fileRelativePath)
.filter( .filter(
fileRelativePath => fileRelativePath =>
@ -50,18 +50,26 @@ export async function command(params: {
})) }))
.filter(({ fileRelativePaths }) => fileRelativePaths.length !== 0); .filter(({ fileRelativePaths }) => fileRelativePaths.length !== 0);
const targetFileRelativePathsByUiModuleMeta = new Map<UiModuleMeta, string[]>(); const targetFileRelativePathsByExtensionModuleMeta = new Map<
ExtensionModuleMeta,
string[]
>();
for (const { uiModuleMeta, fileRelativePaths } of arr) { for (const { extensionModuleMeta, fileRelativePaths } of arr) {
targetFileRelativePathsByUiModuleMeta.set(uiModuleMeta, fileRelativePaths); targetFileRelativePathsByExtensionModuleMeta.set(
extensionModuleMeta,
fileRelativePaths
);
} }
return { targetFileRelativePathsByUiModuleMeta }; return { targetFileRelativePathsByExtensionModuleMeta };
})(); })();
if (targetFileRelativePathsByUiModuleMeta.size === 0) { if (targetFileRelativePathsByExtensionModuleMeta.size === 0) {
console.log( console.log(
chalk.yellow("There is no UI module files matching the provided path.") chalk.yellow(
"There is no Keycloakify extension modules files matching the provided path."
)
); );
process.exit(1); process.exit(1);
} }
@ -72,34 +80,34 @@ export async function command(params: {
}); });
await (cliCommandOptions.isRevert ? command_revert : command_own)({ await (cliCommandOptions.isRevert ? command_revert : command_own)({
uiModuleMetas, extensionModuleMetas,
targetFileRelativePathsByUiModuleMeta, targetFileRelativePathsByExtensionModuleMeta,
ownedFilesRelativePaths_current, ownedFilesRelativePaths_current,
buildContext buildContext
}); });
} }
type Params_subcommands = { type Params_subcommands = {
uiModuleMetas: UiModuleMeta[]; extensionModuleMetas: ExtensionModuleMeta[];
targetFileRelativePathsByUiModuleMeta: Map<UiModuleMeta, string[]>; targetFileRelativePathsByExtensionModuleMeta: Map<ExtensionModuleMeta, string[]>;
ownedFilesRelativePaths_current: string[]; ownedFilesRelativePaths_current: string[];
buildContext: BuildContext; buildContext: BuildContext;
}; };
async function command_own(params: Params_subcommands) { async function command_own(params: Params_subcommands) {
const { const {
uiModuleMetas, extensionModuleMetas,
targetFileRelativePathsByUiModuleMeta, targetFileRelativePathsByExtensionModuleMeta,
ownedFilesRelativePaths_current, ownedFilesRelativePaths_current,
buildContext buildContext
} = params; } = params;
await writeManagedGitignoreFile({ await writeManagedGitignoreFile({
buildContext, buildContext,
uiModuleMetas, extensionModuleMetas,
ownedFilesRelativePaths: [ ownedFilesRelativePaths: [
...ownedFilesRelativePaths_current, ...ownedFilesRelativePaths_current,
...Array.from(targetFileRelativePathsByUiModuleMeta.values()) ...Array.from(targetFileRelativePathsByExtensionModuleMeta.values())
.flat() .flat()
.filter( .filter(
fileRelativePath => fileRelativePath =>
@ -111,11 +119,11 @@ async function command_own(params: Params_subcommands) {
const writeActions: (() => Promise<void>)[] = []; const writeActions: (() => Promise<void>)[] = [];
for (const [ for (const [
uiModuleMeta, extensionModuleMeta,
fileRelativePaths fileRelativePaths
] of targetFileRelativePathsByUiModuleMeta.entries()) { ] of targetFileRelativePathsByExtensionModuleMeta.entries()) {
const uiModuleDirPath = await getInstalledModuleDirPath({ const extensionModuleDirPath = await getInstalledModuleDirPath({
moduleName: uiModuleMeta.moduleName, moduleName: extensionModuleMeta.moduleName,
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath), packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath),
projectDirPath: buildContext.projectDirPath projectDirPath: buildContext.projectDirPath
}); });
@ -129,13 +137,13 @@ async function command_own(params: Params_subcommands) {
} }
writeActions.push(async () => { writeActions.push(async () => {
const sourceCode = await getUiModuleFileSourceCodeReadyToBeCopied({ const sourceCode = await getExtensionModuleFileSourceCodeReadyToBeCopied({
buildContext, buildContext,
fileRelativePath, fileRelativePath,
isOwnershipAction: true, isOwnershipAction: true,
uiModuleName: uiModuleMeta.moduleName, extensionModuleName: extensionModuleMeta.moduleName,
uiModuleDirPath, extensionModuleDirPath,
uiModuleVersion: uiModuleMeta.version extensionModuleVersion: extensionModuleMeta.version
}); });
await fsPr.writeFile( await fsPr.writeFile(
@ -158,14 +166,14 @@ async function command_own(params: Params_subcommands) {
async function command_revert(params: Params_subcommands) { async function command_revert(params: Params_subcommands) {
const { const {
uiModuleMetas, extensionModuleMetas,
targetFileRelativePathsByUiModuleMeta, targetFileRelativePathsByExtensionModuleMeta,
ownedFilesRelativePaths_current, ownedFilesRelativePaths_current,
buildContext buildContext
} = params; } = params;
const ownedFilesRelativePaths_toRemove = Array.from( const ownedFilesRelativePaths_toRemove = Array.from(
targetFileRelativePathsByUiModuleMeta.values() targetFileRelativePathsByExtensionModuleMeta.values()
) )
.flat() .flat()
.filter(fileRelativePath => { .filter(fileRelativePath => {
@ -190,12 +198,12 @@ async function command_revert(params: Params_subcommands) {
await writeManagedGitignoreFile({ await writeManagedGitignoreFile({
buildContext, buildContext,
uiModuleMetas, extensionModuleMetas,
ownedFilesRelativePaths: ownedFilesRelativePaths_current.filter( ownedFilesRelativePaths: ownedFilesRelativePaths_current.filter(
fileRelativePath => fileRelativePath =>
!ownedFilesRelativePaths_toRemove.includes(fileRelativePath) !ownedFilesRelativePaths_toRemove.includes(fileRelativePath)
) )
}); });
await command_postinstall({ buildContext }); await command_syncExtensions({ buildContext });
} }

View File

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

View File

@ -9,13 +9,13 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export function addPostinstallScriptIfNotPresent(params: { export function addSyncExtensionsToPostinstallScript(params: {
parsedPackageJson: { scripts?: Record<string, string | undefined> }; parsedPackageJson: { scripts?: Record<string, string | undefined> };
buildContext: BuildContextLike; buildContext: BuildContextLike;
}) { }) {
const { parsedPackageJson, buildContext } = params; const { parsedPackageJson, buildContext } = params;
const cmd_base = "keycloakify postinstall"; const cmd_base = "keycloakify sync-extensions";
const projectCliOptionValue = (() => { const projectCliOptionValue = (() => {
const packageJsonDirPath = pathDirname(buildContext.packageJsonFilePath); const packageJsonDirPath = pathDirname(buildContext.packageJsonFilePath);

View File

@ -10,15 +10,15 @@ import { crawlAsync } from "../tools/crawlAsync";
import { getIsPrettierAvailable, getPrettier } from "../tools/runPrettier"; import { getIsPrettierAvailable, getPrettier } from "../tools/runPrettier";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion"; import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import { import {
getUiModuleFileSourceCodeReadyToBeCopied, getExtensionModuleFileSourceCodeReadyToBeCopied,
type BuildContextLike as BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied type BuildContextLike as BuildContextLike_getExtensionModuleFileSourceCodeReadyToBeCopied
} from "./getUiModuleFileSourceCodeReadyToBeCopied"; } from "./getExtensionModuleFileSourceCodeReadyToBeCopied";
import * as crypto from "crypto"; import * as crypto from "crypto";
import { KEYCLOAK_THEME } from "../shared/constants"; import { KEYCLOAK_THEME } from "../shared/constants";
import { exclude } from "tsafe/exclude"; import { exclude } from "tsafe/exclude";
import { isAmong } from "tsafe/isAmong"; import { isAmong } from "tsafe/isAmong";
export type UiModuleMeta = { export type ExtensionModuleMeta = {
moduleName: string; moduleName: string;
version: string; version: string;
files: { files: {
@ -29,8 +29,8 @@ export type UiModuleMeta = {
peerDependencies: Record<string, string>; peerDependencies: Record<string, string>;
}; };
const zUiModuleMeta = (() => { const zExtensionModuleMeta = (() => {
type ExpectedType = UiModuleMeta; type ExpectedType = ExtensionModuleMeta;
const zTargetType = z.object({ const zTargetType = z.object({
moduleName: z.string(), moduleName: z.string(),
@ -56,7 +56,7 @@ type ParsedCacheFile = {
keycloakifyVersion: string; keycloakifyVersion: string;
prettierConfigHash: string | null; prettierConfigHash: string | null;
thisFilePath: string; thisFilePath: string;
uiModuleMetas: UiModuleMeta[]; extensionModuleMetas: ExtensionModuleMeta[];
}; };
const zParsedCacheFile = (() => { const zParsedCacheFile = (() => {
@ -66,7 +66,7 @@ const zParsedCacheFile = (() => {
keycloakifyVersion: z.string(), keycloakifyVersion: z.string(),
prettierConfigHash: z.union([z.string(), z.null()]), prettierConfigHash: z.union([z.string(), z.null()]),
thisFilePath: z.string(), thisFilePath: z.string(),
uiModuleMetas: z.array(zUiModuleMeta) extensionModuleMetas: z.array(zExtensionModuleMeta)
}); });
type InferredType = z.infer<typeof zTargetType>; type InferredType = z.infer<typeof zTargetType>;
@ -76,10 +76,10 @@ const zParsedCacheFile = (() => {
return id<z.ZodType<ExpectedType>>(zTargetType); return id<z.ZodType<ExpectedType>>(zTargetType);
})(); })();
const CACHE_FILE_RELATIVE_PATH = pathJoin("ui-modules", "cache.json"); const CACHE_FILE_RELATIVE_PATH = pathJoin("extension-modules", "cache.json");
export type BuildContextLike = export type BuildContextLike =
BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied & { BuildContextLike_getExtensionModuleFileSourceCodeReadyToBeCopied & {
cacheDirPath: string; cacheDirPath: string;
packageJsonFilePath: string; packageJsonFilePath: string;
projectDirPath: string; projectDirPath: string;
@ -87,9 +87,9 @@ export type BuildContextLike =
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export async function getUiModuleMetas(params: { export async function getExtensionModuleMetas(params: {
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<UiModuleMeta[]> { }): Promise<ExtensionModuleMeta[]> {
const { buildContext } = params; const { buildContext } = params;
const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH); const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH);
@ -106,7 +106,7 @@ export async function getUiModuleMetas(params: {
return configHash; return configHash;
})(); })();
const installedUiModules = await (async () => { const installedExtensionModules = await (async () => {
const installedModulesWithKeycloakifyInTheName = await listInstalledModules({ const installedModulesWithKeycloakifyInTheName = await listInstalledModules({
packageJsonFilePath: buildContext.packageJsonFilePath, packageJsonFilePath: buildContext.packageJsonFilePath,
projectDirPath: buildContext.packageJsonFilePath, projectDirPath: buildContext.packageJsonFilePath,
@ -134,7 +134,7 @@ export async function getUiModuleMetas(params: {
return await fsPr.readFile(cacheFilePath); return await fsPr.readFile(cacheFilePath);
})(); })();
const uiModuleMetas_cacheUpToDate: UiModuleMeta[] = await (async () => { const extensionModuleMetas_cacheUpToDate: ExtensionModuleMeta[] = await (async () => {
const parsedCacheFile: ParsedCacheFile | undefined = await (async () => { const parsedCacheFile: ParsedCacheFile | undefined = await (async () => {
if (cacheContent === undefined) { if (cacheContent === undefined) {
return undefined; return undefined;
@ -177,45 +177,51 @@ export async function getUiModuleMetas(params: {
return []; return [];
} }
const uiModuleMetas_cacheUpToDate = parsedCacheFile.uiModuleMetas.filter( const extensionModuleMetas_cacheUpToDate =
uiModuleMeta => { parsedCacheFile.extensionModuleMetas.filter(extensionModuleMeta => {
const correspondingInstalledUiModule = installedUiModules.find( const correspondingInstalledExtensionModule =
installedUiModule => installedExtensionModules.find(
installedUiModule.moduleName === uiModuleMeta.moduleName installedExtensionModule =>
); installedExtensionModule.moduleName ===
extensionModuleMeta.moduleName
);
if (correspondingInstalledUiModule === undefined) { if (correspondingInstalledExtensionModule === undefined) {
return false; return false;
} }
return correspondingInstalledUiModule.version === uiModuleMeta.version; return (
} correspondingInstalledExtensionModule.version ===
); extensionModuleMeta.version
);
});
return uiModuleMetas_cacheUpToDate; return extensionModuleMetas_cacheUpToDate;
})(); })();
const uiModuleMetas = await Promise.all( const extensionModuleMetas = await Promise.all(
installedUiModules.map( installedExtensionModules.map(
async ({ async ({
moduleName, moduleName,
version, version,
peerDependencies, peerDependencies,
dirPath dirPath
}): Promise<UiModuleMeta> => { }): Promise<ExtensionModuleMeta> => {
use_cache: { use_cache: {
const uiModuleMeta_cache = uiModuleMetas_cacheUpToDate.find( const extensionModuleMeta_cache =
uiModuleMeta => uiModuleMeta.moduleName === moduleName extensionModuleMetas_cacheUpToDate.find(
); extensionModuleMeta =>
extensionModuleMeta.moduleName === moduleName
);
if (uiModuleMeta_cache === undefined) { if (extensionModuleMeta_cache === undefined) {
break use_cache; break use_cache;
} }
return uiModuleMeta_cache; return extensionModuleMeta_cache;
} }
const files: UiModuleMeta["files"] = []; const files: ExtensionModuleMeta["files"] = [];
{ {
const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME); const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME);
@ -225,13 +231,13 @@ export async function getUiModuleMetas(params: {
returnedPathsType: "relative to dirPath", returnedPathsType: "relative to dirPath",
onFileFound: async fileRelativePath => { onFileFound: async fileRelativePath => {
const sourceCode = const sourceCode =
await getUiModuleFileSourceCodeReadyToBeCopied({ await getExtensionModuleFileSourceCodeReadyToBeCopied({
buildContext, buildContext,
fileRelativePath, fileRelativePath,
isOwnershipAction: false, isOwnershipAction: false,
uiModuleDirPath: dirPath, extensionModuleDirPath: dirPath,
uiModuleName: moduleName, extensionModuleName: moduleName,
uiModuleVersion: version extensionModuleVersion: version
}); });
const hash = computeHash(sourceCode); const hash = computeHash(sourceCode);
@ -261,7 +267,7 @@ export async function getUiModuleMetas(params: {
}); });
} }
return id<UiModuleMeta>({ return id<ExtensionModuleMeta>({
moduleName, moduleName,
version, version,
files, files,
@ -281,7 +287,7 @@ export async function getUiModuleMetas(params: {
keycloakifyVersion, keycloakifyVersion,
prettierConfigHash, prettierConfigHash,
thisFilePath: cacheFilePath, thisFilePath: cacheFilePath,
uiModuleMetas extensionModuleMetas
}); });
const cacheContent_new = Buffer.from( const cacheContent_new = Buffer.from(
@ -306,7 +312,7 @@ export async function getUiModuleMetas(params: {
await fsPr.writeFile(cacheFilePath, cacheContent_new); await fsPr.writeFile(cacheFilePath, cacheContent_new);
} }
return uiModuleMetas; return extensionModuleMetas;
} }
export function computeHash(data: Buffer) { export function computeHash(data: Buffer) {

View File

@ -11,25 +11,27 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export async function getUiModuleFileSourceCodeReadyToBeCopied(params: { export async function getExtensionModuleFileSourceCodeReadyToBeCopied(params: {
buildContext: BuildContextLike; buildContext: BuildContextLike;
fileRelativePath: string; fileRelativePath: string;
isOwnershipAction: boolean; isOwnershipAction: boolean;
uiModuleDirPath: string; extensionModuleDirPath: string;
uiModuleName: string; extensionModuleName: string;
uiModuleVersion: string; extensionModuleVersion: string;
}): Promise<Buffer> { }): Promise<Buffer> {
const { const {
buildContext, buildContext,
uiModuleDirPath, extensionModuleDirPath,
fileRelativePath, fileRelativePath,
isOwnershipAction, isOwnershipAction,
uiModuleName, extensionModuleName,
uiModuleVersion extensionModuleVersion
} = params; } = params;
let sourceCode = ( let sourceCode = (
await fsPr.readFile(pathJoin(uiModuleDirPath, KEYCLOAK_THEME, fileRelativePath)) await fsPr.readFile(
pathJoin(extensionModuleDirPath, KEYCLOAK_THEME, fileRelativePath)
)
).toString("utf8"); ).toString("utf8");
sourceCode = addCommentToSourceCode({ sourceCode = addCommentToSourceCode({
@ -40,18 +42,18 @@ export async function getUiModuleFileSourceCodeReadyToBeCopied(params: {
return isOwnershipAction return isOwnershipAction
? [ ? [
`This file has been claimed for ownership from ${uiModuleName} version ${uiModuleVersion}.`, `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:`, `To relinquish ownership and restore this file to its original content, run the following command:`,
``, ``,
`$ npx keycloakify own --revert --path '${path}'` `$ npx keycloakify own --path '${path}' --revert`
] ]
: [ : [
`WARNING: Before modifying this file, run the following command:`, `WARNING: Before modifying this file, run the following command:`,
``, ``,
`$ npx keycloakify own --path '${path}'`, `$ npx keycloakify own --path '${path}'`,
``, ``,
`This file is provided by ${uiModuleName} version ${uiModuleVersion}.`, `This file is provided by ${extensionModuleName} version ${extensionModuleVersion}.`,
`It was copied into your repository by the postinstall script: \`keycloakify postinstall\`.` `It was copied into your repository by the postinstall script: \`keycloakify sync-extensions\`.`
]; ];
})() })()
}); });

View File

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

View File

@ -1,6 +1,6 @@
import { assert, type Equals, is } from "tsafe/assert"; import { assert, type Equals, is } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext"; import type { BuildContext } from "../shared/buildContext";
import type { UiModuleMeta } from "./uiModuleMeta"; import type { ExtensionModuleMeta } from "./extensionModuleMeta";
import { z } from "zod"; import { z } from "zod";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import * as fsPr from "fs/promises"; import * as fsPr from "fs/promises";
@ -16,29 +16,29 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export type UiModuleMetaLike = { export type ExtensionModuleMetaLike = {
moduleName: string; moduleName: string;
peerDependencies: Record<string, string>; peerDependencies: Record<string, string>;
}; };
assert<UiModuleMeta extends UiModuleMetaLike ? true : false>(); assert<ExtensionModuleMeta extends ExtensionModuleMetaLike ? true : false>();
export async function installUiModulesPeerDependencies(params: { export async function installExtensionModulesPeerDependencies(params: {
buildContext: BuildContextLike; buildContext: BuildContextLike;
uiModuleMetas: UiModuleMetaLike[]; extensionModuleMetas: ExtensionModuleMetaLike[];
}): Promise<void | never> { }): Promise<void | never> {
const { buildContext, uiModuleMetas } = params; const { buildContext, extensionModuleMetas } = params;
const { uiModulesPerDependencies } = (() => { const { extensionModulesPerDependencies } = (() => {
const uiModulesPerDependencies: Record<string, string> = {}; const extensionModulesPerDependencies: Record<string, string> = {};
for (const { peerDependencies } of uiModuleMetas) { for (const { peerDependencies } of extensionModuleMetas) {
for (const [peerDependencyName, versionRange_candidate] of Object.entries( for (const [peerDependencyName, versionRange_candidate] of Object.entries(
peerDependencies peerDependencies
)) { )) {
const versionRange = (() => { const versionRange = (() => {
const versionRange_current = const versionRange_current =
uiModulesPerDependencies[peerDependencyName]; extensionModulesPerDependencies[peerDependencyName];
if (versionRange_current === undefined) { if (versionRange_current === undefined) {
return versionRange_candidate; return versionRange_candidate;
@ -76,11 +76,11 @@ export async function installUiModulesPeerDependencies(params: {
return versionRange; return versionRange;
})(); })();
uiModulesPerDependencies[peerDependencyName] = versionRange; extensionModulesPerDependencies[peerDependencyName] = versionRange;
} }
} }
return { uiModulesPerDependencies }; return { extensionModulesPerDependencies };
})(); })();
const parsedPackageJson = await (async () => { const parsedPackageJson = await (async () => {
@ -117,7 +117,9 @@ export async function installUiModulesPeerDependencies(params: {
const parsedPackageJson_before = JSON.parse(JSON.stringify(parsedPackageJson)); const parsedPackageJson_before = JSON.parse(JSON.stringify(parsedPackageJson));
for (const [moduleName, versionRange] of Object.entries(uiModulesPerDependencies)) { for (const [moduleName, versionRange] of Object.entries(
extensionModulesPerDependencies
)) {
if (moduleName.startsWith("@types/")) { if (moduleName.startsWith("@types/")) {
(parsedPackageJson.devDependencies ??= {})[moduleName] = versionRange; (parsedPackageJson.devDependencies ??= {})[moduleName] = versionRange;
continue; continue;

View File

@ -7,7 +7,7 @@ import {
} from "path"; } from "path";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext"; import type { BuildContext } from "../shared/buildContext";
import type { UiModuleMeta } from "./uiModuleMeta"; import type { ExtensionModuleMeta } from "./extensionModuleMeta";
import { existsAsync } from "../tools/fs.existsAsync"; import { existsAsync } from "../tools/fs.existsAsync";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath"; import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
@ -22,12 +22,12 @@ const DELIMITER_END = `# === Owned files end =====`;
export async function writeManagedGitignoreFile(params: { export async function writeManagedGitignoreFile(params: {
buildContext: BuildContextLike; buildContext: BuildContextLike;
uiModuleMetas: UiModuleMeta[]; extensionModuleMetas: ExtensionModuleMeta[];
ownedFilesRelativePaths: string[]; ownedFilesRelativePaths: string[];
}): Promise<void> { }): Promise<void> {
const { buildContext, uiModuleMetas, ownedFilesRelativePaths } = params; const { buildContext, extensionModuleMetas, ownedFilesRelativePaths } = params;
if (uiModuleMetas.length === 0) { if (extensionModuleMetas.length === 0) {
return; return;
} }
@ -43,10 +43,10 @@ export async function writeManagedGitignoreFile(params: {
.map(line => `# ${line}`), .map(line => `# ${line}`),
DELIMITER_END, DELIMITER_END,
``, ``,
...uiModuleMetas ...extensionModuleMetas
.map(uiModuleMeta => [ .map(extensionModuleMeta => [
`# === ${uiModuleMeta.moduleName} v${uiModuleMeta.version} ===`, `# === ${extensionModuleMeta.moduleName} v${extensionModuleMeta.version} ===`,
...uiModuleMeta.files ...extensionModuleMeta.files
.map(({ fileRelativePath }) => fileRelativePath) .map(({ fileRelativePath }) => fileRelativePath)
.filter( .filter(
fileRelativePath => fileRelativePath =>

View File

@ -1,6 +1,6 @@
import type { BuildContext } from "../shared/buildContext"; import type { BuildContext } from "../shared/buildContext";
import { getUiModuleMetas, computeHash } from "./uiModuleMeta"; import { getExtensionModuleMetas, computeHash } from "./extensionModuleMeta";
import { installUiModulesPeerDependencies } from "./installUiModulesPeerDependencies"; import { installExtensionModulesPeerDependencies } from "./installExtensionModulesPeerDependencies";
import { import {
readManagedGitignoreFile, readManagedGitignoreFile,
writeManagedGitignoreFile writeManagedGitignoreFile
@ -15,11 +15,11 @@ import { untrackFromGit } from "../tools/untrackFromGit";
export async function command(params: { buildContext: BuildContext }) { export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params; const { buildContext } = params;
const uiModuleMetas = await getUiModuleMetas({ buildContext }); const extensionModuleMetas = await getExtensionModuleMetas({ buildContext });
await installUiModulesPeerDependencies({ await installExtensionModulesPeerDependencies({
buildContext, buildContext,
uiModuleMetas extensionModuleMetas
}); });
const { ownedFilesRelativePaths } = await readManagedGitignoreFile({ const { ownedFilesRelativePaths } = await readManagedGitignoreFile({
@ -29,14 +29,14 @@ export async function command(params: { buildContext: BuildContext }) {
await writeManagedGitignoreFile({ await writeManagedGitignoreFile({
buildContext, buildContext,
ownedFilesRelativePaths, ownedFilesRelativePaths,
uiModuleMetas extensionModuleMetas
}); });
await Promise.all( await Promise.all(
uiModuleMetas extensionModuleMetas
.map(uiModuleMeta => .map(extensionModuleMeta =>
Promise.all( Promise.all(
uiModuleMeta.files.map( extensionModuleMeta.files.map(
async ({ fileRelativePath, copyableFilePath, hash }) => { async ({ fileRelativePath, copyableFilePath, hash }) => {
if (ownedFilesRelativePaths.includes(fileRelativePath)) { if (ownedFilesRelativePaths.includes(fileRelativePath)) {
return; return;

View File

@ -24,7 +24,7 @@ export async function listInstalledModules(params: {
packageJsonFilePath packageJsonFilePath
}); });
const uiModuleNames = ( const extensionModuleNames = (
[parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const [parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const
) )
.filter(exclude(undefined)) .filter(exclude(undefined))
@ -33,7 +33,7 @@ export async function listInstalledModules(params: {
.filter(moduleName => filter({ moduleName })); .filter(moduleName => filter({ moduleName }));
const result = await Promise.all( const result = await Promise.all(
uiModuleNames.map(async moduleName => { extensionModuleNames.map(async moduleName => {
const dirPath = await getInstalledModuleDirPath({ const dirPath = await getInstalledModuleDirPath({
moduleName, moduleName,
packageJsonDirPath: pathDirname(packageJsonFilePath), packageJsonDirPath: pathDirname(packageJsonFilePath),