diff --git a/src/bin/main.ts b/src/bin/main.ts index f74eb5f4..7deaae6a 100644 --- a/src/bin/main.ts +++ b/src/bin/main.ts @@ -263,6 +263,7 @@ program program .command<{ path: string; + revert: boolean; }>({ name: "own", description: [ @@ -286,14 +287,26 @@ program "Example `--path admin/page/Login.tsx`" ].join(" ") }) + .option({ + key: "revert", + name: (() => { + const name = "revert"; + + optionsKeys.push(name); + + return name; + })(), + description: "Revert ownership claim over a given file or directory.", + defaultValue: false + }) .task({ skip, - handler: async ({ projectDirPath, path }) => { + handler: async ({ projectDirPath, path, revert }) => { const { command } = await import("./own"); await command({ buildContext: getBuildContext({ projectDirPath }), - cliCommandOptions: { path } + cliCommandOptions: { path, isRevert: revert } }); } }); diff --git a/src/bin/own.ts b/src/bin/own.ts index 3f5505fc..9c024972 100644 --- a/src/bin/own.ts +++ b/src/bin/own.ts @@ -11,58 +11,109 @@ import { } from "./postinstall/managedGitignoreFile"; import { isInside } from "./tools/isInside"; import chalk from "chalk"; +import type { UiModuleMeta } from "./postinstall/uiModuleMeta"; +import { command as command_postinstall } from "./postinstall"; export async function command(params: { buildContext: BuildContext; cliCommandOptions: { path: string; + isRevert: boolean; }; }) { const { buildContext, cliCommandOptions } = params; - const fileOrDirectoryRelativePath = pathRelative( - buildContext.themeSrcDirPath, - getAbsoluteAndInOsFormatPath({ - cwd: buildContext.themeSrcDirPath, - pathIsh: cliCommandOptions.path - }) - ); - const uiModuleMetas = await getUiModuleMetas({ buildContext }); - const arr = uiModuleMetas - .map(uiModuleMeta => ({ - uiModuleMeta, - fileRelativePaths: uiModuleMeta.files - .map(({ fileRelativePath }) => fileRelativePath) - .filter( - fileRelativePath => - fileRelativePath === fileOrDirectoryRelativePath || - isInside({ - dirPath: fileOrDirectoryRelativePath, - filePath: fileRelativePath - }) - ) - })) - .filter(({ fileRelativePaths }) => fileRelativePaths.length !== 0); + const { targetFileRelativePathsByUiModuleMeta } = await (async () => { + const fileOrDirectoryRelativePath = pathRelative( + buildContext.themeSrcDirPath, + getAbsoluteAndInOsFormatPath({ + cwd: buildContext.themeSrcDirPath, + pathIsh: cliCommandOptions.path + }) + ); - if (arr.length === 0) { + const arr = uiModuleMetas + .map(uiModuleMeta => ({ + uiModuleMeta, + fileRelativePaths: uiModuleMeta.files + .map(({ fileRelativePath }) => fileRelativePath) + .filter( + fileRelativePath => + fileRelativePath === fileOrDirectoryRelativePath || + isInside({ + dirPath: fileOrDirectoryRelativePath, + filePath: fileRelativePath + }) + ) + })) + .filter(({ fileRelativePaths }) => fileRelativePaths.length !== 0); + + const targetFileRelativePathsByUiModuleMeta = new Map(); + + for (const { uiModuleMeta, fileRelativePaths } of arr) { + targetFileRelativePathsByUiModuleMeta.set(uiModuleMeta, fileRelativePaths); + } + + return { targetFileRelativePathsByUiModuleMeta }; + })(); + + if (targetFileRelativePathsByUiModuleMeta.size === 0) { console.log( chalk.yellow("There is no UI module files matching the provided path.") ); process.exit(1); } - const { ownedFilesRelativePaths: ownedFilesRelativePaths_before } = + const { ownedFilesRelativePaths: ownedFilesRelativePaths_current } = await readManagedGitignoreFile({ buildContext }); - const ownedFilesRelativePaths_toAdd: string[] = []; + await (cliCommandOptions.isRevert ? command_revert : command_own)({ + uiModuleMetas, + targetFileRelativePathsByUiModuleMeta, + ownedFilesRelativePaths_current, + buildContext + }); +} + +type Params_subcommands = { + uiModuleMetas: UiModuleMeta[]; + targetFileRelativePathsByUiModuleMeta: Map; + ownedFilesRelativePaths_current: string[]; + buildContext: BuildContext; +}; + +async function command_own(params: Params_subcommands) { + const { + uiModuleMetas, + targetFileRelativePathsByUiModuleMeta, + ownedFilesRelativePaths_current, + buildContext + } = params; + + await writeManagedGitignoreFile({ + buildContext, + uiModuleMetas, + ownedFilesRelativePaths: [ + ...ownedFilesRelativePaths_current, + ...Array.from(targetFileRelativePathsByUiModuleMeta.values()) + .flat() + .filter( + fileRelativePath => + !ownedFilesRelativePaths_current.includes(fileRelativePath) + ) + ] + }); const writeActions: (() => Promise)[] = []; - for (const { uiModuleMeta, fileRelativePaths } of arr) { + for (const [ + uiModuleMeta, + fileRelativePaths + ] of targetFileRelativePathsByUiModuleMeta.entries()) { const uiModuleDirPath = await getInstalledModuleDirPath({ moduleName: uiModuleMeta.moduleName, packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath), @@ -70,46 +121,81 @@ export async function command(params: { }); for (const fileRelativePath of fileRelativePaths) { - if (ownedFilesRelativePaths_before.includes(fileRelativePath)) { + if (ownedFilesRelativePaths_current.includes(fileRelativePath)) { console.log( - chalk.yellow(`You already have ownership over "${fileRelativePath}".`) + chalk.grey(`You already have ownership over '${fileRelativePath}'.`) ); continue; } - const sourceCode = await getUiModuleFileSourceCodeReadyToBeCopied({ - buildContext, - fileRelativePath, - isOwnershipAction: true, - uiModuleName: uiModuleMeta.moduleName, - uiModuleDirPath, - uiModuleVersion: uiModuleMeta.version - }); + writeActions.push(async () => { + const sourceCode = await getUiModuleFileSourceCodeReadyToBeCopied({ + buildContext, + fileRelativePath, + isOwnershipAction: true, + uiModuleName: uiModuleMeta.moduleName, + uiModuleDirPath, + uiModuleVersion: uiModuleMeta.version + }); - writeActions.push(() => - fsPr.writeFile( + await fsPr.writeFile( pathJoin(buildContext.themeSrcDirPath, fileRelativePath), sourceCode - ) - ); + ); - ownedFilesRelativePaths_toAdd.push(fileRelativePath); + console.log(chalk.green(`Ownership over '${fileRelativePath}' claimed.`)); + }); } } - if (ownedFilesRelativePaths_toAdd.length === 0) { + if (writeActions.length === 0) { console.log(chalk.yellow("No new file claimed.")); - process.exit(1); + return; } await Promise.all(writeActions.map(action => action())); +} + +async function command_revert(params: Params_subcommands) { + const { + uiModuleMetas, + targetFileRelativePathsByUiModuleMeta, + ownedFilesRelativePaths_current, + buildContext + } = params; + + const ownedFilesRelativePaths_toRemove = Array.from( + targetFileRelativePathsByUiModuleMeta.values() + ) + .flat() + .filter(fileRelativePath => { + if (!ownedFilesRelativePaths_current.includes(fileRelativePath)) { + console.log( + chalk.grey(`Ownership over '${fileRelativePath}' wasn't claimed.`) + ); + return false; + } + + console.log( + chalk.green(`Ownership over '${fileRelativePath}' relinquished.`) + ); + + return true; + }); + + if (ownedFilesRelativePaths_toRemove.length === 0) { + console.log(chalk.yellow("No file relinquished.")); + return; + } await writeManagedGitignoreFile({ buildContext, uiModuleMetas, - ownedFilesRelativePaths: [ - ...ownedFilesRelativePaths_before, - ...ownedFilesRelativePaths_toAdd - ] + ownedFilesRelativePaths: ownedFilesRelativePaths_current.filter( + fileRelativePath => + !ownedFilesRelativePaths_toRemove.includes(fileRelativePath) + ) }); + + await command_postinstall({ buildContext }); } diff --git a/src/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts b/src/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts index d928b5e4..c8f55f70 100644 --- a/src/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts +++ b/src/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts @@ -35,18 +35,25 @@ export async function getUiModuleFileSourceCodeReadyToBeCopied(params: { sourceCode = addCommentToSourceCode({ sourceCode, fileRelativePath, - commentLines: isOwnershipAction - ? [ - `This file was claimed for ownership from ${uiModuleName} version ${uiModuleVersion}.` - ] - : [ - `WARNING: Before modifying this file run the following command:`, - ``, - `$ npx keycloakify own --path '${fileRelativePath.split(pathSep).join("/")}'`, - ``, - `This file comes from ${uiModuleName} version ${uiModuleVersion}.`, - `This file has been copied over to your repo by your postinstall script: \`npx keycloakify postinstall\`` - ] + commentLines: (() => { + const path = fileRelativePath.split(pathSep).join("/"); + + return isOwnershipAction + ? [ + `This file has been claimed for ownership from ${uiModuleName} version ${uiModuleVersion}.`, + `To relinquish ownership and restore this file to its original content, run the following command:`, + ``, + `$ npx keycloakify own --revert --path '${path}'` + ] + : [ + `WARNING: Before modifying this file, run the following command:`, + ``, + `$ npx keycloakify own --path '${path}'`, + ``, + `This file is provided by ${uiModuleName} version ${uiModuleVersion}.`, + `It was copied into your repository by the postinstall script: \`keycloakify postinstall\`.` + ]; + })() }); const destFilePath = pathJoin(buildContext.themeSrcDirPath, fileRelativePath); diff --git a/src/bin/postinstall/postinstall.ts b/src/bin/postinstall/postinstall.ts index 5f040c12..523d3887 100644 --- a/src/bin/postinstall/postinstall.ts +++ b/src/bin/postinstall/postinstall.ts @@ -9,7 +9,7 @@ import { dirname as pathDirname } from "path"; import { join as pathJoin } from "path"; import { existsAsync } from "../tools/fs.existsAsync"; import * as fsPr from "fs/promises"; -import { getIsTrackedByGit } from "../tools/isTrackedByGit"; +import { getIsKnownByGit } from "../tools/isKnownByGit"; import { untrackFromGit } from "../tools/untrackFromGit"; export async function command(params: { buildContext: BuildContext }) { @@ -65,19 +65,7 @@ export async function command(params: { buildContext: BuildContext }) { return; } - git_untrack: { - if (!doesFileExist) { - break git_untrack; - } - - const isTracked = await getIsTrackedByGit({ - filePath: destFilePath - }); - - if (!isTracked) { - break git_untrack; - } - + if (await getIsKnownByGit({ filePath: destFilePath })) { await untrackFromGit({ filePath: destFilePath }); diff --git a/src/bin/tools/isTrackedByGit.ts b/src/bin/tools/isKnownByGit.ts similarity index 63% rename from src/bin/tools/isTrackedByGit.ts rename to src/bin/tools/isKnownByGit.ts index ae4961d7..dbb12012 100644 --- a/src/bin/tools/isTrackedByGit.ts +++ b/src/bin/tools/isKnownByGit.ts @@ -2,28 +2,28 @@ import * as child_process from "child_process"; import { dirname as pathDirname, basename as pathBasename } from "path"; import { Deferred } from "evt/tools/Deferred"; -export function getIsTrackedByGit(params: { filePath: string }): Promise { +export function getIsKnownByGit(params: { filePath: string }): Promise { const { filePath } = params; - const dIsTracked = new Deferred(); + const dIsKnownByGit = new Deferred(); child_process.exec( `git ls-files --error-unmatch ${pathBasename(filePath)}`, { cwd: pathDirname(filePath) }, error => { if (error === null) { - dIsTracked.resolve(true); + dIsKnownByGit.resolve(true); return; } if (error.code === 1) { - dIsTracked.resolve(false); + dIsKnownByGit.resolve(false); return; } - dIsTracked.reject(error); + dIsKnownByGit.reject(error); } ); - return dIsTracked.pr; + return dIsKnownByGit.pr; }