@ffflorian/gh-open 3.6.5 → 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 {
@@ -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)
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.5.2",
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.5",
55
- "gitHead": "f7a6a79286e4eb85392b5f2d33942ab166142109"
45
+ "version": "3.7.0",
46
+ "gitHead": "f1a74d8ec9721d5b52a00e41b2ec73278e048290"
56
47
  }
@@ -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,23 +0,0 @@
1
- import axios from 'axios';
2
- const TWO_SECONDS_IN_MILLIS = 2000;
3
- export class GitHubClient {
4
- constructor(timeout = TWO_SECONDS_IN_MILLIS) {
5
- this.apiClient = axios.create({ baseURL: 'https://api.github.com', timeout });
6
- }
7
- async getPullRequestByBranch(user, repository, branch) {
8
- const pullRequests = await this.getPullRequests(user, repository);
9
- return pullRequests.find(pr => !!pr.head && pr.head.ref === branch);
10
- }
11
- /**
12
- * @see https://developer.github.com/v3/pulls/#list-pull-requests
13
- */
14
- async getPullRequests(user, repository) {
15
- const resourceUrl = `repos/${user}/${repository}/pulls`;
16
- const response = await this.apiClient.get(resourceUrl, {
17
- params: {
18
- state: 'open',
19
- },
20
- });
21
- return response.data;
22
- }
23
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,51 +0,0 @@
1
- import { assert, expect, describe, test } from 'vitest';
2
- import nock from 'nock';
3
- import { StatusCodes as HTTP_STATUS } from 'http-status-codes';
4
- import { GitHubClient } from './GitHubClient.js';
5
- const TEN_SECONDS_IN_MILLIS = 10000;
6
- const HALF_SECOND_IN_MILLIS = 500;
7
- describe('GitHubClient', () => {
8
- describe('getPullRequests', () => {
9
- test('cancels the request after a given time', async () => {
10
- nock('https://api.github.com')
11
- .get(/repos\/.*\/.*\/pulls/)
12
- .query(true)
13
- .delay(TEN_SECONDS_IN_MILLIS)
14
- .reply(HTTP_STATUS.OK);
15
- const gitHubClient = new GitHubClient(HALF_SECOND_IN_MILLIS);
16
- try {
17
- await gitHubClient.getPullRequests('user', 'repository');
18
- assert.fail('Should not have resolved');
19
- }
20
- catch (error) {
21
- expect(error.message).toBe('timeout of 500ms exceeded');
22
- }
23
- finally {
24
- nock.cleanAll();
25
- }
26
- });
27
- });
28
- describe('getPullRequestsByBranch', () => {
29
- test('correctly parses pull requests', async () => {
30
- const exampleData = [
31
- {
32
- _links: {
33
- html: {
34
- href: 'https://github.com/user/repo/pull/1234',
35
- },
36
- },
37
- head: {
38
- ref: 'branch-name',
39
- },
40
- },
41
- ];
42
- nock('https://api.github.com')
43
- .get(/repos\/.*\/.*\/pulls/)
44
- .query(true)
45
- .reply(HTTP_STATUS.OK, exampleData);
46
- const gitHubClient = new GitHubClient();
47
- const result = await gitHubClient.getPullRequestByBranch('user', 'repository', 'branch-name');
48
- expect(result).toEqual(exampleData[0]);
49
- });
50
- });
51
- });
@@ -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,92 +0,0 @@
1
- import { promises as fsAsync } from 'fs';
2
- import path from 'path';
3
- import logdown from 'logdown';
4
- import { GitHubClient } from './GitHubClient.js';
5
- export class RepositoryService {
6
- constructor(options) {
7
- this.parser = {
8
- fullUrl: new RegExp('^(?:.+?://(?:.+@)?|(?:.+@)?)(.+?)[:/](.+?)(?:.git)?/?$', 'i'),
9
- gitBranch: new RegExp('ref: refs/heads/(?<branch>.*)$', 'mi'),
10
- pullRequest: new RegExp('github\\.com\\/(?<user>[^\\/]+)\\/(?<project>[^/]+)\\/tree\\/(?<branch>.*)'),
11
- rawUrl: new RegExp('.*url = (?<rawUrl>.*)', 'mi'),
12
- };
13
- this.options = { debug: false, timeout: 2000, ...options };
14
- this.gitHubClient = new GitHubClient(this.options.timeout);
15
- this.logger = logdown('gh-open', {
16
- logger: console,
17
- markdown: false,
18
- });
19
- this.logger.state.isEnabled = this.options.debug;
20
- }
21
- async getFullUrl(gitDir) {
22
- const rawUrl = await this.parseGitConfig(gitDir);
23
- const gitBranch = await this.parseGitBranch(gitDir);
24
- const match = this.parser.fullUrl.exec(rawUrl);
25
- if (!match) {
26
- const errorMessage = 'Could not convert raw URL.';
27
- throw new Error(errorMessage);
28
- }
29
- const parsedUrl = rawUrl.replace(this.parser.fullUrl, 'https://$1/$2');
30
- this.logger.info('Found parsed URL', { parsedUrl });
31
- return `${parsedUrl}/tree/${gitBranch}`;
32
- }
33
- async getPullRequestUrl(url) {
34
- const match = this.parser.pullRequest.exec(url);
35
- if (!match || !match.groups) {
36
- const errorMessage = `Could not convert GitHub URL "${url}" to pull request`;
37
- throw new Error(errorMessage);
38
- }
39
- const { user, project, branch } = match.groups;
40
- try {
41
- const response = await this.gitHubClient.getPullRequestByBranch(user, project, branch);
42
- if (response && response._links && response._links.html && response._links.html.href) {
43
- const pullRequestUrl = response._links.html.href;
44
- this.logger.info('Got pull request URL', { pullRequestUrl });
45
- return pullRequestUrl;
46
- }
47
- }
48
- catch (error) {
49
- this.logger.warn(`Request failed: "${error.message}"`);
50
- }
51
- }
52
- async parseGitBranch(gitDir) {
53
- const gitHeadFile = path.join(gitDir, 'HEAD');
54
- let gitHead;
55
- try {
56
- gitHead = await fsAsync.readFile(gitHeadFile, 'utf-8');
57
- gitHead = gitHead.trim();
58
- this.logger.info('Read git head file', { gitHead });
59
- }
60
- catch (error) {
61
- const errorMessage = `Could not find git HEAD file in "${gitDir}".`;
62
- throw new Error(errorMessage);
63
- }
64
- const match = this.parser.gitBranch.exec(gitHead);
65
- if (!match || !match.groups) {
66
- const errorMessage = `No branch found in git HEAD file: "${gitHead}"`;
67
- throw new Error(errorMessage);
68
- }
69
- return match.groups.branch;
70
- }
71
- async parseGitConfig(gitDir) {
72
- const gitConfigFile = path.join(gitDir, 'config');
73
- let gitConfig;
74
- try {
75
- gitConfig = await fsAsync.readFile(gitConfigFile, 'utf-8');
76
- gitConfig = gitConfig.trim();
77
- this.logger.info('Read git config file', { gitConfigFile });
78
- }
79
- catch (error) {
80
- const errorMessage = `Could not find git config file: "${gitConfigFile}"`;
81
- throw new Error(errorMessage);
82
- }
83
- const match = this.parser.rawUrl.exec(gitConfig);
84
- if (!match || !match.groups) {
85
- const errorMessage = `No URL found in git config file: "${gitConfigFile}"`;
86
- throw new Error(errorMessage);
87
- }
88
- const rawUrl = match.groups.rawUrl;
89
- this.logger.info('Found raw URL', { rawUrl });
90
- return rawUrl;
91
- }
92
- }
@@ -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 {};
package/dist/esm/cli.js DELETED
@@ -1,51 +0,0 @@
1
- #!/usr/bin/env node
2
- import path from 'path';
3
- import { program as commander } from 'commander';
4
- import { findUp } from 'find-up';
5
- import open from 'open';
6
- import { createRequire } from 'module';
7
- const require = createRequire(import.meta.url);
8
- import { RepositoryService } from './RepositoryService.js';
9
- const { description, name, version } = require('../package.json');
10
- commander
11
- .name(name.replace(/^@[^/]+\//, ''))
12
- .description(description)
13
- .option('-d, --debug', 'Enable debug logging')
14
- .option('-p, --print', 'Just print the URL')
15
- .option('-b, --branch', 'Open the branch tree (and not the PR)')
16
- .option('-t, --timeout <number>', 'Set a custom timeout for HTTP requests')
17
- .arguments('[directory]')
18
- .version(version, '-v, --version')
19
- .parse(process.argv);
20
- const resolvedBaseDir = path.resolve(commander.args[0] || '.');
21
- const commanderOptions = commander.opts();
22
- void (async () => {
23
- try {
24
- const gitDir = await findUp('.git', { cwd: resolvedBaseDir, type: 'directory' });
25
- if (!gitDir) {
26
- throw new Error(`Could not find a git repository in "${resolvedBaseDir}".`);
27
- }
28
- const repositoryService = new RepositoryService({
29
- ...(commanderOptions.debug && { debug: commanderOptions.debug }),
30
- ...(commanderOptions.timeout && { timeout: parseInt(commanderOptions.timeout, 10) }),
31
- });
32
- let fullUrl = await repositoryService.getFullUrl(gitDir);
33
- if (!commanderOptions.branch) {
34
- const pullRequestUrl = await repositoryService.getPullRequestUrl(fullUrl);
35
- if (pullRequestUrl) {
36
- fullUrl = pullRequestUrl;
37
- }
38
- }
39
- if (commanderOptions.print) {
40
- console.info(fullUrl);
41
- }
42
- else {
43
- await open(fullUrl);
44
- }
45
- process.exit();
46
- }
47
- catch (error) {
48
- console.error(error.message);
49
- process.exit(1);
50
- }
51
- })();
@@ -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