@ffflorian/gh-open 3.6.4 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ Open a GitHub repository in your browser.
9
9
 
10
10
  ## Installation
11
11
 
12
- ℹ️ This is a hybrid [CommonJS](https://nodejs.org/docs/latest/api/modules.html#modules-commonjs-modules) / [ESM](https://nodejs.org/api/esm.html#introduction) module.
12
+ ℹ️ This is a pure [ESM](https://nodejs.org/api/esm.html#introduction) module.
13
13
 
14
14
  Run `yarn global add @ffflorian/gh-open` or `npm install -g @ffflorian/gh-open`.
15
15
 
@@ -1,5 +1,5 @@
1
- import { promises as fsAsync } from 'fs';
2
- import path from 'path';
1
+ import { promises as fsAsync } from 'node:fs';
2
+ import path from 'node:path';
3
3
  import logdown from 'logdown';
4
4
  import { GitHubClient } from './GitHubClient.js';
5
5
  export class RepositoryService {
@@ -10,7 +10,7 @@ export class RepositoryService {
10
10
  pullRequest: new RegExp('github\\.com\\/(?<user>[^\\/]+)\\/(?<project>[^/]+)\\/tree\\/(?<branch>.*)'),
11
11
  rawUrl: new RegExp('.*url = (?<rawUrl>.*)', 'mi'),
12
12
  };
13
- this.options = Object.assign({ debug: false, timeout: 2000 }, options);
13
+ this.options = { debug: false, timeout: 2000, ...options };
14
14
  this.gitHubClient = new GitHubClient(this.options.timeout);
15
15
  this.logger = logdown('gh-open', {
16
16
  logger: console,
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import path from 'path';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
3
5
  import { program as commander } from 'commander';
4
6
  import { findUp } from 'find-up';
5
7
  import open from 'open';
6
- import { createRequire } from 'module';
7
- const require = createRequire(import.meta.url);
8
8
  import { RepositoryService } from './RepositoryService.js';
9
- const { description, name, version } = require('../package.json');
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const packageJsonPath = path.join(__dirname, '../package.json');
12
+ const { description, name, version } = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
10
13
  commander
11
14
  .name(name.replace(/^@[^/]+\//, ''))
12
15
  .description(description)
@@ -25,7 +28,10 @@ void (async () => {
25
28
  if (!gitDir) {
26
29
  throw new Error(`Could not find a git repository in "${resolvedBaseDir}".`);
27
30
  }
28
- const repositoryService = new RepositoryService(Object.assign(Object.assign({}, (commanderOptions.debug && { debug: commanderOptions.debug })), (commanderOptions.timeout && { timeout: parseInt(commanderOptions.timeout, 10) })));
31
+ const repositoryService = new RepositoryService({
32
+ ...(commanderOptions.debug && { debug: commanderOptions.debug }),
33
+ ...(commanderOptions.timeout && { timeout: parseInt(commanderOptions.timeout, 10) }),
34
+ });
29
35
  let fullUrl = await repositoryService.getFullUrl(gitDir);
30
36
  if (!commanderOptions.branch) {
31
37
  const pullRequestUrl = await repositoryService.getPullRequestUrl(fullUrl);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "author": "Florian Imdahl <git@ffflorian.de>",
3
- "bin": "dist/cjs/cli.js",
3
+ "bin": "dist/cli.js",
4
4
  "dependencies": {
5
- "axios": "1.7.2",
5
+ "axios": "1.7.3",
6
6
  "commander": "12.1.0",
7
7
  "find-up": "7.0.0",
8
8
  "logdown": "3.3.1",
@@ -12,19 +12,14 @@
12
12
  "devDependencies": {
13
13
  "http-status-codes": "2.3.0",
14
14
  "nock": "13.5.4",
15
- "rimraf": "5.0.7",
16
- "typescript": "5.4.5",
17
- "vitest": "1.6.0"
15
+ "rimraf": "6.0.1",
16
+ "typescript": "5.5.4",
17
+ "vitest": "2.0.5"
18
18
  },
19
19
  "engines": {
20
20
  "node": ">= 18.0"
21
21
  },
22
- "exports": {
23
- ".": {
24
- "import": "./dist/esm/index.js",
25
- "require": "./dist/cjs/index.js"
26
- }
27
- },
22
+ "exports": "./dist/index.js",
28
23
  "files": [
29
24
  "dist"
30
25
  ],
@@ -36,21 +31,17 @@
36
31
  "typescript"
37
32
  ],
38
33
  "license": "GPL-3.0",
39
- "main": "dist/cjs/index.js",
40
- "module": "dist/esm/index.js",
34
+ "module": "dist/index.js",
41
35
  "name": "@ffflorian/gh-open",
42
36
  "repository": "https://github.com/ffflorian/node-packages/tree/main/packages/gh-open",
43
37
  "scripts": {
44
- "build": "yarn build:cjs && yarn build:esm && yarn generate:packagejson",
45
- "build:cjs": "tsc -p tsconfig.cjs.json",
46
- "build:esm": "tsc -p tsconfig.json",
38
+ "build": "tsc -p tsconfig.json",
47
39
  "clean": "rimraf dist",
48
40
  "dist": "yarn clean && yarn build",
49
- "generate:packagejson": "../../bin/generate-hybrid-package-json.sh",
50
41
  "start": "node --loader ts-node/esm src/cli.ts -d",
51
42
  "test": "vitest run"
52
43
  },
53
44
  "type": "module",
54
- "version": "3.6.4",
55
- "gitHead": "28c184f53a87d8eb082cc27c923ef7b352bbe875"
45
+ "version": "3.7.0",
46
+ "gitHead": "f1a74d8ec9721d5b52a00e41b2ec73278e048290"
56
47
  }
@@ -1,36 +0,0 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
- import axios from 'axios';
11
- const TWO_SECONDS_IN_MILLIS = 2000;
12
- export class GitHubClient {
13
- constructor(timeout = TWO_SECONDS_IN_MILLIS) {
14
- this.apiClient = axios.create({ baseURL: 'https://api.github.com', timeout });
15
- }
16
- getPullRequestByBranch(user, repository, branch) {
17
- return __awaiter(this, void 0, void 0, function* () {
18
- const pullRequests = yield this.getPullRequests(user, repository);
19
- return pullRequests.find(pr => !!pr.head && pr.head.ref === branch);
20
- });
21
- }
22
- /**
23
- * @see https://developer.github.com/v3/pulls/#list-pull-requests
24
- */
25
- getPullRequests(user, repository) {
26
- return __awaiter(this, void 0, void 0, function* () {
27
- const resourceUrl = `repos/${user}/${repository}/pulls`;
28
- const response = yield this.apiClient.get(resourceUrl, {
29
- params: {
30
- state: 'open',
31
- },
32
- });
33
- return response.data;
34
- });
35
- }
36
- }
@@ -1,60 +0,0 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
- import { assert, expect, describe, test } from 'vitest';
11
- import nock from 'nock';
12
- import { StatusCodes as HTTP_STATUS } from 'http-status-codes';
13
- import { GitHubClient } from './GitHubClient.js';
14
- const TEN_SECONDS_IN_MILLIS = 10000;
15
- const HALF_SECOND_IN_MILLIS = 500;
16
- describe('GitHubClient', () => {
17
- describe('getPullRequests', () => {
18
- test('cancels the request after a given time', () => __awaiter(void 0, void 0, void 0, function* () {
19
- nock('https://api.github.com')
20
- .get(/repos\/.*\/.*\/pulls/)
21
- .query(true)
22
- .delay(TEN_SECONDS_IN_MILLIS)
23
- .reply(HTTP_STATUS.OK);
24
- const gitHubClient = new GitHubClient(HALF_SECOND_IN_MILLIS);
25
- try {
26
- yield gitHubClient.getPullRequests('user', 'repository');
27
- assert.fail('Should not have resolved');
28
- }
29
- catch (error) {
30
- expect(error.message).toBe('timeout of 500ms exceeded');
31
- }
32
- finally {
33
- nock.cleanAll();
34
- }
35
- }));
36
- });
37
- describe('getPullRequestsByBranch', () => {
38
- test('correctly parses pull requests', () => __awaiter(void 0, void 0, void 0, function* () {
39
- const exampleData = [
40
- {
41
- _links: {
42
- html: {
43
- href: 'https://github.com/user/repo/pull/1234',
44
- },
45
- },
46
- head: {
47
- ref: 'branch-name',
48
- },
49
- },
50
- ];
51
- nock('https://api.github.com')
52
- .get(/repos\/.*\/.*\/pulls/)
53
- .query(true)
54
- .reply(HTTP_STATUS.OK, exampleData);
55
- const gitHubClient = new GitHubClient();
56
- const result = yield gitHubClient.getPullRequestByBranch('user', 'repository', 'branch-name');
57
- expect(result).toEqual(exampleData[0]);
58
- }));
59
- });
60
- });
@@ -1,109 +0,0 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
- import { promises as fsAsync } from 'fs';
11
- import path from 'path';
12
- import logdown from 'logdown';
13
- import { GitHubClient } from './GitHubClient.js';
14
- export class RepositoryService {
15
- constructor(options) {
16
- this.parser = {
17
- fullUrl: new RegExp('^(?:.+?://(?:.+@)?|(?:.+@)?)(.+?)[:/](.+?)(?:.git)?/?$', 'i'),
18
- gitBranch: new RegExp('ref: refs/heads/(?<branch>.*)$', 'mi'),
19
- pullRequest: new RegExp('github\\.com\\/(?<user>[^\\/]+)\\/(?<project>[^/]+)\\/tree\\/(?<branch>.*)'),
20
- rawUrl: new RegExp('.*url = (?<rawUrl>.*)', 'mi'),
21
- };
22
- this.options = Object.assign({ debug: false, timeout: 2000 }, options);
23
- this.gitHubClient = new GitHubClient(this.options.timeout);
24
- this.logger = logdown('gh-open', {
25
- logger: console,
26
- markdown: false,
27
- });
28
- this.logger.state.isEnabled = this.options.debug;
29
- }
30
- getFullUrl(gitDir) {
31
- return __awaiter(this, void 0, void 0, function* () {
32
- const rawUrl = yield this.parseGitConfig(gitDir);
33
- const gitBranch = yield this.parseGitBranch(gitDir);
34
- const match = this.parser.fullUrl.exec(rawUrl);
35
- if (!match) {
36
- const errorMessage = 'Could not convert raw URL.';
37
- throw new Error(errorMessage);
38
- }
39
- const parsedUrl = rawUrl.replace(this.parser.fullUrl, 'https://$1/$2');
40
- this.logger.info('Found parsed URL', { parsedUrl });
41
- return `${parsedUrl}/tree/${gitBranch}`;
42
- });
43
- }
44
- getPullRequestUrl(url) {
45
- return __awaiter(this, void 0, void 0, function* () {
46
- const match = this.parser.pullRequest.exec(url);
47
- if (!match || !match.groups) {
48
- const errorMessage = `Could not convert GitHub URL "${url}" to pull request`;
49
- throw new Error(errorMessage);
50
- }
51
- const { user, project, branch } = match.groups;
52
- try {
53
- const response = yield this.gitHubClient.getPullRequestByBranch(user, project, branch);
54
- if (response && response._links && response._links.html && response._links.html.href) {
55
- const pullRequestUrl = response._links.html.href;
56
- this.logger.info('Got pull request URL', { pullRequestUrl });
57
- return pullRequestUrl;
58
- }
59
- }
60
- catch (error) {
61
- this.logger.warn(`Request failed: "${error.message}"`);
62
- }
63
- });
64
- }
65
- parseGitBranch(gitDir) {
66
- return __awaiter(this, void 0, void 0, function* () {
67
- const gitHeadFile = path.join(gitDir, 'HEAD');
68
- let gitHead;
69
- try {
70
- gitHead = yield fsAsync.readFile(gitHeadFile, 'utf-8');
71
- gitHead = gitHead.trim();
72
- this.logger.info('Read git head file', { gitHead });
73
- }
74
- catch (error) {
75
- const errorMessage = `Could not find git HEAD file in "${gitDir}".`;
76
- throw new Error(errorMessage);
77
- }
78
- const match = this.parser.gitBranch.exec(gitHead);
79
- if (!match || !match.groups) {
80
- const errorMessage = `No branch found in git HEAD file: "${gitHead}"`;
81
- throw new Error(errorMessage);
82
- }
83
- return match.groups.branch;
84
- });
85
- }
86
- parseGitConfig(gitDir) {
87
- return __awaiter(this, void 0, void 0, function* () {
88
- const gitConfigFile = path.join(gitDir, 'config');
89
- let gitConfig;
90
- try {
91
- gitConfig = yield fsAsync.readFile(gitConfigFile, 'utf-8');
92
- gitConfig = gitConfig.trim();
93
- this.logger.info('Read git config file', { gitConfigFile });
94
- }
95
- catch (error) {
96
- const errorMessage = `Could not find git config file: "${gitConfigFile}"`;
97
- throw new Error(errorMessage);
98
- }
99
- const match = this.parser.rawUrl.exec(gitConfig);
100
- if (!match || !match.groups) {
101
- const errorMessage = `No URL found in git config file: "${gitConfigFile}"`;
102
- throw new Error(errorMessage);
103
- }
104
- const rawUrl = match.groups.rawUrl;
105
- this.logger.info('Found raw URL', { rawUrl });
106
- return rawUrl;
107
- });
108
- }
109
- }
package/dist/cjs/cli.js DELETED
@@ -1,57 +0,0 @@
1
- #!/usr/bin/env node
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- import path from 'path';
12
- import { program as commander } from 'commander';
13
- import { findUp } from 'find-up';
14
- import open from 'open';
15
- import { createRequire } from 'module';
16
- const require = createRequire(import.meta.url);
17
- import { RepositoryService } from './RepositoryService.js';
18
- const { description, name, version } = require('../package.json');
19
- commander
20
- .name(name.replace(/^@[^/]+\//, ''))
21
- .description(description)
22
- .option('-d, --debug', 'Enable debug logging')
23
- .option('-p, --print', 'Just print the URL')
24
- .option('-b, --branch', 'Open the branch tree (and not the PR)')
25
- .option('-t, --timeout <number>', 'Set a custom timeout for HTTP requests')
26
- .arguments('[directory]')
27
- .version(version, '-v, --version')
28
- .parse(process.argv);
29
- const resolvedBaseDir = path.resolve(commander.args[0] || '.');
30
- const commanderOptions = commander.opts();
31
- void (() => __awaiter(void 0, void 0, void 0, function* () {
32
- try {
33
- const gitDir = yield findUp('.git', { cwd: resolvedBaseDir, type: 'directory' });
34
- if (!gitDir) {
35
- throw new Error(`Could not find a git repository in "${resolvedBaseDir}".`);
36
- }
37
- const repositoryService = new RepositoryService(Object.assign(Object.assign({}, (commanderOptions.debug && { debug: commanderOptions.debug })), (commanderOptions.timeout && { timeout: parseInt(commanderOptions.timeout, 10) })));
38
- let fullUrl = yield repositoryService.getFullUrl(gitDir);
39
- if (!commanderOptions.branch) {
40
- const pullRequestUrl = yield repositoryService.getPullRequestUrl(fullUrl);
41
- if (pullRequestUrl) {
42
- fullUrl = pullRequestUrl;
43
- }
44
- }
45
- if (commanderOptions.print) {
46
- console.info(fullUrl);
47
- }
48
- else {
49
- yield open(fullUrl);
50
- }
51
- process.exit();
52
- }
53
- catch (error) {
54
- console.error(error.message);
55
- process.exit(1);
56
- }
57
- }))();
@@ -1,3 +0,0 @@
1
- {
2
- "type": "commonjs"
3
- }
@@ -1,19 +0,0 @@
1
- export interface PullRequest {
2
- _links: {
3
- html: {
4
- href: string;
5
- };
6
- };
7
- head: {
8
- ref: string;
9
- };
10
- }
11
- export declare class GitHubClient {
12
- private readonly apiClient;
13
- constructor(timeout?: number);
14
- getPullRequestByBranch(user: string, repository: string, branch: string): Promise<PullRequest | undefined>;
15
- /**
16
- * @see https://developer.github.com/v3/pulls/#list-pull-requests
17
- */
18
- getPullRequests(user: string, repository: string): Promise<PullRequest[]>;
19
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,15 +0,0 @@
1
- export interface Options {
2
- debug?: boolean;
3
- timeout?: number;
4
- }
5
- export declare class RepositoryService {
6
- private readonly gitHubClient;
7
- private readonly logger;
8
- private readonly options;
9
- private readonly parser;
10
- constructor(options?: Options);
11
- getFullUrl(gitDir: string): Promise<string>;
12
- getPullRequestUrl(url: string): Promise<string | void>;
13
- parseGitBranch(gitDir: string): Promise<string>;
14
- parseGitConfig(gitDir: string): Promise<string>;
15
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,74 +0,0 @@
1
- import { expect, describe, test } from 'vitest';
2
- import { RepositoryService } from './RepositoryService.js';
3
- describe('RepositoryService', () => {
4
- const repositoryService = new RepositoryService();
5
- describe('getFullUrl', () => {
6
- const normalizedUrl = 'https://github.com/ffflorian/gh-open';
7
- const testRegex = (str) => {
8
- const match = repositoryService['parser'].fullUrl.exec(str);
9
- expect(match[0]).toEqual(expect.any(String));
10
- const replaced = str.replace(repositoryService['parser'].fullUrl, 'https://$1/$2');
11
- expect(replaced).toBe(normalizedUrl);
12
- };
13
- test('converts complete git URLs', () => {
14
- const gitUrl = 'git@github.com:ffflorian/gh-open.git';
15
- testRegex(gitUrl);
16
- });
17
- test('converts git URLs without a suffix', () => {
18
- const gitUrl = 'git@github.com:ffflorian/gh-open';
19
- testRegex(gitUrl);
20
- });
21
- test('converts git URLs without a user', () => {
22
- const gitUrl = 'github.com:ffflorian/gh-open.git';
23
- testRegex(gitUrl);
24
- });
25
- test('converts complete https URLs', () => {
26
- const gitUrl = 'https://github.com/ffflorian/gh-open.git';
27
- testRegex(gitUrl);
28
- });
29
- test('converts https URLs without suffix', () => {
30
- const gitUrl = 'https://github.com/ffflorian/gh-open';
31
- testRegex(gitUrl);
32
- });
33
- test('converts https URLs with a username', () => {
34
- const gitUrl = 'https://git@github.com/ffflorian/gh-open.git';
35
- testRegex(gitUrl);
36
- });
37
- test('converts https URLs with a username and password', () => {
38
- const gitUrl = 'https://git:password@github.com/ffflorian/gh-open.git';
39
- testRegex(gitUrl);
40
- });
41
- });
42
- describe('parseGitConfig', () => {
43
- const rawUrl = 'git@github.com:ffflorian/gh-open.git';
44
- const testRegex = (str) => {
45
- const match = repositoryService['parser'].rawUrl.exec(str);
46
- expect(match.groups.rawUrl).toBe(rawUrl);
47
- };
48
- test('converts a normal git config', () => {
49
- const gitConfig = `[remote "origin"]
50
- url = git@github.com:ffflorian/gh-open.git
51
- fetch = +refs/heads/*:refs/remotes/origin/*
52
- [branch "main"]
53
- remote = origin
54
- merge = refs/heads/main`;
55
- testRegex(gitConfig);
56
- });
57
- describe('parseGitBranch', () => {
58
- const testRegex = (str, result) => {
59
- const match = repositoryService['parser'].gitBranch.exec(str);
60
- expect(match.groups.branch).toBe(result);
61
- };
62
- test('detects the main branch', () => {
63
- const rawBranch = 'main';
64
- const gitHead = 'ref: refs/heads/main\n';
65
- testRegex(gitHead, rawBranch);
66
- });
67
- test('detects a branch with a slash', () => {
68
- const rawBranch = 'fix/regex';
69
- const gitHead = 'ref: refs/heads/fix/regex\n';
70
- testRegex(gitHead, rawBranch);
71
- });
72
- });
73
- });
74
- });
package/dist/esm/cli.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
@@ -1 +0,0 @@
1
- export * from './RepositoryService.js';
package/dist/esm/index.js DELETED
@@ -1 +0,0 @@
1
- export * from './RepositoryService.js';
@@ -1,3 +0,0 @@
1
- {
2
- "type": "module"
3
- }
File without changes
File without changes
File without changes
File without changes