diff --git a/src/bin/initialize-account-heme/copyBoilerplate.ts b/src/bin/initialize-account-heme/copyBoilerplate.ts new file mode 100644 index 00000000..983af445 --- /dev/null +++ b/src/bin/initialize-account-heme/copyBoilerplate.ts @@ -0,0 +1,32 @@ +import * as fs from "fs"; +import { join as pathJoin } from "path"; +import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath"; +import { assert, type Equals } from "tsafe/assert"; + +export function copyBoilerplate(params: { + accountThemeType: "Single-Page" | "Multi-Page"; + accountThemeSrcDirPath: string; +}) { + const { accountThemeType, accountThemeSrcDirPath } = params; + + fs.cpSync( + pathJoin( + getThisCodebaseRootDirPath(), + "src", + "bin", + "initialize-account-theme", + "src", + (() => { + switch (accountThemeType) { + case "Single-Page": + return "single-page"; + case "Multi-Page": + return "multi-page"; + } + assert>(false); + })() + ), + accountThemeSrcDirPath, + { recursive: true } + ); +} diff --git a/src/bin/initialize-account-heme/index.ts b/src/bin/initialize-account-heme/index.ts new file mode 100644 index 00000000..b4701b54 --- /dev/null +++ b/src/bin/initialize-account-heme/index.ts @@ -0,0 +1 @@ +export * from "./initialize-account-theme"; diff --git a/src/bin/initialize-account-heme/initialize-account-theme.ts b/src/bin/initialize-account-heme/initialize-account-theme.ts new file mode 100644 index 00000000..22e641ca --- /dev/null +++ b/src/bin/initialize-account-heme/initialize-account-theme.ts @@ -0,0 +1,95 @@ +import { getBuildContext } from "../shared/buildContext"; +import type { CliCommandOptions } from "../main"; +import cliSelect from "cli-select"; +import child_process from "child_process"; +import chalk from "chalk"; +import { join as pathJoin, relative as pathRelative } from "path"; +import * as fs from "fs"; +import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig"; + +export async function command(params: { cliCommandOptions: CliCommandOptions }) { + const { cliCommandOptions } = params; + + const buildContext = getBuildContext({ cliCommandOptions }); + + const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account"); + + if (fs.existsSync(accountThemeSrcDirPath)) { + console.warn( + chalk.red( + `There is already a ${pathRelative( + process.cwd(), + accountThemeSrcDirPath + )} directory in your project. Aborting.` + ) + ); + + process.exit(-1); + } + + exit_if_uncommitted_changes: { + let hasUncommittedChanges: boolean | undefined = undefined; + + try { + hasUncommittedChanges = + child_process + .execSync(`git status --porcelain`, { + cwd: buildContext.projectDirPath + }) + .toString() + .trim() !== ""; + } catch { + // Probably not a git repository + break exit_if_uncommitted_changes; + } + + if (!hasUncommittedChanges) { + break exit_if_uncommitted_changes; + } + console.warn( + [ + chalk.red( + "Please commit or stash your changes before running this command.\n" + ), + "This command will modify your project's files so it's better to have a clean working directory", + "so that you can easily see what has been changed and revert if needed." + ].join(" ") + ); + + process.exit(-1); + } + + const { value: accountThemeType } = await cliSelect({ + values: ["Single-Page" as const, "Multi-Page" as const] + }).catch(() => { + process.exit(-1); + }); + + switch (accountThemeType) { + case "Multi-Page": + { + const { initializeAccountTheme_multiPage } = await import( + "./initializeAccountTheme_multiPage" + ); + + await initializeAccountTheme_multiPage({ + accountThemeSrcDirPath + }); + } + break; + case "Single-Page": + { + const { initializeAccountTheme_singlePage } = await import( + "./initializeAccountTheme_singlePage" + ); + + await initializeAccountTheme_singlePage({ + accountThemeSrcDirPath, + buildContext + }); + } + break; + } + + updateAccountThemeImplementationInConfig({ buildContext, accountThemeType }); +} diff --git a/src/bin/initialize-account-heme/initializeAccountTheme_multiPage.ts b/src/bin/initialize-account-heme/initializeAccountTheme_multiPage.ts new file mode 100644 index 00000000..d3dfe78d --- /dev/null +++ b/src/bin/initialize-account-heme/initializeAccountTheme_multiPage.ts @@ -0,0 +1,21 @@ +import { relative as pathRelative } from "path"; +import chalk from "chalk"; +import { copyBoilerplate } from "./copyBoilerplate"; + +export async function initializeAccountTheme_multiPage(params: { + accountThemeSrcDirPath: string; +}) { + const { accountThemeSrcDirPath } = params; + + copyBoilerplate({ + accountThemeType: "Multi-Page", + accountThemeSrcDirPath + }); + + console.log( + [ + chalk.green("The Multi-Page account theme has been initialized."), + `Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}` + ].join("\n") + ); +} diff --git a/src/bin/initialize-account-heme/initializeAccountTheme_singlePage.ts b/src/bin/initialize-account-heme/initializeAccountTheme_singlePage.ts new file mode 100644 index 00000000..e262944a --- /dev/null +++ b/src/bin/initialize-account-heme/initializeAccountTheme_singlePage.ts @@ -0,0 +1,136 @@ +import { relative as pathRelative, dirname as pathDirname } from "path"; +import type { BuildContext } from "../shared/buildContext"; +import * as fs from "fs"; +import chalk from "chalk"; +import { getLatestsSemVersionedTag } from "../shared/getLatestsSemVersionedTag"; +import fetch from "make-fetch-happen"; +import { z } from "zod"; +import { assert, type Equals } from "tsafe/assert"; +import { is } from "tsafe/is"; +import { id } from "tsafe/id"; +import { npmInstall } from "../tools/npmInstall"; +import { copyBoilerplate } from "./copyBoilerplate"; + +type BuildContextLike = { + cacheDirPath: string; + fetchOptions: BuildContext["fetchOptions"]; + packageJsonFilePath: string; +}; + +assert(); + +export async function initializeAccountTheme_singlePage(params: { + accountThemeSrcDirPath: string; + buildContext: BuildContextLike; +}) { + const { accountThemeSrcDirPath, buildContext } = params; + + const OWNER = "keycloakify"; + const REPO = "keycloak-account-ui"; + + const [semVersionedTag] = await getLatestsSemVersionedTag({ + cacheDirPath: buildContext.cacheDirPath, + owner: OWNER, + repo: REPO, + count: 1, + doIgnoreReleaseCandidates: false + }); + + const dependencies = await fetch( + `https://raw.githubusercontent.com/${OWNER}/${REPO}/${semVersionedTag.tag}/dependencies.gen.json`, + buildContext.fetchOptions + ) + .then(r => r.json()) + .then( + (() => { + type Dependencies = { + dependencies: Record; + devDependencies?: Record; + }; + + const zDependencies = (() => { + type TargetType = Dependencies; + + const zTargetType = z.object({ + dependencies: z.record(z.string()), + devDependencies: z.record(z.string()).optional() + }); + + assert, TargetType>>(); + + return id>(zTargetType); + })(); + + return o => zDependencies.parse(o); + })() + ); + + dependencies.dependencies["@keycloakify/keycloak-account-ui"] = semVersionedTag.tag; + + const parsedPackageJson = (() => { + type ParsedPackageJson = { + dependencies?: Record; + devDependencies?: Record; + }; + + const zParsedPackageJson = (() => { + type TargetType = ParsedPackageJson; + + const zTargetType = z.object({ + dependencies: z.record(z.string()).optional(), + devDependencies: z.record(z.string()).optional() + }); + + assert, TargetType>>(); + + return id>(zTargetType); + })(); + const parsedPackageJson = JSON.parse( + fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8") + ); + + zParsedPackageJson.parse(parsedPackageJson); + + assert(is(parsedPackageJson)); + + return parsedPackageJson; + })(); + + parsedPackageJson.dependencies = { + ...parsedPackageJson.dependencies, + ...dependencies.dependencies + }; + + parsedPackageJson.devDependencies = { + ...parsedPackageJson.devDependencies, + ...dependencies.devDependencies + }; + + if (Object.keys(parsedPackageJson.devDependencies).length === 0) { + delete parsedPackageJson.devDependencies; + } + + fs.writeFileSync( + buildContext.packageJsonFilePath, + JSON.stringify(parsedPackageJson, undefined, 4) + ); + + npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) }); + + copyBoilerplate({ + accountThemeType: "Single-Page", + accountThemeSrcDirPath + }); + + console.log( + [ + chalk.green( + "The Single-Page account theme has been successfully initialized." + ), + `Using Account UI of Keycloak version: ${chalk.bold(semVersionedTag.tag.split("-")[0])}`, + `Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}`, + `Dependencies added to your project's package.json: `, + chalk.bold(JSON.stringify(dependencies, null, 2)) + ].join("\n") + ); +} diff --git a/src/bin/initialize-account-heme/src/multi-page/KcContext.ts b/src/bin/initialize-account-heme/src/multi-page/KcContext.ts new file mode 100644 index 00000000..a043f253 --- /dev/null +++ b/src/bin/initialize-account-heme/src/multi-page/KcContext.ts @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import type { ExtendKcContext } from "keycloakify/account"; +import type { KcEnvName, ThemeName } from "../kc.gen"; + +export type KcContextExtension = { + themeName: ThemeName; + properties: Record & {}; +}; + +export type KcContextExtensionPerPage = {}; + +export type KcContext = ExtendKcContext; diff --git a/src/bin/initialize-account-heme/src/multi-page/KcPage.tsx b/src/bin/initialize-account-heme/src/multi-page/KcPage.tsx new file mode 100644 index 00000000..6f86dc76 --- /dev/null +++ b/src/bin/initialize-account-heme/src/multi-page/KcPage.tsx @@ -0,0 +1,25 @@ +import { Suspense } from "react"; +import type { ClassKey } from "keycloakify/account"; +import type { KcContext } from "./KcContext"; +import { useI18n } from "./i18n"; +import DefaultPage from "keycloakify/account/DefaultPage"; +import Template from "keycloakify/account/Template"; + +export default function KcPage(props: { kcContext: KcContext }) { + const { kcContext } = props; + + const { i18n } = useI18n({ kcContext }); + + return ( + + {(() => { + switch (kcContext.pageId) { + default: + return ; + } + })()} + + ); +} + +const classes = {} satisfies { [key in ClassKey]?: string }; diff --git a/src/bin/initialize-account-heme/src/multi-page/KcPageStory.tsx b/src/bin/initialize-account-heme/src/multi-page/KcPageStory.tsx new file mode 100644 index 00000000..c0595475 --- /dev/null +++ b/src/bin/initialize-account-heme/src/multi-page/KcPageStory.tsx @@ -0,0 +1,38 @@ +import type { DeepPartial } from "keycloakify/tools/DeepPartial"; +import type { KcContext } from "./KcContext"; +import { createGetKcContextMock } from "keycloakify/account/KcContext"; +import type { KcContextExtension, KcContextExtensionPerPage } from "./KcContext"; +import KcPage from "./KcPage"; +import { themeNames, kcEnvDefaults } from "../kc.gen"; + +const kcContextExtension: KcContextExtension = { + themeName: themeNames[0], + properties: { + ...kcEnvDefaults + } +}; +const kcContextExtensionPerPage: KcContextExtensionPerPage = {}; + +export const { getKcContextMock } = createGetKcContextMock({ + kcContextExtension, + kcContextExtensionPerPage, + overrides: {}, + overridesPerPage: {} +}); + +export function createKcPageStory(params: { pageId: PageId }) { + const { pageId } = params; + + function KcPageStory(props: { kcContext?: DeepPartial> }) { + const { kcContext: overrides } = props; + + const kcContextMock = getKcContextMock({ + pageId, + overrides + }); + + return ; + } + + return { KcPageStory }; +} diff --git a/src/bin/initialize-account-heme/src/multi-page/i18n.ts b/src/bin/initialize-account-heme/src/multi-page/i18n.ts new file mode 100644 index 00000000..c4ad70c1 --- /dev/null +++ b/src/bin/initialize-account-heme/src/multi-page/i18n.ts @@ -0,0 +1,5 @@ +import { createUseI18n } from "keycloakify/account"; + +export const { useI18n, ofTypeI18n } = createUseI18n({}); + +export type I18n = typeof ofTypeI18n; diff --git a/src/bin/initialize-account-heme/src/single-page/KcContext.ts b/src/bin/initialize-account-heme/src/single-page/KcContext.ts new file mode 100644 index 00000000..db2be626 --- /dev/null +++ b/src/bin/initialize-account-heme/src/single-page/KcContext.ts @@ -0,0 +1,7 @@ +import type { KcContextLike } from "@keycloakify/keycloak-account-ui"; +import type { KcEnvName } from "../kc.gen"; + +export type KcContext = KcContextLike & { + themeType: "account"; + properties: Record; +}; diff --git a/src/bin/initialize-account-heme/src/single-page/KcPage.tsx b/src/bin/initialize-account-heme/src/single-page/KcPage.tsx new file mode 100644 index 00000000..e086f00d --- /dev/null +++ b/src/bin/initialize-account-heme/src/single-page/KcPage.tsx @@ -0,0 +1,11 @@ +import { lazy } from "react"; +import { KcAccountUiLoader } from "@keycloakify/keycloak-account-ui"; +import type { KcContext } from "./KcContext"; + +const KcAccountUi = lazy(() => import("@keycloakify/keycloak-account-ui/KcAccountUi")); + +export default function KcPage(props: { kcContext: KcContext }) { + const { kcContext } = props; + + return ; +} diff --git a/src/bin/initialize-account-heme/updateAccountThemeImplementationInConfig.ts b/src/bin/initialize-account-heme/updateAccountThemeImplementationInConfig.ts new file mode 100644 index 00000000..364e9233 --- /dev/null +++ b/src/bin/initialize-account-heme/updateAccountThemeImplementationInConfig.ts @@ -0,0 +1,92 @@ +import { join as pathJoin } from "path"; +import { assert, type Equals } from "tsafe/assert"; +import type { BuildContext } from "../shared/buildContext"; +import * as fs from "fs"; +import chalk from "chalk"; +import { z } from "zod"; +import { id } from "tsafe/id"; + +export type BuildContextLike = { + bundler: BuildContext["bundler"]; +}; + +assert(); + +export function updateAccountThemeImplementationInConfig(params: { + buildContext: BuildContext; + accountThemeType: "Single-Page" | "Multi-Page"; +}) { + const { buildContext, accountThemeType } = params; + + switch (buildContext.bundler) { + case "vite": + { + const viteConfigPath = pathJoin( + buildContext.projectDirPath, + "vite.config.ts" + ); + + if (!fs.existsSync(viteConfigPath)) { + console.log( + chalk.bold( + `You must manually set the accountThemeImplementation to "${accountThemeType}" in your vite config` + ) + ); + break; + } + + const viteConfigContent = fs + .readFileSync(viteConfigPath) + .toString("utf8"); + + const modifiedViteConfigContent = viteConfigContent.replace( + /accountThemeImplementation\s*:\s*"none"/, + `accountThemeImplementation: "${accountThemeType}"` + ); + + if (modifiedViteConfigContent === viteConfigContent) { + console.log( + chalk.bold( + `You must manually set the accountThemeImplementation to "${accountThemeType}" in your vite.config.ts` + ) + ); + break; + } + + fs.writeFileSync(viteConfigPath, modifiedViteConfigContent); + } + break; + case "webpack": + { + const parsedPackageJson = (() => { + type ParsedPackageJson = { + keycloakify: Record; + }; + + const zParsedPackageJson = (() => { + type TargetType = ParsedPackageJson; + + const zTargetType = z.object({ + keycloakify: z.record(z.string()) + }); + + assert, TargetType>>(); + + return id>(zTargetType); + })(); + + return zParsedPackageJson.parse( + JSON.parse( + fs + .readFileSync(buildContext.packageJsonFilePath) + .toString("utf8") + ) + ); + })(); + + parsedPackageJson.keycloakify.accountThemeImplementation = + accountThemeType; + } + break; + } +} diff --git a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts index 4012c774..382631aa 100644 --- a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts +++ b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts @@ -55,7 +55,7 @@ export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & environmentVariables: { name: string; default: string }[]; implementedThemeTypes: BuildContext["implementedThemeTypes"]; themeSrcDirPath: string; - bundler: { type: "vite" } | { type: "webpack" }; + bundler: "vite" | "webpack"; }; assert(); @@ -121,7 +121,7 @@ export async function generateResourcesForMainTheme(params: { ); if (fs.existsSync(dirPath)) { - assert(buildContext.bundler.type === "webpack"); + assert(buildContext.bundler === "webpack"); throw new Error( [ diff --git a/src/bin/keycloakify/keycloakify.ts b/src/bin/keycloakify/keycloakify.ts index ec6be63b..548a9e67 100644 --- a/src/bin/keycloakify/keycloakify.ts +++ b/src/bin/keycloakify/keycloakify.ts @@ -85,7 +85,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) }); run_post_build_script: { - if (buildContext.bundler.type !== "vite") { + if (buildContext.bundler !== "vite") { break run_post_build_script; } diff --git a/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts b/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts index 0317cf33..06e08a2c 100644 --- a/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts +++ b/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts @@ -8,7 +8,7 @@ export type BuildContextLike = { projectBuildDirPath: string; assetsDirPath: string; urlPathname: string | undefined; - bundler: { type: "vite" } | { type: "webpack" }; + bundler: "vite" | "webpack"; }; assert(); @@ -20,7 +20,7 @@ export function replaceImportsInJsCode(params: { const { jsCode, buildContext } = params; const { fixedJsCode } = (() => { - switch (buildContext.bundler.type) { + switch (buildContext.bundler) { case "vite": return replaceImportsInJsCode_vite({ jsCode, diff --git a/src/bin/main.ts b/src/bin/main.ts index c935e171..3284ebc6 100644 --- a/src/bin/main.ts +++ b/src/bin/main.ts @@ -179,6 +179,20 @@ program } }); +program + .command({ + name: "initialize-account-theme", + description: "Initialize the account theme." + }) + .task({ + skip, + handler: async cliCommandOptions => { + const { command } = await import("./initialize-account-heme"); + + await command({ cliCommandOptions }); + } + }); + program .command({ name: "copy-keycloak-resources-to-public", diff --git a/src/bin/shared/buildContext.ts b/src/bin/shared/buildContext.ts index e67df932..0405f225 100644 --- a/src/bin/shared/buildContext.ts +++ b/src/bin/shared/buildContext.ts @@ -54,15 +54,8 @@ export type BuildContext = { | { isImplemented: false } | { isImplemented: true; type: "Single-Page" | "Multi-Page" }; }; - bundler: - | { - type: "vite"; - } - | { - type: "webpack"; - packageJsonDirPath: string; - packageJsonScripts: Record; - }; + packageJsonFilePath: string; + bundler: "vite" | "webpack"; jarTargets: { keycloakVersionRange: KeycloakVersionRange; jarFileBasename: string; @@ -245,7 +238,6 @@ export function getBuildContext(params: { projectBuildDirPath?: string; staticDirPathInProjectBuildDirPath?: string; publicDirPath?: string; - scripts?: Record; }; type ParsedPackageJson = { @@ -346,8 +338,7 @@ export function getBuildContext(params: { z.object({ projectBuildDirPath: z.string().optional(), staticDirPathInProjectBuildDirPath: z.string().optional(), - publicDirPath: z.string().optional(), - scripts: z.record(z.string()).optional() + publicDirPath: z.string().optional() }) ); @@ -490,19 +481,8 @@ export function getBuildContext(params: { })(); return { - bundler: (() => { - switch (bundler) { - case "vite": - return { type: "vite" }; - case "webpack": - return { - type: "webpack", - packageJsonDirPath: pathDirname(packageJsonFilePath), - packageJsonScripts: parsedPackageJson.keycloakify?.scripts ?? {} - }; - } - assert>(false); - })(), + bundler, + packageJsonFilePath, themeVersion: buildOptions.themeVersion ?? parsedPackageJson.version ?? "0.0.0", themeNames, extraThemeProperties: buildOptions.extraThemeProperties, diff --git a/src/bin/shared/getLatestsSemVersionedTag.ts b/src/bin/shared/getLatestsSemVersionedTag.ts new file mode 100644 index 00000000..462cb418 --- /dev/null +++ b/src/bin/shared/getLatestsSemVersionedTag.ts @@ -0,0 +1,180 @@ +import { getLatestsSemVersionedTagFactory } from "../tools/octokit-addons/getLatestsSemVersionedTag"; +import { Octokit } from "@octokit/rest"; +import type { ReturnType } from "tsafe"; +import type { Param0 } from "tsafe"; +import { join as pathJoin, dirname as pathDirname } from "path"; +import * as fs from "fs"; +import { z } from "zod"; +import { assert, type Equals } from "tsafe/assert"; +import { id } from "tsafe/id"; +import type { SemVer } from "../tools/SemVer"; +import { same } from "evt/tools/inDepth/same"; + +type GetLatestsSemVersionedTag = ReturnType< + typeof getLatestsSemVersionedTagFactory +>["getLatestsSemVersionedTag"]; + +type Params = Param0; +type R = ReturnType; + +let getLatestsSemVersionedTag_stateless: GetLatestsSemVersionedTag | undefined = + undefined; + +const CACHE_VERSION = 1; + +type Cache = { + version: typeof CACHE_VERSION; + entries: { + time: number; + params: Params; + result: R; + }[]; +}; + +export async function getLatestsSemVersionedTag({ + cacheDirPath, + ...params +}: Params & { cacheDirPath: string }): Promise { + const cacheFilePath = pathJoin(cacheDirPath, "latest-sem-versioned-tags.json"); + + const cacheLookupResult = (() => { + const getResult_currentCache = (currentCacheEntries: Cache["entries"]) => ({ + hasCachedResult: false as const, + currentCache: { + version: CACHE_VERSION, + entries: currentCacheEntries + } + }); + + if (!fs.existsSync(cacheFilePath)) { + return getResult_currentCache([]); + } + + let cache_json; + + try { + cache_json = fs.readFileSync(cacheFilePath).toString("utf8"); + } catch { + return getResult_currentCache([]); + } + + let cache_json_parsed: unknown; + + try { + cache_json_parsed = JSON.parse(cache_json); + } catch { + return getResult_currentCache([]); + } + + const zSemVer = (() => { + type TargetType = SemVer; + + const zTargetType = z.object({ + major: z.number(), + minor: z.number(), + patch: z.number(), + rc: z.number().optional(), + parsedFrom: z.string() + }); + + assert, TargetType>>(); + + return id>(zTargetType); + })(); + + const zCache = (() => { + type TargetType = Cache; + + const zTargetType = z.object({ + version: z.literal(CACHE_VERSION), + entries: z.array( + z.object({ + time: z.number(), + params: z.object({ + owner: z.string(), + repo: z.string(), + count: z.number(), + doIgnoreReleaseCandidates: z.boolean() + }), + result: z.array( + z.object({ + tag: z.string(), + version: zSemVer + }) + ) + }) + ) + }); + + assert, TargetType>>(); + + return id>(zTargetType); + })(); + + let cache: Cache; + + try { + cache = zCache.parse(cache_json_parsed); + } catch { + return getResult_currentCache([]); + } + + const cacheEntry = cache.entries.find(e => same(e.params, params)); + + if (cacheEntry === undefined) { + return getResult_currentCache(cache.entries); + } + + if (Date.now() - cacheEntry.time > 3_600_000) { + return getResult_currentCache(cache.entries.filter(e => e !== cacheEntry)); + } + return { + hasCachedResult: true as const, + cachedResult: cacheEntry.result + }; + })(); + + if (cacheLookupResult.hasCachedResult) { + return cacheLookupResult.cachedResult; + } + + const { currentCache } = cacheLookupResult; + + getLatestsSemVersionedTag_stateless ??= (() => { + const octokit = (() => { + const githubToken = process.env.GITHUB_TOKEN; + + const octokit = new Octokit( + githubToken === undefined ? undefined : { auth: githubToken } + ); + + return octokit; + })(); + + const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({ + octokit + }); + + return getLatestsSemVersionedTag; + })(); + + const result = await getLatestsSemVersionedTag_stateless(params); + + currentCache.entries.push({ + time: Date.now(), + params, + result + }); + + { + const dirPath = pathDirname(cacheFilePath); + + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + } + + fs.writeFileSync(cacheFilePath, JSON.stringify(currentCache, null, 2)); + + return result; +} diff --git a/src/bin/shared/promptKeycloakVersion.ts b/src/bin/shared/promptKeycloakVersion.ts index daefbe3b..ed84c828 100644 --- a/src/bin/shared/promptKeycloakVersion.ts +++ b/src/bin/shared/promptKeycloakVersion.ts @@ -1,11 +1,6 @@ -import { getLatestsSemVersionedTagFactory } from "../tools/octokit-addons/getLatestsSemVersionedTag"; -import { Octokit } from "@octokit/rest"; +import { getLatestsSemVersionedTag } from "./getLatestsSemVersionedTag"; import cliSelect from "cli-select"; import { SemVer } from "../tools/SemVer"; -import { join as pathJoin, dirname as pathDirname } from "path"; -import * as fs from "fs"; -import type { ReturnType } from "tsafe"; -import { id } from "tsafe/id"; export async function promptKeycloakVersion(params: { startingFromMajor: number | undefined; @@ -14,79 +9,15 @@ export async function promptKeycloakVersion(params: { }) { const { startingFromMajor, excludeMajorVersions, cacheDirPath } = params; - const { getLatestsSemVersionedTag } = (() => { - const { octokit } = (() => { - const githubToken = process.env.GITHUB_TOKEN; - - const octokit = new Octokit( - githubToken === undefined ? undefined : { auth: githubToken } - ); - - return { octokit }; - })(); - - const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({ - octokit - }); - - return { getLatestsSemVersionedTag }; - })(); - const semVersionedTagByMajor = new Map(); - const semVersionedTags = await (async () => { - const cacheFilePath = pathJoin(cacheDirPath, "keycloak-versions.json"); - - type Cache = { - time: number; - semVersionedTags: ReturnType; - }; - - use_cache: { - if (!fs.existsSync(cacheFilePath)) { - break use_cache; - } - - const cache: Cache = JSON.parse( - fs.readFileSync(cacheFilePath).toString("utf8") - ); - - if (Date.now() - cache.time > 3_600_000) { - fs.unlinkSync(cacheFilePath); - break use_cache; - } - - return cache.semVersionedTags; - } - - const semVersionedTags = await getLatestsSemVersionedTag({ - count: 50, - owner: "keycloak", - repo: "keycloak" - }); - - { - const dirPath = pathDirname(cacheFilePath); - - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - } - } - - fs.writeFileSync( - cacheFilePath, - JSON.stringify( - id({ - time: Date.now(), - semVersionedTags - }), - null, - 2 - ) - ); - - return semVersionedTags; - })(); + const semVersionedTags = await getLatestsSemVersionedTag({ + cacheDirPath, + count: 50, + owner: "keycloak", + repo: "keycloak", + doIgnoreReleaseCandidates: true + }); semVersionedTags.forEach(semVersionedTag => { if ( diff --git a/src/bin/start-keycloak/appBuild.ts b/src/bin/start-keycloak/appBuild.ts index 812ead4a..6b1c72a6 100644 --- a/src/bin/start-keycloak/appBuild.ts +++ b/src/bin/start-keycloak/appBuild.ts @@ -5,12 +5,15 @@ import type { BuildContext } from "../shared/buildContext"; import chalk from "chalk"; import { sep as pathSep, join as pathJoin } from "path"; import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath"; +import * as fs from "fs"; +import { dirname as pathDirname, relative as pathRelative } from "path"; export type BuildContextLike = { projectDirPath: string; keycloakifyBuildDirPath: string; bundler: BuildContext["bundler"]; projectBuildDirPath: string; + packageJsonFilePath: string; }; assert(); @@ -20,7 +23,7 @@ export async function appBuild(params: { }): Promise<{ isAppBuildSuccess: boolean }> { const { buildContext } = params; - switch (buildContext.bundler.type) { + switch (buildContext.bundler) { case "vite": return appBuild_vite({ buildContext }); case "webpack": @@ -33,7 +36,7 @@ async function appBuild_vite(params: { }): Promise<{ isAppBuildSuccess: boolean }> { const { buildContext } = params; - assert(buildContext.bundler.type === "vite"); + assert(buildContext.bundler === "vite"); const dIsSuccess = new Deferred(); @@ -66,17 +69,18 @@ async function appBuild_webpack(params: { }): Promise<{ isAppBuildSuccess: boolean }> { const { buildContext } = params; - assert(buildContext.bundler.type === "webpack"); + assert(buildContext.bundler === "webpack"); - const entries = Object.entries(buildContext.bundler.packageJsonScripts).filter( - ([, scriptCommand]) => scriptCommand.includes("keycloakify build") - ); + const entries = Object.entries( + (JSON.parse(fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")) + .scripts ?? {}) as Record + ).filter(([, scriptCommand]) => scriptCommand.includes("keycloakify build")); if (entries.length === 0) { console.log( chalk.red( [ - `You should have a script in your package.json at ${buildContext.bundler.packageJsonDirPath}`, + `You should have a script in your package.json at ${pathRelative(process.cwd(), pathDirname(buildContext.packageJsonFilePath))}`, `that includes the 'keycloakify build' command` ].join(" ") ) @@ -123,7 +127,7 @@ async function appBuild_webpack(params: { process.exit(-1); } - let commandCwd = buildContext.bundler.packageJsonDirPath; + let commandCwd = pathDirname(buildContext.packageJsonFilePath); for (const subCommand of appBuildSubCommands) { const dIsSuccess = new Deferred(); @@ -152,7 +156,7 @@ async function appBuild_webpack(params: { return [ pathJoin( - buildContext.bundler.packageJsonDirPath, + pathDirname(buildContext.packageJsonFilePath), "node_modules", ".bin" ), diff --git a/src/bin/tools/npmInstall.ts b/src/bin/tools/npmInstall.ts new file mode 100644 index 00000000..a95a99df --- /dev/null +++ b/src/bin/tools/npmInstall.ts @@ -0,0 +1,63 @@ +import * as fs from "fs"; +import { join as pathJoin } from "path"; +import * as child_process from "child_process"; +import chalk from "chalk"; + +export function npmInstall(params: { packageJsonDirPath: string }) { + const { packageJsonDirPath } = params; + + const packageManagerBinName = (() => { + const packageMangers = [ + { + binName: "yarn", + lockFileBasename: "yarn.lock" + }, + { + binName: "npm", + lockFileBasename: "package-lock.json" + }, + { + binName: "pnpm", + lockFileBasename: "pnpm-lock.yaml" + }, + { + binName: "bun", + lockFileBasename: "bun.lockdb" + } + ] as const; + + for (const packageManager of packageMangers) { + if ( + fs.existsSync( + pathJoin(packageJsonDirPath, packageManager.lockFileBasename) + ) || + fs.existsSync(pathJoin(process.cwd(), packageManager.lockFileBasename)) + ) { + return packageManager.binName; + } + } + + return undefined; + })(); + + install_dependencies: { + if (packageManagerBinName === undefined) { + break install_dependencies; + } + + console.log(`Installing the new dependencies...`); + + try { + child_process.execSync(`${packageManagerBinName} install`, { + cwd: packageJsonDirPath, + stdio: "inherit" + }); + } catch { + console.log( + chalk.yellow( + `\`${packageManagerBinName} install\` failed, continuing anyway...` + ) + ); + } + } +} diff --git a/src/bin/tools/octokit-addons/getLatestsSemVersionedTag.ts b/src/bin/tools/octokit-addons/getLatestsSemVersionedTag.ts index 570e509c..f829c7e3 100644 --- a/src/bin/tools/octokit-addons/getLatestsSemVersionedTag.ts +++ b/src/bin/tools/octokit-addons/getLatestsSemVersionedTag.ts @@ -9,13 +9,14 @@ export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) { owner: string; repo: string; count: number; + doIgnoreReleaseCandidates: boolean; }): Promise< { tag: string; version: SemVer; }[] > { - const { owner, repo, count } = params; + const { owner, repo, count, doIgnoreReleaseCandidates } = params; const semVersionedTags: { tag: string; version: SemVer }[] = []; @@ -30,7 +31,7 @@ export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) { continue; } - if (version.rc !== undefined) { + if (doIgnoreReleaseCandidates && version.rc !== undefined) { continue; } diff --git a/src/bin/tsconfig.json b/src/bin/tsconfig.json index 164e263c..44ede8c8 100644 --- a/src/bin/tsconfig.json +++ b/src/bin/tsconfig.json @@ -8,5 +8,7 @@ "moduleResolution": "node", "outDir": "../../dist/bin", "rootDir": "." - } + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["initialize-account-heme/src"] }