├── .eslintignore
├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── auto-imports.d.ts
├── cli.mjs
├── components.d.ts
├── logo.svg
├── package.json
├── pnpm-lock.yaml
├── src
├── App.vue
├── components
│ ├── Deps.vue
│ ├── Download.vue
│ ├── Guide.vue
│ ├── Header.vue
│ ├── Input.ts
│ ├── InputBox.vue
│ └── Package.vue
├── main.ts
├── services
│ ├── algolia.ts
│ ├── ni.ts
│ └── npm-registry.ts
└── store
│ ├── index.ts
│ └── search.ts
├── tsconfig.json
├── tsconfig.node.json
├── vite-env.d.ts
├── vite.config.ts
└── vtui-npm.gif
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .history
4 | temp
5 | **/*.log
6 | **/.DS_Store
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@antfu",
3 | "rules":{
4 | "no-console":0,
5 | "@typescript-eslint/ban-ts-comment":0,
6 | "vue/one-component-per-file":0,
7 | "vue/attribute-hyphenation": 0
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist/
3 | .eslintcache
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 webfansplz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Interactive CLI for npm - search and install JavaScript package. Powered by vue-termui .
12 |
13 |
14 |
15 | ## 📺 Preview
16 |
17 |
18 |
19 |
20 |
21 | ## 🔥 Features
22 |
23 | - 🔍 **Powerful Search:** Search all the packages from npm and Yarn.
24 | - 👼 **User Friendly:** Search and install packages with a simple keystroke.
25 | - 📦 **Powerful Installer:** Support npm · yarn · pnpm · bun.
26 |
27 | ## 📦 Install
28 |
29 | ```sh
30 | npm install vtui-npm
31 | ```
32 |
33 | ## 🎮 Usage
34 |
35 | Open the terminal and then typing `vnpm`.
36 |
37 | ```sh
38 | vnpm
39 | ```
40 |
41 | ```sh
42 | # Switch to npm registry for search (algolia by default),we recommend using the algolia search.
43 | vnpm -n | vnpm --npm
44 | ```
45 |
46 |
47 |
48 | ## 💗 Credits
49 |
50 | - Search powered by [Algolia](https://github.com/algolia/algoliasearch-client-javascript).
51 | - Installer powered by [@antfu/ni](https://github.com/antfu/ni).
52 | - Terminal UI powered by [vue-termui](https://github.com/vue-terminal/vue-termui).
53 |
54 |
55 | ## 📄 License
56 |
57 | [MIT](./LICENSE)
58 |
--------------------------------------------------------------------------------
/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by 'unplugin-auto-import'
2 | export {}
3 | declare global {
4 | const EffectScope: typeof import('vue-termui')['EffectScope']
5 | const MouseEventType: typeof import('vue-termui')['MouseEventType']
6 | const computed: typeof import('vue-termui')['computed']
7 | const createApp: typeof import('vue-termui')['createApp']
8 | const customRef: typeof import('vue-termui')['customRef']
9 | const defineAsyncComponent: typeof import('vue-termui')['defineAsyncComponent']
10 | const defineComponent: typeof import('vue-termui')['defineComponent']
11 | const effectScope: typeof import('vue-termui')['effectScope']
12 | const getCurrentInstance: typeof import('vue-termui')['getCurrentInstance']
13 | const getCurrentScope: typeof import('vue-termui')['getCurrentScope']
14 | const h: typeof import('vue-termui')['h']
15 | const inject: typeof import('vue-termui')['inject']
16 | const inputDataToString: typeof import('vue-termui')['inputDataToString']
17 | const isInputDataEvent: typeof import('vue-termui')['isInputDataEvent']
18 | const isKeyDataEvent: typeof import('vue-termui')['isKeyDataEvent']
19 | const isMouseDataEvent: typeof import('vue-termui')['isMouseDataEvent']
20 | const isReadonly: typeof import('vue-termui')['isReadonly']
21 | const isRef: typeof import('vue-termui')['isRef']
22 | const markRaw: typeof import('vue-termui')['markRaw']
23 | const nextTick: typeof import('vue-termui')['nextTick']
24 | const onActivated: typeof import('vue-termui')['onActivated']
25 | const onBeforeMount: typeof import('vue-termui')['onBeforeMount']
26 | const onBeforeUnmount: typeof import('vue-termui')['onBeforeUnmount']
27 | const onBeforeUpdate: typeof import('vue-termui')['onBeforeUpdate']
28 | const onDeactivated: typeof import('vue-termui')['onDeactivated']
29 | const onErrorCaptured: typeof import('vue-termui')['onErrorCaptured']
30 | const onInputData: typeof import('vue-termui')['onInputData']
31 | const onKeyData: typeof import('vue-termui')['onKeyData']
32 | const onMounted: typeof import('vue-termui')['onMounted']
33 | const onMouseData: typeof import('vue-termui')['onMouseData']
34 | const onRenderTracked: typeof import('vue-termui')['onRenderTracked']
35 | const onRenderTriggered: typeof import('vue-termui')['onRenderTriggered']
36 | const onScopeDispose: typeof import('vue-termui')['onScopeDispose']
37 | const onServerPrefetch: typeof import('vue-termui')['onServerPrefetch']
38 | const onUnmounted: typeof import('vue-termui')['onUnmounted']
39 | const onUpdated: typeof import('vue-termui')['onUpdated']
40 | const provide: typeof import('vue-termui')['provide']
41 | const reactive: typeof import('vue-termui')['reactive']
42 | const readonly: typeof import('vue-termui')['readonly']
43 | const ref: typeof import('vue-termui')['ref']
44 | const resolveComponent: typeof import('vue-termui')['resolveComponent']
45 | const shallowReactive: typeof import('vue-termui')['shallowReactive']
46 | const shallowReadonly: typeof import('vue-termui')['shallowReadonly']
47 | const shallowRef: typeof import('vue-termui')['shallowRef']
48 | const toRaw: typeof import('vue-termui')['toRaw']
49 | const toRef: typeof import('vue-termui')['toRef']
50 | const toRefs: typeof import('vue-termui')['toRefs']
51 | const triggerRef: typeof import('vue-termui')['triggerRef']
52 | const unref: typeof import('vue-termui')['unref']
53 | const useAttrs: typeof import('vue-termui')['useAttrs']
54 | const useInterval: typeof import('vue-termui')['useInterval']
55 | const useLog: typeof import('vue-termui')['useLog']
56 | const useRootNode: typeof import('vue-termui')['useRootNode']
57 | const useSlots: typeof import('vue-termui')['useSlots']
58 | const useTimeout: typeof import('vue-termui')['useTimeout']
59 | const watch: typeof import('vue-termui')['watch']
60 | const watchEffect: typeof import('vue-termui')['watchEffect']
61 | }
62 |
--------------------------------------------------------------------------------
/cli.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import('./dist/main.mjs')
3 |
--------------------------------------------------------------------------------
/components.d.ts:
--------------------------------------------------------------------------------
1 | // generated by unplugin-vue-components
2 | // We suggest you to commit this file into source control
3 | // Read more: https://github.com/vuejs/core/pull/3399
4 | import '@vue/runtime-core'
5 |
6 | export {}
7 |
8 | declare module '@vue/runtime-core' {
9 | export interface GlobalComponents {
10 | Br: typeof import('vue-termui')['TuiNewline']
11 | Deps: typeof import('./src/components/Deps.vue')['default']
12 | Div: typeof import('vue-termui')['TuiBox']
13 | Download: typeof import('./src/components/Download.vue')['default']
14 | Guide: typeof import('./src/components/Guide.vue')['default']
15 | Header: typeof import('./src/components/Header.vue')['default']
16 | InputBox: typeof import('./src/components/InputBox.vue')['default']
17 | Link: typeof import('vue-termui')['TuiLink']
18 | Package: typeof import('./src/components/Package.vue')['default']
19 | Progressbar: typeof import('vue-termui')['TuiProgressBar']
20 | Span: typeof import('vue-termui')['TuiText']
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vtui-npm",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "packageManager": "pnpm@7.9.0",
6 | "description": "Interactive CLI for npm - search and install JavaScript package.",
7 | "author": "webfansplz",
8 | "license": "MIT",
9 | "homepage": "https://github.com/webfansplz/vtui-npm#readme",
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/webfansplz/vtui-npm.git"
13 | },
14 | "keywords": [
15 | "vue",
16 | "vite",
17 | "terminal",
18 | "vue-termui",
19 | "npm",
20 | "vue-npm"
21 | ],
22 | "bin": {
23 | "vnpm": "./cli.mjs"
24 | },
25 | "files": [
26 | "dist/*.mjs"
27 | ],
28 | "scripts": {
29 | "dev": "vtui dev",
30 | "build": "vtui build",
31 | "lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx,.md,.json --max-warnings 0 --cache",
32 | "lint:fix": "pnpm run lint --fix",
33 | "release": "pnpm run build && npm publish"
34 | },
35 | "dependencies": {
36 | "@algolia/requester-node-http": "^4.14.2",
37 | "@antfu/ni": "^0.18.3",
38 | "@vue/runtime-core": "^3.2.41",
39 | "algoliasearch": "^4.14.2",
40 | "chalk": "^5.1.2",
41 | "execa": "^6.1.0",
42 | "ohmyfetch": "^0.4.21",
43 | "pinia": "^2.0.23",
44 | "vue": "^3.2.41",
45 | "vue-termui": "^0.0.17"
46 | },
47 | "devDependencies": {
48 | "@antfu/eslint-config": "^0.29.3",
49 | "@types/node": "^18.11.9",
50 | "@vitejs/plugin-vue": "3.2.0",
51 | "@vue-termui/cli": "^0.0.17",
52 | "eslint": "^8.26.0",
53 | "typescript": "^4.8.4",
54 | "unplugin-auto-import": "^0.11.2",
55 | "unplugin-vue-components": "^0.22.8",
56 | "vite": "3.2.2",
57 | "vite-plugin-vue-termui": "^0.0.11"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/Deps.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
16 | Dependencies
17 |
18 | {{ item.name }}@{{ item.version }}
19 |
20 |
21 |
27 | DevDependencies
28 |
29 | {{ item.name }}@{{ item.version }}
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/components/Download.vue:
--------------------------------------------------------------------------------
1 |
47 |
48 |
49 |
55 |
58 |
59 | Downloading:
60 |
61 |
62 |
63 |
64 |
65 | Download Success 🎉
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/components/Guide.vue:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
12 |
13 |
14 | Usage guide:
15 |
16 |
17 | Select Package: ↑↓ + `space`
18 |
19 | Toggle Version: → + ↑↓
20 |
21 | Install Package: `enter`
22 |
23 |
24 |
30 |
31 |
32 | Credits:
33 |
34 |
35 | Search powered by Algolia
36 |
37 | Installer powered by @antfu/ni
38 |
39 | Terminal UI powered by vue-termui
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/components/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | VTUI NPM
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/components/Input.ts:
--------------------------------------------------------------------------------
1 | import { computed, defineComponent, h, ref } from '@vue/runtime-core'
2 | import type { PropType } from '@vue/runtime-core'
3 | import type { KeyDataEvent } from 'vue-termui'
4 | import { TuiText, onInputData } from 'vue-termui'
5 | import chalk from 'chalk'
6 |
7 | const SKIP_EVENT_KEY = ['ArrowUp', 'ArrowDown', 'Ctrl', 'Tab', 'Shift', ' ', 'ArrowLeft', 'ArrowRight', 'Enter']
8 |
9 | export const Input = defineComponent({
10 | props: {
11 | placeholder: {
12 | type: String,
13 | default: '',
14 | },
15 | modelValue: {
16 | type: String,
17 | required: true,
18 | },
19 | type: {
20 | type: String as PropType<'text' | 'password'>,
21 | default: 'text',
22 | },
23 | },
24 | emits: ['update:modelValue'],
25 | setup(props, { emit }) {
26 | const active = ref(true)
27 | const content = computed(() => {
28 | if (active.value) {
29 | if (props.modelValue) {
30 | return (
31 | props.modelValue
32 | + chalk.inverse(' ')
33 | )
34 | }
35 | else {
36 | return props.placeholder ? '' : chalk.inverse(' ')
37 | }
38 | }
39 | else {
40 | return props.modelValue
41 | }
42 | })
43 |
44 | function updateValue(value: string) {
45 | emit('update:modelValue', value)
46 | }
47 |
48 | onInputData(({ data, event }) => {
49 | if (!active.value)
50 | return
51 | const eventKey = (event!).key
52 | if (SKIP_EVENT_KEY.includes(eventKey) || !eventKey)
53 | return
54 |
55 | // Delete Content
56 | if (
57 | eventKey === 'Backspace'
58 | || eventKey === 'Delete'
59 | || (eventKey === 'H' && data !== 'H') // Windows compatible
60 | ) {
61 | props.modelValue && updateValue(
62 | props.modelValue.slice(0, props.modelValue.length - 1),
63 | )
64 | }
65 | // Typing Content
66 | else {
67 | updateValue(
68 | props.modelValue
69 | + data,
70 | )
71 | }
72 | })
73 |
74 | return () =>
75 | props.placeholder && !props.modelValue
76 | ? h(
77 | TuiText,
78 | {
79 | dimmed: true,
80 | },
81 | () => props.placeholder,
82 | )
83 | : h(TuiText, () => content.value)
84 | },
85 | })
86 |
--------------------------------------------------------------------------------
/src/components/InputBox.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/Package.vue:
--------------------------------------------------------------------------------
1 |
108 |
109 |
110 |
115 |
118 |
119 |
120 |
121 |
128 |
129 | {{ item.name }}
130 |
131 |
132 |
133 |
134 |
135 |
139 |
140 |
141 |
142 | ❯
143 | ◉
144 | ◉
145 |
146 |
147 |
154 |
155 |
161 | ❯
167 |
168 |
169 | {{ item[column.id] }}
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue-termui'
2 | import { createPinia } from 'pinia'
3 | import App from './App.vue'
4 | const pinia = createPinia()
5 | const app = createApp(App, { swapScreens: process.env.NODE_ENV === 'production' })
6 | app.use(pinia)
7 | app.mount()
8 |
9 | process.on('exit', () => {
10 | app.unmount()
11 | })
12 |
--------------------------------------------------------------------------------
/src/services/algolia.ts:
--------------------------------------------------------------------------------
1 | import algoliasearch from 'algoliasearch'
2 | import { createNodeHttpRequester } from '@algolia/requester-node-http'
3 |
4 | export interface PackageInfo {
5 | name: string
6 | version: string
7 | descriptions: string
8 | owner: {
9 | name: string
10 | link: string
11 | }
12 | repository: {
13 | url: string
14 | }
15 | humanDownloadsLast30Days: string
16 | versions: string[]
17 | author: string
18 | downloads: string
19 | versionIndex: number
20 | activeVersion: string
21 | repoLink: string
22 | authorLink: string
23 | }
24 |
25 | /* Config */
26 | const algolia = {
27 | appId: 'OFCNCOG2CU',
28 | apiKey: 'f54e21fa3a2a0160595bb058179bfb1e',
29 | indexName: 'npm-search',
30 | }
31 |
32 | const client = algoliasearch(algolia.appId, algolia.apiKey, {
33 | requester: createNodeHttpRequester(),
34 | }).initIndex(
35 | algolia.indexName,
36 | )
37 |
38 | export const search = async (
39 | query: string,
40 | page = 0,
41 | ) => {
42 | const res = await client.search(query, {
43 | attributesToRetrieve: [
44 | 'name',
45 | 'version',
46 | 'description',
47 | 'owner',
48 | 'repository',
49 | 'humanDownloadsLast30Days',
50 | 'versions',
51 | ],
52 | page,
53 | hitsPerPage: 10,
54 | }) as unknown as { hits: PackageInfo[]; query: string }
55 |
56 | return res
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/src/services/ni.ts:
--------------------------------------------------------------------------------
1 | import { detect, parseNi, parseNr } from '@antfu/ni'
2 | import { execaCommand } from 'execa'
3 |
4 | export interface InstallPackageOptions {
5 |
6 | /**
7 | * Whether to DevDependencies
8 | * @default false
9 | */
10 | isDev?: boolean
11 | /**
12 | Current working directory.
13 |
14 | Using a `URL` is only supported in Node.js `14.18.0`, `16.14.0` or above.
15 |
16 | @default process.cwd()
17 | */
18 | cwd?: string
19 | }
20 |
21 | export async function installPackage(packages: string[], options: InstallPackageOptions = {}) {
22 | const {
23 | isDev = false,
24 | cwd = process.cwd(),
25 | } = options
26 | const agent = await detect({})
27 |
28 | const command = await parseNi(agent!, [...(isDev ? ['-D'] : []), ...packages])
29 |
30 | await execaCommand(command!, { stdio: 'pipe', encoding: 'utf-8', cwd })
31 | }
32 |
33 | export async function execScript(script: string) {
34 | const agent = await detect({})
35 | const command = await parseNr(agent!, [script])
36 | await execaCommand(command!, { stdio: 'inherit', encoding: 'utf-8' })
37 | }
38 |
--------------------------------------------------------------------------------
/src/services/npm-registry.ts:
--------------------------------------------------------------------------------
1 | import { $fetch } from 'ohmyfetch'
2 |
3 | interface PackageInfo {
4 | name: string
5 | version: string
6 | description: string
7 | keywords: string[]
8 | date: string
9 | author?: {
10 | name?: string
11 | email?: string
12 | url?: string
13 | }
14 | links?: {
15 | npm?: string
16 | homepage?: string
17 | repository?: string
18 | bugs?: string
19 | }
20 | publisher?: {
21 | username: string
22 | email: string
23 | }
24 | maintainers?: {
25 | username: string
26 | email: string
27 | }[]
28 | }
29 |
30 | export interface NpmPackageInfo {
31 | package: PackageInfo
32 | score: {
33 | final: number
34 | detail: {
35 | quality: number
36 | popularity: number
37 | maintenance: number
38 | }
39 | }
40 | searchScore: number
41 | }
42 |
43 | let controller: AbortController | null = null
44 |
45 | export const search = async (
46 | query: string,
47 | page = 10,
48 | ) => {
49 | controller?.abort()
50 | controller = new AbortController()
51 | const signal = controller.signal
52 | const res = await $fetch(`http://registry.npmjs.com/-/v1/search?text=${query}&from=${page}`, { signal }).catch(() => {
53 | return { count: 0 }
54 | })
55 | if (res.count <= 0)
56 | return { query, hits: [] }
57 |
58 | return { query, hits: res.objects }
59 | }
60 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './search'
2 |
--------------------------------------------------------------------------------
/src/store/search.ts:
--------------------------------------------------------------------------------
1 | import { ref } from '@vue/runtime-core'
2 | import { defineStore } from 'pinia'
3 | import { search as algoliaSearch } from '@/services/algolia'
4 | import { search as npmSearch } from '@/services/npm-registry'
5 | import type { PackageInfo } from '@/services/algolia'
6 | import type { NpmPackageInfo } from '@/services/npm-registry'
7 |
8 | interface DepsInfo {
9 | name: string
10 | version: string
11 | }
12 |
13 | // for algolia registry
14 | function normalizePackages(data: PackageInfo[]) {
15 | const value = data.map((item) => {
16 | const versions = item.versions as unknown as Record
17 | const normalizedVersions = Object.keys(versions).sort((a, b) => new Date(versions[b]).getTime() - new Date(versions[a]).getTime())
18 | return {
19 | ...item,
20 | downloads: item.humanDownloadsLast30Days,
21 | author: item.owner.name,
22 | versions: [...new Set([item.version, ...normalizedVersions])],
23 | versionIndex: 0,
24 | activeVersion: item.version,
25 | repoLink: item.repository?.url,
26 | authorLink: item.owner?.link,
27 | }
28 | })
29 | return value
30 | }
31 |
32 | // for npm registry
33 | function normalizeNpmPackages(data: NpmPackageInfo[]) {
34 | const value = data.map(({ package: item }) => {
35 | return {
36 | ...item,
37 | downloads: '---',
38 | humanDownloadsLast30Days: '',
39 | descriptions: item.description,
40 | author: item.author?.name ?? '',
41 | versions: [item.version],
42 | versionIndex: 0,
43 | activeVersion: item.version,
44 | repoLink: item.links?.repository,
45 | authorLink: item.author?.url,
46 | owner: {
47 | link: item.author?.url,
48 | name: item.author?.name,
49 | },
50 | repository: {
51 | url: item.links?.repository,
52 | },
53 | }
54 | }) as PackageInfo[]
55 | return value
56 | }
57 |
58 | export const useSearchStore = defineStore('search', () => {
59 | const page = ref(0)
60 | const keyword = ref('')
61 | const packages = ref([])
62 | const searchRegistry = ref<'algolia' | 'npm'>('algolia')
63 |
64 | async function search(k: string, p = 0) {
65 | page.value = p
66 | if (k === '') {
67 | packages.value = []
68 | return
69 | }
70 | const request = {
71 | algolia: algoliaSearch,
72 | npm: npmSearch,
73 | }[searchRegistry.value]
74 |
75 | const normalize = {
76 | algolia: normalizePackages,
77 | npm: normalizeNpmPackages,
78 | }[searchRegistry.value]
79 |
80 | const result = await request(k, p).catch((e) => {
81 | setTimeout(() => {
82 | console.log(e)
83 | }, 2000)
84 | return { query: null, hits: [] }
85 | })
86 |
87 | if (result.query === keyword.value) {
88 | const normalizedResult = normalize(result.hits)
89 | packages.value = page.value === 0 ? normalizedResult : [...packages.value, ...normalizedResult]
90 | }
91 | }
92 |
93 | function toggleRegistry(source: 'algolia' | 'npm') {
94 | searchRegistry.value = source
95 | }
96 |
97 | watch(keyword, () => {
98 | search(keyword.value)
99 | })
100 |
101 | return { keyword, packages, page, search, searchRegistry, toggleRegistry }
102 | })
103 |
104 | export const useDepsStore = defineStore('deps', () => {
105 | const deps = ref([])
106 | const devDeps = ref([])
107 | const add = (dep: DepsInfo, isDev = false) => {
108 | if (isDev)
109 | devDeps.value = [...devDeps.value, dep]
110 | else
111 | deps.value = [...deps.value, dep]
112 | }
113 | const remove = (name: string, isDev = false) => {
114 | if (isDev)
115 | devDeps.value = devDeps.value.filter(item => item.name !== name)
116 | else
117 | deps.value = deps.value.filter(item => item.name !== name)
118 | }
119 | const has = (name: string, isDev = false) => {
120 | if (isDev)
121 | return devDeps.value.some(item => item.name === name)
122 | else
123 | return deps.value.some(item => item.name === name)
124 | }
125 | const normalize = (deps: DepsInfo[]) => {
126 | return deps.map(item => `${item.name}@${item.version}`)
127 | }
128 | const reset = () => {
129 | deps.value = []
130 | devDeps.value = []
131 | }
132 | return { deps, devDeps, add, remove, has, normalize, reset }
133 | })
134 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "ESNext",
5 | "useDefineForClassFields": true,
6 | "module": "ESNext",
7 | "moduleResolution": "Node",
8 | "strict": true,
9 | "jsx": "preserve",
10 | "resolveJsonModule": true,
11 | "isolatedModules": true,
12 | "esModuleInterop": true,
13 | "lib": ["ESNext", "DOM"],
14 | "skipLibCheck": true,
15 | "noEmit": true,
16 | "allowSyntheticDefaultImports": true,
17 | "paths": {
18 | "@/*": [
19 | "src/*"
20 | ]
21 | },
22 | "types": [
23 | "vite/client"
24 | ]
25 | },
26 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue", "*.d.ts"],
27 | "references": [{ "path": "./tsconfig.node.json" }]
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true,
7 | "resolveJsonModule": true
8 | },
9 | "include": ["vite.config.ts", "package.json"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module '*.vue' {
4 | import type { DefineComponent } from 'vue'
5 | const component: DefineComponent<{}, {}, any>
6 | export default component
7 | }
8 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { defineConfig } from 'vite'
3 | import VueTermui from 'vite-plugin-vue-termui'
4 | import pkg from './package.json'
5 |
6 | const deps = Object.keys(pkg.dependencies)
7 |
8 | export default defineConfig({
9 | define: {
10 | __DEV__: JSON.stringify(!(process.env.NODE_ENV === 'production')),
11 | },
12 |
13 | resolve: {
14 | alias: [
15 | { find: '@', replacement: resolve(__dirname, 'src') },
16 | ],
17 | mainFields: process.env.NODE_ENV === 'production' ? ['module', 'main'] : ['main', 'module'],
18 | },
19 |
20 | plugins: [
21 | VueTermui(),
22 | ],
23 |
24 | build: {
25 | rollupOptions: {
26 | external: deps,
27 | },
28 | },
29 |
30 | })
31 |
--------------------------------------------------------------------------------
/vtui-npm.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webfansplz/vtui-npm/ff270786534c64bb8a79f4220e1ed1f933c66279/vtui-npm.gif
--------------------------------------------------------------------------------