Skip to content

Commit aa70713

Browse files
committed
add filter tokens, list examples, more metadata in actions
1 parent fff41a2 commit aa70713

File tree

5 files changed

+107
-14
lines changed

5 files changed

+107
-14
lines changed

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
<dd align="center"><img alt="React Native Directory Logo" height="96" src="./assets/icon.png" /></dd>
22
<h1 align="center">vscode-react-native-directory</h1>
33

4-
A VS Code extension allowing to browse through React Native Directory and perform actions on the packages inside build-in Command Palette.
4+
A VS Code extension allowing to browse through React Native Directory and perform actions related to the chosen package
5+
inside build-in Command Palette.
56

67
<p align="center">
7-
<img alt="Extension preview" src="./assets/screenshot.png" width="520" align="center" />
8+
<img alt="Search mode" src="./assets/screenshot.png" width="500" align="center" />
9+
<img alt="Actions list" src="./assets/screenshot-actions.png" width="500" align="center" />
810
</p>
911

12+
## ⚡️ Features
13+
14+
* Search through the packages registered in the React Native Directory.
15+
* Narrow down the results by using filter tokens, such as `:ios`, `:newArchitecture`, or `:hasTypes`.
16+
* Valid tokens are a subset of all possible API query options, and the values can be seen [in this file](https://github.com/react-native-community/vscode-react-native-directory/blob/main/src/utils.ts#L33).
17+
* Install the selected packages in the current workspace using your preferred package manager.
18+
* Dive deep into the stats and analysis with the provided metadata and links.
19+
20+
## Preview
21+
1022
## 📦 Installation
1123

1224
> [!tip]

assets/screenshot-actions.png

107 KB
Loading

src/extension.ts

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@ import * as vscode from 'vscode';
33
import { QuickPickItemKind } from 'vscode';
44
import preferredPM from 'preferred-pm';
55
import { DirectoryEntry } from './types';
6-
import { ENTRY_OPTION, fetchData, getCommandToRun, STRINGS } from './utils';
6+
import {
7+
ENTRY_OPTION,
8+
fetchData,
9+
getCommandToRun,
10+
KEYWORD_REGEX,
11+
numberFormatter,
12+
STRINGS,
13+
VALID_KEYWORDS_MAP,
14+
ValidKeyword
15+
} from './utils';
716

817
export async function activate(context: vscode.ExtensionContext) {
918
const workspacePath = vscode.workspace.workspaceFolders?.[0].uri.fsPath ?? vscode.workspace.rootPath;
@@ -16,7 +25,7 @@ export async function activate(context: vscode.ExtensionContext) {
1625
const packagesPick = vscode.window.createQuickPick<DirectoryEntry>();
1726

1827
packagesPick.placeholder = STRINGS.PLACEHOLDER_BUSY;
19-
packagesPick.title = 'Search in React Native Directory';
28+
packagesPick.title = STRINGS.DEFAULT_TITLE;
2029
packagesPick.matchOnDescription = false;
2130
packagesPick.matchOnDetail = false;
2231
packagesPick.busy = true;
@@ -29,31 +38,72 @@ export async function activate(context: vscode.ExtensionContext) {
2938

3039
packagesPick.onDidChangeValue(async (value) => {
3140
packagesPick.busy = true;
32-
packagesPick.title = 'Search in React Native Directory';
33-
packagesPick.items = await fetchData(value);
41+
42+
if (value.includes(':')) {
43+
const keywords = (value.match(KEYWORD_REGEX) ?? []).map((token) => token.slice(1));
44+
const searchString = value.replace(KEYWORD_REGEX, '').trim();
45+
46+
const validKeywords = keywords
47+
.map((keyword) => keyword.toLowerCase())
48+
.filter((keyword): keyword is ValidKeyword => keyword in VALID_KEYWORDS_MAP)
49+
.map((keyword) => VALID_KEYWORDS_MAP[keyword] as ValidKeyword);
50+
51+
if (validKeywords.length > 0) {
52+
packagesPick.title = `Active filters: ${validKeywords.join(', ')}`;
53+
} else {
54+
packagesPick.title = STRINGS.DEFAULT_TITLE;
55+
}
56+
packagesPick.items = await fetchData(searchString, validKeywords);
57+
} else {
58+
packagesPick.items = await fetchData(value);
59+
}
60+
3461
packagesPick.busy = false;
3562
});
3663

3764
packagesPick.onDidAccept(() => {
3865
const selectedEntry = packagesPick.selectedItems[0];
3966

67+
const examplesActions =
68+
selectedEntry.examples && selectedEntry.examples.length > 0
69+
? [
70+
{ label: 'view examples', kind: QuickPickItemKind.Separator },
71+
...selectedEntry.examples.map((example, index) => ({
72+
label: `Example #${index + 1}`,
73+
description: example
74+
}))
75+
]
76+
: [];
77+
4078
const possibleActions = [
4179
workspacePath && {
4280
label: ENTRY_OPTION.INSTALL,
4381
description: `with ${preferredManager}${selectedEntry.dev ? ' as devDependency' : ''}`
4482
},
4583
{ label: `open URLs`, kind: QuickPickItemKind.Separator },
84+
{
85+
label: ENTRY_OPTION.VISIT_REPO,
86+
description: [
87+
`$(star) ${numberFormatter.format(selectedEntry.github.stats.stars)}`,
88+
`$(gist-fork) ${numberFormatter.format(selectedEntry.github.stats.forks)}`
89+
].join(' ')
90+
},
91+
{
92+
label: ENTRY_OPTION.VISIT_NPM,
93+
description: selectedEntry.npm?.downloads
94+
? `$(arrow-circle-down) ${numberFormatter.format(selectedEntry.npm.downloads)}`
95+
: ''
96+
},
4697
selectedEntry.github.urls.homepage && {
4798
label: ENTRY_OPTION.VISIT_HOMEPAGE,
4899
description: selectedEntry.github.urls.homepage
49100
},
50-
{ label: ENTRY_OPTION.VISIT_REPO },
51-
{ label: ENTRY_OPTION.VISIT_NPM },
52-
{ label: ENTRY_OPTION.VIEW_BUNDLEPHOBIA },
53101
selectedEntry.github.license && {
54102
label: ENTRY_OPTION.VIEW_LICENSE,
55103
description: selectedEntry.github.license.name
56104
},
105+
{ label: ENTRY_OPTION.VIEW_BUNDLEPHOBIA },
106+
...examplesActions,
57107
{ label: 'copy data', kind: QuickPickItemKind.Separator },
58108
{ label: ENTRY_OPTION.COPY_NAME },
59109
{ label: ENTRY_OPTION.COPY_REPO_URL },

src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { type QuickPickItem } from 'vscode';
33
export type DirectoryEntry = QuickPickItem & PackageData;
44

55
/**
6-
* Mirror of https://github.com/react-native-community/directory/blob/main/types/index.ts#L41
6+
* Mirror of React Native Directory library type.
7+
* @see https://github.com/react-native-community/directory/blob/main/types/index.ts#L41
78
*/
89
export type PackageData = {
910
githubUrl: string;

src/utils.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import axios from 'axios';
33

44
import { DirectoryEntry, PackageData } from './types';
55

6-
const BASE_API_URL = 'https://reactnative.directory/api/libraries';
6+
export const BASE_API_URL = 'https://reactnative.directory/api/libraries';
7+
export const KEYWORD_REGEX = /:\w+/g;
78

89
export const numberFormatter = new Intl.NumberFormat('en-EN', { notation: 'compact' });
910

@@ -21,10 +22,35 @@ export enum ENTRY_OPTION {
2122
}
2223

2324
export enum STRINGS {
25+
DEFAULT_TITLE = 'Search in React Native Directory',
2426
PLACEHOLDER_BUSY = 'Loading directory data...',
2527
PLACEHOLDER = 'Search for a package'
2628
}
2729

30+
/**
31+
* A subset of API query params mapped with normalized (lowercased) keyword values.
32+
* @see https://github.com/react-native-community/directory/blob/main/types/index.ts#L14
33+
*/
34+
export const VALID_KEYWORDS_MAP = {
35+
android: 'android',
36+
expogo: 'expoGo',
37+
ios: 'ios',
38+
macos: 'macos',
39+
fireos: 'fireos',
40+
tvos: 'tvos',
41+
visionos: 'visionos',
42+
web: 'web',
43+
windows: 'windows',
44+
hasexample: 'hasExample',
45+
hasimage: 'hasImage',
46+
hastypes: 'hasTypes',
47+
ismaintained: 'isMaintained',
48+
ispopular: 'isPopular',
49+
wasrecentlyupdated: 'wasRecentlyUpdated',
50+
newarchitecture: 'newArchitecture'
51+
};
52+
export type ValidKeyword = keyof typeof VALID_KEYWORDS_MAP;
53+
2854
function getDetailLabel(item: PackageData) {
2955
const platforms = [
3056
item.android ? 'Android' : null,
@@ -42,8 +68,8 @@ function getDetailLabel(item: PackageData) {
4268
item.npm?.downloads && `$(arrow-circle-down) ${numberFormatter.format(item.npm.downloads)}`,
4369
'•',
4470
platforms.join(', '),
45-
(item.newArchitecture || item.github.hasTypes) && '•',
46-
item.newArchitecture && `$(verified) New Architecture`,
71+
(item.newArchitecture || item.expoGo || item.github.hasTypes) && '•',
72+
(item.newArchitecture || item.expoGo) && `$(verified) New Architecture`,
4773
item.github.hasTypes && `$(symbol-type-parameter) Types`
4874
]
4975
.filter(Boolean)
@@ -61,7 +87,7 @@ export function getCommandToRun({ dev, npmPkg }: DirectoryEntry, preferredManage
6187
}
6288
}
6389

64-
export async function fetchData(query?: string): Promise<DirectoryEntry[]> {
90+
export async function fetchData(query?: string, keywords?: ValidKeyword[]): Promise<DirectoryEntry[]> {
6591
try {
6692
const apiUrl = new URL(BASE_API_URL);
6793

@@ -70,6 +96,10 @@ export async function fetchData(query?: string): Promise<DirectoryEntry[]> {
7096
apiUrl.searchParams.append('order', 'downloads');
7197
}
7298

99+
if (keywords) {
100+
keywords.forEach((keyword) => apiUrl.searchParams.append(keyword, 'true'));
101+
}
102+
73103
const { data } = await axios.get(apiUrl.href);
74104

75105
if ('libraries' in data && Array.isArray(data.libraries)) {

0 commit comments

Comments
 (0)