Skip to content

Commit

Permalink
feat: added automatic global hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
callmeteus committed Oct 20, 2023
1 parent fdc3a62 commit c702ed4
Show file tree
Hide file tree
Showing 10 changed files with 614 additions and 318 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@
},
"devDependencies": {
"@types/node": "^20.8.7",
"@types/which": "^3.0.1",
"@types/yargs": "^17.0.29",
"typescript": "^5.2.2"
},
"resolutions": {
"wrap-ansi": "7.0.0"
}
}
251 changes: 27 additions & 224 deletions src/core/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import * as fs from "fs";

import yaml from "yaml";
import deepmerge from "deepmerge";

import { Yarn } from "../helpers/Yarn";
import { logger } from "./Logger";

import colors from "colors";
import * as glob from "glob";
import { exitWithError } from "../helpers/Process";

const appPackageJson = require("../../package.json");

interface ILinkOptions {
export interface ILinkOptions {
package?: string;
global?: boolean;
force?: boolean;
Expand All @@ -31,6 +31,16 @@ interface IConfig {
}

export class App {
private static _instance: App;

public static instance() {
if (!this._instance) {
this._instance = new App();
}

return this._instance;
}

private globalConfig: IConfig = {};
private localConfig: IConfig = {};
private config: IConfig = {};
Expand All @@ -44,25 +54,13 @@ export class App {
optionalDependencies?: Record<string, string>;
};

private yarn = new Yarn();
private version: string;
/**
* The current application version.
*/
public version: string;

constructor() {
this.setup();

this.version = appPackageJson.version;

console.log(colors.bold("nayr v%s"), this.version);
}

/**
* Exits the application and displays an error.
* @param message The error message.
* @param params Any params to be passed to the console.error message.
*/
public exitWithError(message: string, ...params: any[]) {
logger.error(message, ...params);
process.exit(1);
}

/**
Expand Down Expand Up @@ -97,139 +95,6 @@ export class App {
return path.resolve(process.cwd(), ".nayr");
}

/**
* Creates a link for a given package.
* @param link The link options.
*/
public link(link: ILinkOptions) {
// If no package was given
if (!link.package) {
// If has no package.json
if (!this.packageJson) {
throw new Error("No package name was given and also a package.json wasn't found.");
}

// Create a new link for it
const obj = this[link.global ? "globalConfig" : "localConfig"];

// Create a link for it
obj.links[this.packageJson.name] = process.cwd();

// Save the configuration files
this.save(link.global ? "global" : "local");
}
}

/**
* Creates multiple links for a given pattern.
* @param link The link options.
*/
public async mklink(link: {
pattern: string;
depth?: number;
}) {
// Remove the last slash from the pattern if has one
if (link.pattern.endsWith("/")) {
link.pattern = link.pattern.substring(0, link.pattern.length - 1);
}

const possibleProjects = await glob.glob(link.pattern + "/", {
maxDepth: link.depth,
follow: true,
absolute: true,
cwd: process.cwd(),
ignore: "node_modules/**"
});

for(const projectPath of possibleProjects) {
const packageJsonPath = path.resolve(projectPath, "package.json");

// Ignore if there's no package.json in the folder
if (!fs.existsSync(packageJsonPath)) {
continue;
}

const packageJson = require(packageJsonPath);

// Call a link for it
await this.yarn.execYarn({
cmd: "link",
cwd: projectPath
});

logger.info("linked package \"%s\"", packageJson.name);
}
}

/**
* Deletes a link for a given package.
* @param unlink The link options.
*/
public async unlink(unlink: ILinkOptions) {
// If no package was given
if (!unlink.package) {
// If has no package.json
if (!this.packageJson) {
throw new Error("No package name was given and also a package.json wasn't found.");
}

// Create a new link for it
const obj = this[unlink.global ? "globalConfig" : "localConfig"];

// Create a link for it
obj.links[this.packageJson.name] = process.cwd();

// Save the configuration files
this.save(unlink.global ? "global" : "local");

return;
}

// If it's a global unlink
if (unlink.global) {
// Try finding the original link folder
const linkFolder = await this.yarn.getGlobalLinkSymlinkPath(unlink.package);

// If no folder was found
if (!linkFolder) {
// Welp, there's nothing to do here
return this.exitWithError("Unable to determine the location for \"%s\"", unlink.package);
}

logger.info("the folder for %s is %s", unlink.package, linkFolder);

try {
// If the symlink still exists
if (fs.lstatSync(linkFolder).isSymbolicLink()) {
// If it's a broken symlink
if (!fs.existsSync(linkFolder)) {
// If it's not forcing a deletion
if (!unlink.force) {
return this.exitWithError("The given symlink doesn't resolve to anywhere. Use --force to abruptly delete the symlink.");
}

// Delete the symlink
await fs.promises.unlink(linkFolder);
} else {
// Just all unlink at the folder
await this.yarn.execYarn({
cmd: "unlink",
cwd: path.resolve(linkFolder)
});
}
} else {
throw new Error("Invalid symlink found.");
}
} catch(e) {
console.error(e);

return this.exitWithError("There's no symlink for \"%s\"", unlink.package);
}
}

logger.info("the symlink for \"%s\" was sucessfully removed.", unlink.package);
}

/**
* Saves a given target.
* @param target The saving target.
Expand All @@ -244,51 +109,12 @@ export class App {
}
}

/**
* Perform all links based on configurations.
*/
public performLinks() {
if (this.config.rules) {
for(const rule of this.config.rules) {
if ("includes" in rule) {
this.processIncludeRule(rule.includes);
}
}
}

// If isn't ignoring global links
if (!this.config.ignoreGlobalLinks) {
this.processGlobalLinks();
}

logger.info("all packages were linked sucessfully");
}

/**
* Resets global links.
* @param opts Any options to be passed to the resetter.
*/
public async resetGlobalLinks(opts?: {
/**
* Will only delete broken symlinks.
*/
onlyBroken?: boolean;
}) {
// Retrieve all linked packages
for(const link of await this.yarn.getGloballyLinkedPackages({
includeBroken: opts?.onlyBroken
})) {
// Unlink it
fs.unlinkSync(await this.yarn.getGlobalLinkSymlinkPath(link));

logger.info("unlinked \"%s\"", link);
}
}

/**
* Sets up the application.
*/
private setup() {
this.version = appPackageJson.version;

if (fs.existsSync(this.getGlobalConfigFilename())) {
this.globalConfig = yaml.parse(
fs.readFileSync(this.getGlobalConfigFilename(), "utf-8")
Expand Down Expand Up @@ -316,7 +142,7 @@ export class App {
* Retrieves all local installed packages.
* @returns
*/
private getLocalPackages() {
public getLocalPackages() {
return {
...this.packageJson.dependencies ?? {},
...this.packageJson.devDependencies ?? {},
Expand All @@ -326,60 +152,37 @@ export class App {
};
}

/**
* Processes a include rule.
* @param include The name that needs to appear in the package names.
*/
private async processIncludeRule(include: string) {
// Iterate over all packages
for(const packageName in this.getLocalPackages()) {
// If the package name includes the given name
if (packageName.includes(include)) {
await this.performSingleLink(packageName);
}
}
}

/**
* Processes all global links
*/
private async processGlobalLinks() {
for(const link of await this.yarn.getGloballyLinkedPackages()) {
await this.performSingleLink(link);
}
}

/**
* Performs a single package link.
* @param packageName The package name to be linked.
* @returns
*/
private async performSingleLink(packageName: string) {
public async performSingleLink(packageName: string) {
// Ignore if it's not installed
if (!this.yarn.isInstalled(packageName)) {
if (!Yarn.isInstalled(packageName)) {
logger.silly("%s isn't installed, will ignore it", packageName);
return;
}

// Ignore if it's already linked
if (this.yarn.isLinked(packageName)) {
if (Yarn.isLinked(packageName)) {
logger.silly("%s is already linked, will ignore it", packageName);
return;
}

logger.info("will link %s via include rule", packageName);
logger.debug("will try to link %s...", packageName);

try {
// Try performing a link for it
await this.yarn.link(packageName);
await Yarn.link(packageName);
} catch(e) {
if (e.message.includes("No registered package")) {
logger.warn("no registered package \"%s\" was found", packageName);
} else {
return this.exitWithError("failed to link \"%s\": %O", packageName, e);
return exitWithError("failed to link \"%s\": %O", packageName, e);
}
}

logger.info("sucessfully linked \"%s\"", packageName);
logger.debug("sucessfully linked \"%s\"", packageName);
}
}
Loading

0 comments on commit c702ed4

Please sign in to comment.