mirror of
https://github.com/purocean/yn.git
synced 2026-09-01 15:37:16 +08:00
feat: add extension manager
This commit is contained in:
@@ -11,5 +11,6 @@ module.exports = {
|
||||
'@share/(.*)': '<rootDir>/src/share/$1',
|
||||
'@main/(.*)': '<rootDir>/src/main/$1',
|
||||
'@fe/(.*)': '<rootDir>/src/renderer/$1',
|
||||
"^lodash-es$": "lodash"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"safe-buffer": "^5.2.1",
|
||||
"socket.io": "^2.4.0",
|
||||
"socks-proxy-agent": "^6.1.1",
|
||||
"tar-stream": "^2.2.0",
|
||||
"transliteration": "^2.2.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
@@ -73,6 +74,7 @@
|
||||
"@types/request": "^2.48.5",
|
||||
"@types/socket.io-client": "^1.4.34",
|
||||
"@types/sortablejs": "^1.10.6",
|
||||
"@types/tar-stream": "^2.2.2",
|
||||
"@types/turndown": "^5.0.0",
|
||||
"@types/yargs": "^15.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
||||
@@ -101,6 +103,7 @@
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^27.4.5",
|
||||
"jest-extended": "^1.2.0",
|
||||
"js-untar": "^2.0.0",
|
||||
"juice": "^8.0.0",
|
||||
"katex": "^0.15.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
@@ -117,6 +120,7 @@
|
||||
"mermaid": "^8.13.8",
|
||||
"monaco-editor": "^0.31.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"parse-author": "^2.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"sass": "^1.35.1",
|
||||
"semver": "^7.3.7",
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs-extra'
|
||||
import request from 'request'
|
||||
import { unzip } from 'zlib'
|
||||
import tar from 'tar-stream'
|
||||
import { USER_EXTENSION_DIR } from './constant'
|
||||
import { getAction } from './action'
|
||||
import { Readable } from 'stream'
|
||||
import config from './config'
|
||||
|
||||
const configKey = 'extensions'
|
||||
function getExtensionPath (id: string) {
|
||||
if (!/[A-Za-z0-9-_]+/.test(id)) {
|
||||
throw new Error('Invalid extension id')
|
||||
}
|
||||
|
||||
return path.join(USER_EXTENSION_DIR, id)
|
||||
}
|
||||
|
||||
function changeExtensionConfig (id: string, val: { enabled: boolean }) {
|
||||
const extensions = config.get(configKey, {}) || {}
|
||||
extensions[id] = { ...extensions[id], ...val }
|
||||
config.set(configKey, extensions)
|
||||
}
|
||||
|
||||
export async function list () {
|
||||
const list = (await fs.readdir(USER_EXTENSION_DIR, { withFileTypes: true }))
|
||||
.filter(x => (x.isDirectory() || x.isSymbolicLink()) && !x.name.startsWith('.'))
|
||||
|
||||
const extensionsSettings = config.get(configKey, {})
|
||||
|
||||
Object.keys(extensionsSettings).forEach(key => {
|
||||
if (!list.some(x => x.name === key)) {
|
||||
delete extensionsSettings[key]
|
||||
}
|
||||
})
|
||||
|
||||
config.set(configKey, extensionsSettings)
|
||||
|
||||
return list.map(x => {
|
||||
const ext = extensionsSettings[x.name]
|
||||
return { id: x.name, enabled: (ext && ext.enabled) }
|
||||
})
|
||||
}
|
||||
|
||||
export async function install (id: string, url: string) {
|
||||
console.log('[extension] install', id, url)
|
||||
const extensionPath = getExtensionPath(id)
|
||||
if (await fs.pathExists(extensionPath)) {
|
||||
console.log('[extension] already installed. upgrade:', id)
|
||||
}
|
||||
|
||||
const agent = await getAction('get-proxy-agent')(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request({ url, agent, encoding: null }, (err, _, body) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
unzip(body, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
const extract = tar.extract()
|
||||
|
||||
extract.on('entry', (header, stream, next) => {
|
||||
const filePath = path.join(extensionPath, header.name.replace(/^package/, ''))
|
||||
console.log('[extension] write file', filePath)
|
||||
|
||||
fs.ensureFile(filePath).then(() => {
|
||||
const fileStream = fs.createWriteStream(filePath)
|
||||
stream.pipe(fileStream)
|
||||
stream.on('end', next)
|
||||
}).catch(reject)
|
||||
})
|
||||
|
||||
extract.on('finish', () => {
|
||||
resolve(undefined)
|
||||
})
|
||||
|
||||
extract.on('error', reject)
|
||||
|
||||
Readable.from(data).pipe(extract)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function uninstall (id: string) {
|
||||
const extensionPath = getExtensionPath(id)
|
||||
if (await fs.pathExists(extensionPath)) {
|
||||
await fs.remove(extensionPath)
|
||||
}
|
||||
}
|
||||
|
||||
export async function enable (id: string) {
|
||||
changeExtensionConfig(id, { enabled: true })
|
||||
}
|
||||
|
||||
export async function disable (id: string) {
|
||||
changeExtensionConfig(id, { enabled: false })
|
||||
}
|
||||
+37
-17
@@ -16,6 +16,7 @@ import shell from '../shell'
|
||||
import config from '../config'
|
||||
import * as jwt from '../jwt'
|
||||
import { getAction } from '../action'
|
||||
import * as extension from '../extension'
|
||||
|
||||
const isLocalhost = (address: string) => {
|
||||
return ip.isEqual(address, '127.0.0.1') || ip.isEqual(address, '::1')
|
||||
@@ -307,7 +308,18 @@ const customCss = async (ctx: any, next: any) => {
|
||||
|
||||
try {
|
||||
const filename = config.get(configKey, defaultCss)
|
||||
ctx.body = await fs.readFile(path.join(USER_THEME_DIR, filename))
|
||||
|
||||
if (filename.startsWith('extension:')) {
|
||||
const extensions = await extension.list()
|
||||
const extensionName = filename.substring('extension:'.length, filename.indexOf('/'))
|
||||
if (extensions.some(x => x.enabled && x.id === extensionName)) {
|
||||
ctx.redirect(`/extensions/${filename.replace('extension:', '')}`)
|
||||
} else {
|
||||
throw new Error(`extension not found [${extensionName}]`)
|
||||
}
|
||||
} else {
|
||||
ctx.body = await fs.readFile(path.join(USER_THEME_DIR, filename))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
@@ -336,6 +348,7 @@ const setting = async (ctx: any, next: any) => {
|
||||
data.mark = []
|
||||
delete data['server.jwt-secret']
|
||||
delete data.license
|
||||
delete data.extensions
|
||||
return data
|
||||
}
|
||||
}
|
||||
@@ -434,25 +447,32 @@ const sendFile = async (ctx: any, next: any, filePath: string, fullback = true)
|
||||
}
|
||||
|
||||
const userExtension = async (ctx: any, next: any) => {
|
||||
if (ctx.path.startsWith('/api/extensions') && ctx.method === 'GET') {
|
||||
const list = await fs.readdir(USER_EXTENSION_DIR)
|
||||
if (ctx.method === 'GET') {
|
||||
if (ctx.path.startsWith('/api/extensions')) {
|
||||
ctx.body = result('ok', 'success', await extension.list())
|
||||
} else if (ctx.path.startsWith('/extensions/') && ctx.method === 'GET') {
|
||||
const filePath = path.join(USER_EXTENSION_DIR, ctx.path.replace('/extensions', ''))
|
||||
if (await sendFile(ctx, next, filePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const extensionsSettings = config.get('extensions', {})
|
||||
const extensions = list.map(x => {
|
||||
const ext = extensionsSettings[x]
|
||||
return { id: x, enabled: !!(ext && ext.enabled) }
|
||||
})
|
||||
|
||||
ctx.body = result('ok', 'success', extensions)
|
||||
} else if (ctx.path.startsWith('/extensions/') && ctx.method === 'GET') {
|
||||
const filePath = path.join(USER_EXTENSION_DIR, ctx.path.replace('/extensions', ''))
|
||||
if (await sendFile(ctx, next, filePath)) {
|
||||
return
|
||||
await next()
|
||||
} else {
|
||||
await next()
|
||||
}
|
||||
|
||||
await next()
|
||||
} else {
|
||||
await next()
|
||||
const id = ctx.query.id
|
||||
if (ctx.path.startsWith('/api/extensions/install')) {
|
||||
ctx.body = result('ok', 'success', await extension.install(id, ctx.query.url))
|
||||
} else if (ctx.path.startsWith('/api/extensions/uninstall')) {
|
||||
ctx.body = result('ok', 'success', await extension.uninstall(id))
|
||||
} else if (ctx.path.startsWith('/api/extensions/enable')) {
|
||||
ctx.body = result('ok', 'success', await extension.enable(id))
|
||||
} else if (ctx.path.startsWith('/api/extensions/disable')) {
|
||||
ctx.body = result('ok', 'success', await extension.disable(id))
|
||||
} else {
|
||||
await next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import * as extension from '@fe/others/extension'
|
||||
|
||||
jest.mock('@fe/support/api', () => ({}))
|
||||
jest.mock('@fe/services/theme', () => ({}))
|
||||
jest.mock('js-untar', () => ({}))
|
||||
|
||||
jest.mock('@fe/utils', () => ({
|
||||
getLogger: console.log
|
||||
}))
|
||||
|
||||
jest.mock('@fe/services/i18n', () => ({
|
||||
getCurrentLanguage: () => 'zh-CN'
|
||||
}))
|
||||
|
||||
jest.mock('@fe/core/action', () => ({
|
||||
getActionHandler: () => () => 0
|
||||
}))
|
||||
|
||||
;(global as any).__APP_VERSION__ = '3.29.0'
|
||||
|
||||
test('readInfoFromJson', () => {
|
||||
expect(extension.readInfoFromJson(undefined)).toBeNull()
|
||||
expect(extension.readInfoFromJson({})).toBeNull()
|
||||
expect(extension.readInfoFromJson({ name: 'test' })).toBeNull()
|
||||
|
||||
expect(extension.readInfoFromJson({ name: 'test', version: '1.1.2' })).toStrictEqual({
|
||||
id: 'test',
|
||||
author: { name: '' },
|
||||
displayName: 'test',
|
||||
main: undefined,
|
||||
description: '',
|
||||
version: '1.1.2',
|
||||
themes: [],
|
||||
origin: 'unknown',
|
||||
dist: { tarball: '', unpackedSize: 0 },
|
||||
icon: '',
|
||||
homepage: '',
|
||||
license: '',
|
||||
compatible: {
|
||||
reason: 'Not yank note extension.',
|
||||
value: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(extension.readInfoFromJson({
|
||||
name: 'test',
|
||||
version: '1.1.2',
|
||||
license: 'MIT',
|
||||
engines: {
|
||||
'yank-note': '>=3.29.0',
|
||||
},
|
||||
})).toStrictEqual({
|
||||
id: 'test',
|
||||
author: { name: '' },
|
||||
displayName: 'test',
|
||||
main: undefined,
|
||||
description: '',
|
||||
version: '1.1.2',
|
||||
themes: [],
|
||||
origin: 'unknown',
|
||||
dist: { tarball: '', unpackedSize: 0 },
|
||||
icon: '',
|
||||
homepage: '',
|
||||
license: 'MIT',
|
||||
compatible: {
|
||||
reason: 'Compatible',
|
||||
value: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(extension.readInfoFromJson({
|
||||
name: 'test',
|
||||
version: '1.1.2',
|
||||
engines: {
|
||||
'yank-note': '>=3.30.0',
|
||||
},
|
||||
})).toStrictEqual({
|
||||
id: 'test',
|
||||
author: { name: '' },
|
||||
displayName: 'test',
|
||||
main: undefined,
|
||||
description: '',
|
||||
version: '1.1.2',
|
||||
themes: [],
|
||||
origin: 'unknown',
|
||||
dist: { tarball: '', unpackedSize: 0 },
|
||||
icon: '',
|
||||
homepage: '',
|
||||
license: '',
|
||||
compatible: {
|
||||
reason: 'Need Yank Note [>=3.30.0].',
|
||||
value: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(extension.readInfoFromJson({
|
||||
name: 'test',
|
||||
author: 'test <test@t.t>',
|
||||
version: '1.1.2',
|
||||
description: 'HELLO!',
|
||||
displayName: 'HELLO',
|
||||
})).toStrictEqual({
|
||||
id: 'test',
|
||||
author: { name: 'test', email: 'test@t.t' },
|
||||
main: undefined,
|
||||
displayName: 'HELLO',
|
||||
description: 'HELLO!',
|
||||
version: '1.1.2',
|
||||
themes: [],
|
||||
origin: 'unknown',
|
||||
dist: { tarball: '', unpackedSize: 0 },
|
||||
icon: '',
|
||||
homepage: '',
|
||||
license: '',
|
||||
compatible: {
|
||||
reason: 'Not yank note extension.',
|
||||
value: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(extension.readInfoFromJson({
|
||||
name: 'test',
|
||||
version: '1.1.2',
|
||||
author: { name: 'hello', email: 'xxx@email.com' },
|
||||
description: 'HELLO!',
|
||||
displayName: 'HELLO',
|
||||
'description_ZH-CN': '你好!',
|
||||
'displayName_ZH-CN': '你好',
|
||||
themes: [
|
||||
{ name: 'a', css: './a.css' },
|
||||
{ name: 'b', css: './b.css' },
|
||||
],
|
||||
})).toStrictEqual({
|
||||
id: 'test',
|
||||
author: { name: 'hello', email: 'xxx@email.com' },
|
||||
main: undefined,
|
||||
displayName: '你好',
|
||||
description: '你好!',
|
||||
version: '1.1.2',
|
||||
themes: [
|
||||
{ name: 'a', css: './a.css' },
|
||||
{ name: 'b', css: './b.css' },
|
||||
],
|
||||
origin: 'unknown',
|
||||
dist: { tarball: '', unpackedSize: 0 },
|
||||
icon: '',
|
||||
homepage: '',
|
||||
license: '',
|
||||
compatible: {
|
||||
reason: 'Not yank note extension.',
|
||||
value: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,700 @@
|
||||
<template>
|
||||
<XMask :mask-closeable="false" :style="{paddingTop: '7vh'}" :show="!!showManager" @close="hide">
|
||||
<div class="wrapper">
|
||||
<div class="body">
|
||||
<div class="side">
|
||||
<GroupTabs class="tabs" :tabs="listTypes" v-model="listType" />
|
||||
<div v-if="extensions.length > 0" class="list">
|
||||
<div
|
||||
v-for="item in extensions"
|
||||
:key="item.id"
|
||||
:class="{item: true, selected: item.id === currentExtension?.id}"
|
||||
@click="choose(item.id)">
|
||||
<div class="left">
|
||||
<div v-if="item.icon" class="icon-extension" :style="{ 'background-size': '98% 98%', 'background-image': `url(${item.icon})` }" />
|
||||
<div v-else class="icon-extension" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="title">
|
||||
<div class="name">{{ item.displayName }}</div>
|
||||
<div class="version">
|
||||
<span>{{ item.version }}</span>
|
||||
<span class="upgradable" v-if="item.upgradable"> {{ $t('extension.upgradable') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">{{ item.description }}</div>
|
||||
<div class="bottom">
|
||||
<div v-if="item.origin === 'official'" class="author"><i>Yank Note</i></div>
|
||||
<div v-else class="author" >{{ item.author.name }}</div>
|
||||
<div class="status-list">
|
||||
<div v-if="!item.compatible.value" class="status">{{ $t('extension.incompatible') }}</div>
|
||||
<div v-if="!item.installed" class="status">{{ $t('extension.not-installed') }}</div>
|
||||
<div v-if="item.installed && item.enabled" class="status">{{ $t('extension.enabled') }}</div>
|
||||
<div v-if="item.installed && !item.enabled" class="status">{{ $t('extension.disabled') }}</div>
|
||||
<div v-if="item.dirty" class="status">{{ $t('extension.reload-required') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="list">
|
||||
<div class="placeholder">{{ $t(registryExtensions ? 'extension.no-extension' : 'loading') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail">
|
||||
<template v-if="currentExtension">
|
||||
<div class="info">
|
||||
<div class="left">
|
||||
<div v-if="currentExtension.icon" class="icon-extension" :style="{ 'background-size': '94% 94%', 'background-image': `url(${currentExtension.icon})` }" />
|
||||
<div v-else class="icon-extension" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="title">
|
||||
<div class="name">{{ currentExtension.displayName }}</div>
|
||||
<div class="version" v-if="currentExtension.version">
|
||||
<span>{{ currentExtension.version }}</span>
|
||||
<span class="upgradable" v-if="currentExtension.upgradable"> {{ $t('extension.upgradable') }} {{ currentExtension.latestVersion }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tags">
|
||||
<div class="tag">
|
||||
<span>{{ $t('extension.author') }}</span>
|
||||
<span v-if="currentExtension.origin === 'official'"><i>Yank Note</i></span>
|
||||
<span v-else>{{ currentExtension.author.name }}</span>
|
||||
</div>
|
||||
<div v-if="currentExtension.latestVersion" class="tag">
|
||||
<span>{{ $t('extension.latest-version') }}</span>
|
||||
<span>{{ currentExtension.latestVersion }}</span>
|
||||
</div>
|
||||
<div v-if="currentExtension.dist.unpackedSize" class="tag">
|
||||
<span>{{ $t('extension.unpacked-size') }}</span>
|
||||
<span>{{ (currentExtension.dist.unpackedSize / 1024).toFixed(2) }}KiB</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="currentExtension.homepage && currentExtension.homepage.split('/')[2]"
|
||||
class="tag"
|
||||
style="cursor: pointer;"
|
||||
@click="openUrl(currentExtension?.homepage)"
|
||||
>
|
||||
<span>{{ $t('extension.homepage') }}</span>
|
||||
<span>{{ currentExtension.homepage.split('/')[2] }}</span>
|
||||
</div>
|
||||
<div v-if="currentExtension.license" class="tag">
|
||||
<span>License</span>
|
||||
<span>{{ currentExtension.license }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="currentExtension.dist.unpackedSize"
|
||||
class="tag"
|
||||
style="cursor: pointer;"
|
||||
@click="openUrl(`https://www.npmjs.com/package/${currentExtension?.id}`)"
|
||||
>
|
||||
<img alt="npm" :src="`https://img.shields.io/npm/dy/${currentExtension.id}?color=%234180bd&label=Download&style=flat-square`">
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">{{ currentExtension.description }}</div>
|
||||
<div v-if="installing" class="actions"><i>{{ $t('extension.installing') }}</i></div>
|
||||
<div v-else-if="uninstalling" class="actions"><i>{{ $t('extension.uninstalling') }}</i></div>
|
||||
<div v-else class="actions">
|
||||
<template v-if="currentExtension.dirty">
|
||||
<button class="small" @click="reload">{{$t('extension.reload')}}</button>
|
||||
<i>{{ $t('extension.reload-required') }}</i>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="!currentExtension.installed">
|
||||
<button class="small" :disabled="!currentExtension.compatible.value" @click="install(currentExtension)">{{ $t('extension.install') }}</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="small" @click="uninstall(currentExtension)">{{ $t('extension.uninstall') }}</button>
|
||||
<button v-if="currentExtension.enabled" class="small" @click="disable(currentExtension)">{{ $t('extension.disable') }}</button>
|
||||
<button v-else-if="currentExtension.compatible.value" class="small" @click="enable(currentExtension)">{{ $t('extension.enable') }}</button>
|
||||
<button v-if="currentExtension.upgradable" :disabled="!currentExtension.newVersionCompatible?.value" class="small" @click="upgrade(currentExtension)">{{ $t('extension.upgrade') }}</button>
|
||||
</template>
|
||||
<i v-if="!currentExtension.compatible.value">{{currentExtension.compatible.reason}}</i>
|
||||
<i v-if="currentExtension.upgradable && !currentExtension.newVersionCompatible?.value">{{currentExtension.newVersionCompatible?.reason}}</i>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="currentExtension.dist.unpackedSize">
|
||||
<div v-show="iframeLoaded" class="content">
|
||||
<iframe @load="iframeOnload" sandbox="allow-scripts allow-popups allow-same-origin" referrerpolicy="no-referrer" :src="`/api/proxy?url=https://www.npmjs.com/package/${currentExtension.id}`" />
|
||||
</div>
|
||||
<div v-if="!iframeLoaded" class="placeholder">{{ $t('loading') }}</div>
|
||||
</template>
|
||||
</template>
|
||||
<div v-else class="placeholder">{{ $t('extension.extension-manager') }}</div>
|
||||
<div class="dialog-actions">
|
||||
<div class="left">
|
||||
<b>{{$t('extension.registry')}}:</b>
|
||||
<select v-model="currentRegistry">
|
||||
<option
|
||||
v-for="hostname in registries"
|
||||
:key="hostname"
|
||||
:value="hostname"
|
||||
:selected="hostname === currentRegistry"
|
||||
>{{ hostname }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="right">
|
||||
<template v-if="reloadRequired">
|
||||
<i>{{ $t('extension.reload-required') }}</i>
|
||||
<button class="btn" @click="reload">{{$t('extension.reload')}}</button>
|
||||
</template>
|
||||
<button class="btn" @click="hide">{{$t('close')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</XMask>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from '@fe/services/i18n'
|
||||
import { getLogger } from '@fe/utils'
|
||||
import { registerAction, removeAction } from '@fe/core/action'
|
||||
import XMask from '@fe/components/Mask.vue'
|
||||
import GroupTabs from '@fe/components/GroupTabs.vue'
|
||||
import * as extensionManager from '@fe/others/extension'
|
||||
import type { Extension, Compatible } from '@fe/others/extension'
|
||||
import * as setting from '@fe/services/setting'
|
||||
import { useModal } from '@fe/support/ui/modal'
|
||||
import { useToast } from '@fe/support/ui/toast'
|
||||
|
||||
const logger = getLogger('extension-manager-component')
|
||||
|
||||
const { $t } = useI18n()
|
||||
|
||||
const registries = extensionManager.registries
|
||||
|
||||
const showManager = ref(false)
|
||||
const currentId = ref('')
|
||||
const iframeLoaded = ref(false)
|
||||
const installing = ref(false)
|
||||
const uninstalling = ref(false)
|
||||
const registryExtensions = ref<Extension[] | null>(null)
|
||||
const installedExtensions = ref<Extension[]>([])
|
||||
const listType = ref<'all' | 'installed'>('all')
|
||||
const currentRegistry = ref(setting.getSetting('extension.registry', 'registry.npmjs.org'))
|
||||
|
||||
const listTypes = computed(() => [
|
||||
{ label: $t.value('extension.all'), value: 'all' },
|
||||
{ label: $t.value('extension.installed'), value: 'installed' },
|
||||
])
|
||||
|
||||
const extensions = computed(() => {
|
||||
let list: (Extension & {
|
||||
dirty?: boolean,
|
||||
upgradable?: boolean,
|
||||
latestVersion?: string,
|
||||
newVersionCompatible?: Compatible,
|
||||
})[] = []
|
||||
|
||||
if (registryExtensions.value) {
|
||||
list = registryExtensions.value.map(item => {
|
||||
const installedInfo = installedExtensions.value.find(installed => installed.id === item.id)
|
||||
if (installedInfo) {
|
||||
return {
|
||||
...item,
|
||||
installed: true,
|
||||
enabled: installedInfo.enabled,
|
||||
version: installedInfo.version,
|
||||
compatible: installedInfo.compatible,
|
||||
newVersionCompatible: item.compatible,
|
||||
latestVersion: item.version,
|
||||
upgradable: installedInfo.version !== item.version,
|
||||
}
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
installedExtensions.value.forEach(item => {
|
||||
if (!registryExtensions.value?.some(registry => registry.id === item.id)) {
|
||||
list.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
return list
|
||||
.filter(item => listType.value !== 'installed' || item.installed)
|
||||
.map(item => {
|
||||
const loadStatus = extensionManager.getLoadStatus(item.id)
|
||||
|
||||
return {
|
||||
...item,
|
||||
dirty: (loadStatus.themes || loadStatus.plugin) &&
|
||||
(!item.installed || !item.enabled || item.version !== loadStatus.version),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const currentExtension = computed(() => {
|
||||
return extensions.value.find(item => item.id === currentId.value)
|
||||
})
|
||||
|
||||
const reloadRequired = computed(() => {
|
||||
return extensions.value.some(item => item.dirty)
|
||||
})
|
||||
|
||||
function choose (id: string) {
|
||||
logger.debug('choose', id)
|
||||
if (currentId.value === id) {
|
||||
return
|
||||
}
|
||||
|
||||
currentId.value = id
|
||||
iframeLoaded.value = false
|
||||
}
|
||||
|
||||
function show (id?: string) {
|
||||
if (id) {
|
||||
choose(id)
|
||||
}
|
||||
|
||||
showManager.value = true
|
||||
}
|
||||
|
||||
function hide () {
|
||||
showManager.value = false
|
||||
}
|
||||
|
||||
async function refreshInstalledExtensions () {
|
||||
installedExtensions.value = await extensionManager.getInstalledExtensions()
|
||||
}
|
||||
|
||||
async function fetchExtensions () {
|
||||
try {
|
||||
registryExtensions.value = null
|
||||
registryExtensions.value = await extensionManager.getRegistryExtensions(currentRegistry.value)
|
||||
} finally {
|
||||
refreshInstalledExtensions()
|
||||
}
|
||||
}
|
||||
|
||||
async function install (extension?: Extension) {
|
||||
if (!extension) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
installing.value = true
|
||||
await extensionManager.install(extension, currentRegistry.value)
|
||||
await refreshInstalledExtensions()
|
||||
} catch (error: any) {
|
||||
logger.error('install', error)
|
||||
useToast().show('warning', error.message)
|
||||
throw error
|
||||
} finally {
|
||||
installing.value = false
|
||||
}
|
||||
|
||||
useToast().show('info', $t.value('extension.toast-loaded'))
|
||||
}
|
||||
|
||||
async function uninstall (extension?: Extension) {
|
||||
if (!extension) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug('uninstall', extension.id)
|
||||
if (await useModal().confirm({
|
||||
title: $t.value('extension.uninstall'),
|
||||
content: $t.value('extension.uninstall-confirm', extension.displayName),
|
||||
})) {
|
||||
try {
|
||||
uninstalling.value = true
|
||||
await extensionManager.uninstall(extension)
|
||||
await refreshInstalledExtensions()
|
||||
} catch (error: any) {
|
||||
logger.error('uninstall', error)
|
||||
useToast().show('warning', error.message)
|
||||
throw error
|
||||
} finally {
|
||||
uninstalling.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function upgrade (extension?: Extension) {
|
||||
install(extension)
|
||||
}
|
||||
|
||||
function reload () {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
async function enable (extension?: Extension) {
|
||||
if (!extension) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug('enable', extension.id)
|
||||
await extensionManager.enable(extension)
|
||||
refreshInstalledExtensions()
|
||||
useToast().show('info', $t.value('extension.toast-loaded'))
|
||||
}
|
||||
|
||||
async function disable (extension?: Extension) {
|
||||
if (!extension) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug('disable', extension.id)
|
||||
await extensionManager.disable(extension)
|
||||
refreshInstalledExtensions()
|
||||
}
|
||||
|
||||
function iframeOnload (e: any) {
|
||||
const win = e.target.contentWindow
|
||||
win.addEventListener('click', (e: any) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (e.target.href) {
|
||||
window.open(e.target.href)
|
||||
}
|
||||
}, true)
|
||||
|
||||
const article = win.document.querySelector('article')
|
||||
win.document.body.innerHTML = ''
|
||||
win.document.body.appendChild(article)
|
||||
win.document.body.style.padding = '12px'
|
||||
iframeLoaded.value = true
|
||||
}
|
||||
|
||||
function openUrl (url?: string) {
|
||||
if (url) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
watch(extensions, () => {
|
||||
if (!currentId.value && extensions.value.length > 0) {
|
||||
choose(extensions.value[0].id)
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(showManager, (val) => {
|
||||
if (val) {
|
||||
fetchExtensions()
|
||||
}
|
||||
})
|
||||
|
||||
watch(currentRegistry, (val) => {
|
||||
fetchExtensions()
|
||||
setting.setSetting('extension.registry', val)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
registerAction({ name: 'extension.show-manager', handler: show })
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
removeAction('extension.show-manager')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
width: 90vw;
|
||||
background: var(--g-color-95);
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
color: var(--g-color-5);
|
||||
box-shadow: rgba(0, 0, 0 , 0.3) 2px 2px 10px;
|
||||
border-radius: var(--g-border-radius);
|
||||
position: relative;
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
height: 75vh;
|
||||
}
|
||||
|
||||
.side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 320px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.list {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
// font-family: monospace;
|
||||
font-size: 16px;
|
||||
background-color: var(--g-color-92);
|
||||
// padding: 6px;
|
||||
|
||||
.item {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
// background-color: var(--g-color-92);
|
||||
border-bottom: 1px var(--g-color-80) solid;
|
||||
color: var(--g-color-20);
|
||||
display: flex;
|
||||
height: 77px;
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.left {
|
||||
padding-right: 6px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding-top: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.status-list {
|
||||
display: flex;
|
||||
.status {
|
||||
font-size: 12px;
|
||||
color: var(--g-color-30);
|
||||
white-space: nowrap;
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
font-size: 13px;
|
||||
color: var(--g-color-30);
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 13px;
|
||||
color: var(--g-color-30);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--g-color-86);
|
||||
border-radius: var(--g-border-radius);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--g-color-86);
|
||||
border-radius: var(--g-border-radius);
|
||||
color: var(--g-color-0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: inline-flex;
|
||||
margin-bottom: 8px;
|
||||
z-index: 1;
|
||||
flex: none;
|
||||
|
||||
::v-deep(.tab) {
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-extension {
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMiAyMiI+PHBhdGggZD0ibTEzLjU1NSAxMDQzLjgzaC0uOTI3di0yLjQ3M2MwLS42OC0uNTU2LTEuMjM2LTEuMjM2LTEuMjM2aC0yLjQ3MnYtLjkyN2MwLS44NjUtLjY4LTEuNTQ1LTEuNTQ1LTEuNTQ1LS44NjUgMC0xLjU0NS42OC0xLjU0NSAxLjU0NXYuOTI3aC0yLjQ3MmMtLjY4IDAtMS4yMzYuNTU2LTEuMjM2IDEuMjM2djIuMzQ5aC45MjdjLjkyNyAwIDEuNjY5Ljc0MiAxLjY2OSAxLjY2OSAwIC45MjctLjc0MiAxLjY2OS0xLjY2OSAxLjY2OWgtLjkyN3YyLjM0OWMwIC42OC41NTYgMS4yMzYgMS4yMzYgMS4yMzZoMi4zNDl2LS45MjdjMC0uOTI3Ljc0Mi0xLjY2OSAxLjY2OS0xLjY2OS45MjcgMCAxLjY2OS43NDIgMS42NjkgMS42Njl2LjkyN2gyLjM0OWMuNjggMCAxLjIzNi0uNTU2IDEuMjM2LTEuMjM2di0yLjQ3MmguOTI3Yy44NjUgMCAxLjU0NS0uNjggMS41NDUtMS41NDUgMC0uODY1LS42OC0xLjU0NS0xLjU0NS0xLjU0NSIgZmlsbD0iIzg4OCIgdHJhbnNmb3JtPSJtYXRyaXgoMS4yMzI2NSAwIDAgMS4yMzI2NC4zOTctMTI3Ni4wNSkiLz48L3N2Zz4=);
|
||||
background-size: 80px 80px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding: 12px;
|
||||
padding-right: 0;
|
||||
padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex: none;
|
||||
|
||||
.left {
|
||||
flex: none;
|
||||
|
||||
.icon-extension {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
background-size: 130px 130px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding: 12px;
|
||||
|
||||
.title {
|
||||
align-items: flex-end;
|
||||
justify-content: start;
|
||||
overflow: hidden;
|
||||
margin-top: -6px;
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.tag {
|
||||
margin: 3px 0;
|
||||
border-radius: var(--g-border-radius);
|
||||
overflow: hidden;
|
||||
background: rgb(80, 80, 80);
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
color: #fefefe;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 4px 6px;
|
||||
display: inline-block;
|
||||
|
||||
&:last-child {
|
||||
background: rgb(65, 128, 189);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--g-color-30);
|
||||
margin-top: 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 10px;
|
||||
button {
|
||||
margin-left: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
margin-top: 10px;
|
||||
border-top: 3px solid var(--g-color-86);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--g-color-0);
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: var(--g-color-60);
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
select {
|
||||
margin-left: 12px;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upgradable {
|
||||
color: #4caf50;
|
||||
}
|
||||
</style>
|
||||
@@ -42,7 +42,7 @@
|
||||
import { computed, defineComponent, nextTick, PropType, ref, toRefs, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useContextMenu } from '@fe/support/ui/context-menu'
|
||||
import extensions from '@fe/others/extensions'
|
||||
import extensions from '@fe/others/file-extensions'
|
||||
import { triggerHook } from '@fe/core/hook'
|
||||
import { getContextMenuItems } from '@fe/services/tree'
|
||||
import type { Components } from '@fe/types'
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
import * as storage from '@fe/utils/storage'
|
||||
import * as utils from '@fe/utils/index'
|
||||
import { showPremium } from '@fe/others/premium'
|
||||
import * as extension from '@fe/others/extension'
|
||||
import * as ioc from '@fe/core/ioc'
|
||||
import * as plugin from '@fe/core/plugin'
|
||||
import * as hook from '@fe/core/hook'
|
||||
@@ -56,6 +57,8 @@ const ctx = {
|
||||
removeHook: hook.removeHook,
|
||||
triggerHook: hook.triggerHook,
|
||||
showPremium: showPremium,
|
||||
showExtensionManager: extension.showManager,
|
||||
getExtensionLoadStatus: extension.getLoadStatus,
|
||||
version: __APP_VERSION__,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import parseAuthor from 'parse-author'
|
||||
import semver from 'semver'
|
||||
import pako from 'pako'
|
||||
import untar from 'js-untar'
|
||||
import { getLogger, path } from '@fe/utils'
|
||||
import * as api from '@fe/support/api'
|
||||
import { getActionHandler } from '@fe/core/action'
|
||||
import type { RegistryHostname } from '@fe/types'
|
||||
import * as i18n from '@fe/services/i18n'
|
||||
import * as theme from '@fe/services/theme'
|
||||
|
||||
export type Compatible = { value: boolean, reason: string }
|
||||
export type LoadStatus = { version?: string, themes: boolean, plugin: boolean }
|
||||
|
||||
export interface Extension {
|
||||
id: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
homepage: string;
|
||||
license: string;
|
||||
author: {
|
||||
name: string;
|
||||
email?: string;
|
||||
url?: string;
|
||||
};
|
||||
version: string;
|
||||
themes: { name: string; css: string }[];
|
||||
compatible: Compatible;
|
||||
main: string;
|
||||
enabled?: boolean;
|
||||
installed: boolean;
|
||||
origin: 'official' | 'registry' | 'unknown';
|
||||
dist: { tarball: string, unpackedSize: number };
|
||||
}
|
||||
|
||||
const logger = getLogger('extension')
|
||||
|
||||
const loaded = new Map<string, LoadStatus>()
|
||||
|
||||
export const registries: RegistryHostname[] = [
|
||||
'registry.npmjs.org',
|
||||
'registry.npmmirror.com',
|
||||
]
|
||||
|
||||
function changeRegistryOrigin (hostname: RegistryHostname, url: string) {
|
||||
const _url = new URL(url)
|
||||
_url.hostname = hostname
|
||||
return _url.toString()
|
||||
}
|
||||
|
||||
export function getLoadStatus (id: string): LoadStatus {
|
||||
return loaded.get(id) || { version: undefined, themes: false, plugin: false }
|
||||
}
|
||||
|
||||
export function getCompatible (engines?: { 'yank-note': string }): Compatible {
|
||||
if (!engines || !engines['yank-note']) {
|
||||
return { value: false, reason: 'Not yank note extension.' }
|
||||
}
|
||||
|
||||
const engineVersion = __APP_VERSION__
|
||||
|
||||
const value = semver.satisfies(engineVersion, engines['yank-note'])
|
||||
|
||||
return {
|
||||
value,
|
||||
reason: value ? 'Compatible' : `Need Yank Note [${engines['yank-note']}].`,
|
||||
}
|
||||
}
|
||||
|
||||
export function readInfoFromJson (json: any): Omit<Extension, 'installed'> | null {
|
||||
if (!json || !json.name || !json.version) {
|
||||
return null
|
||||
}
|
||||
|
||||
const language = i18n.getCurrentLanguage().toUpperCase()
|
||||
|
||||
return {
|
||||
id: json.name,
|
||||
version: json.version,
|
||||
license: typeof json.license === 'string' ? json.license : '',
|
||||
author: typeof json.author === 'string'
|
||||
? parseAuthor(json.author) || { name: '' }
|
||||
: json.author || { name: '' },
|
||||
themes: json.themes || [],
|
||||
main: json.main,
|
||||
icon: json.icon || '',
|
||||
displayName: json[`displayName_${language}`] || json.displayName || json.name,
|
||||
description: json[`description_${language}`] || json.description || '',
|
||||
compatible: getCompatible(json.engines),
|
||||
origin: json.origin || 'unknown',
|
||||
dist: json.dist || { tarball: '', unpackedSize: 0 },
|
||||
homepage: json.homepage || '',
|
||||
}
|
||||
}
|
||||
|
||||
export async function getInstalledExtension (id: string): Promise<Extension | null> {
|
||||
let json
|
||||
|
||||
try {
|
||||
json = await api.fetchHttp(`/extensions/${id}/package.json`)
|
||||
if (!json.name || !json.version) {
|
||||
throw new Error('Invalid extension package.json')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
return null
|
||||
}
|
||||
|
||||
const info = readInfoFromJson(json)
|
||||
if (info) {
|
||||
return { ...info, installed: true }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export async function getInstalledExtensions () {
|
||||
const extensions: Extension[] = []
|
||||
for (const item of await api.fetchInstalledExtensions()) {
|
||||
const info = await getInstalledExtension(item.id)
|
||||
if (info) {
|
||||
extensions.push({
|
||||
...info,
|
||||
enabled: item.enabled && info.compatible.value,
|
||||
icon: path.join('/extensions/', item.id, info.icon),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
|
||||
export async function getRegistryExtensions (registry: RegistryHostname = 'registry.npmjs.org'): Promise<Extension[]> {
|
||||
logger.debug('getRegistryExtensions', registry)
|
||||
|
||||
const registryUrl = `https://${registry}/yank-note-registry`
|
||||
const registryJson = await api.proxyRequest(registryUrl).then(r => r.json())
|
||||
const latest = registryJson['dist-tags'].latest
|
||||
const tarballUrl = changeRegistryOrigin(registry, registryJson.versions[latest].dist.tarball)
|
||||
|
||||
const extensions = await api.proxyRequest(tarballUrl)
|
||||
.then(r => r.arrayBuffer())
|
||||
.then(data => pako.inflate(new Uint8Array(data)))
|
||||
.then(arr => arr.buffer)
|
||||
.then(buffer => untar(buffer))
|
||||
.then(files => files.find((x: any) => x.name === 'package/index.json'))
|
||||
.then(file => new TextDecoder('utf-8').decode(file.buffer))
|
||||
.then(JSON.parse)
|
||||
|
||||
return extensions.map(readInfoFromJson)
|
||||
}
|
||||
|
||||
export function showManager (id?: string) {
|
||||
getActionHandler('extension.show-manager')(id)
|
||||
}
|
||||
|
||||
export async function enable (extension: Extension) {
|
||||
await api.enableExtension(extension.id)
|
||||
extension.enabled = true
|
||||
load(extension)
|
||||
}
|
||||
|
||||
export async function disable (extension: Pick<Extension, 'id'>) {
|
||||
await api.disableExtension(extension.id)
|
||||
}
|
||||
|
||||
export async function uninstall (extension: Pick<Extension, 'id'>) {
|
||||
await api.uninstallExtension(extension.id)
|
||||
}
|
||||
|
||||
export async function install (extension: Extension, registry: RegistryHostname = 'registry.npmjs.org') {
|
||||
const url = extension.dist.tarball
|
||||
if (!url) {
|
||||
throw new Error('No dist url')
|
||||
}
|
||||
|
||||
await api.installExtension(extension.id, changeRegistryOrigin(registry, url))
|
||||
await enable(extension)
|
||||
}
|
||||
|
||||
function load (extension: Extension) {
|
||||
if (extension.enabled && extension.compatible) {
|
||||
logger.debug('load', extension.id)
|
||||
const loadStatus: LoadStatus = loaded.get(extension.id) || { themes: false, plugin: false }
|
||||
|
||||
loadStatus.version = extension.version
|
||||
|
||||
if (!loadStatus.themes && extension?.themes && extension.themes.length) {
|
||||
extension.themes.forEach(style => {
|
||||
theme.registerThemeStyle({
|
||||
from: 'extension',
|
||||
name: `[${extension.id.replace(/^yank-note-extension-/, '')}]: ${style.name}`,
|
||||
css: `extension:${path.join(extension.id, style.css)}`,
|
||||
})
|
||||
})
|
||||
loadStatus.themes = true
|
||||
}
|
||||
|
||||
const main = extension?.main
|
||||
if (!loadStatus.plugin && main && main.endsWith('.js')) {
|
||||
const script = window.document.createElement('script')
|
||||
script.src = path.resolve('/extensions', extension.id, main)
|
||||
script.async = true
|
||||
window.document.body.appendChild(script)
|
||||
loadStatus.plugin = true
|
||||
}
|
||||
|
||||
loaded.set(extension.id, loadStatus)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization extension system
|
||||
*/
|
||||
export async function init () {
|
||||
logger.debug('init')
|
||||
|
||||
for (const extension of await getInstalledExtensions()) {
|
||||
load(extension)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,14 @@ export default {
|
||||
id: 'status-bar-tool',
|
||||
position: 'left',
|
||||
title: ctx.i18n.t('status-bar.tool.tool'),
|
||||
list: []
|
||||
list: [
|
||||
{
|
||||
id: 'extension-manager',
|
||||
type: 'normal',
|
||||
title: ctx.i18n.t('status-bar.tool.extension-manager'),
|
||||
onClick: () => ctx.showExtensionManager(),
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ export function setLanguage (language: LanguageName) {
|
||||
*/
|
||||
export function mergeLanguage (lang: Language, nls: Record<string, any>) {
|
||||
_mergeLanguage(lang, nls)
|
||||
triggerHook('I18N_CHANGE_LANGUAGE', { lang: getLanguage(), currentLang: getCurrentLanguage() })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,14 +76,26 @@ export function removeStyles (id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* register theme style
|
||||
* @param style
|
||||
*/
|
||||
export function registerThemeStyle (style: ThemeStyle) {
|
||||
ioc.register('THEME_STYLES', style)
|
||||
}
|
||||
|
||||
/**
|
||||
* get theme styles
|
||||
* @returns
|
||||
*/
|
||||
export function getThemeStyles (): ThemeStyle[] {
|
||||
return ioc.get('THEME_STYLES')
|
||||
}
|
||||
|
||||
/**
|
||||
* remove theme styles
|
||||
* @param style
|
||||
*/
|
||||
export function removeThemeStyle (style: ThemeStyle | ((item: ThemeStyle) => boolean)) {
|
||||
if (typeof style === 'function') {
|
||||
ioc.removeWhen('THEME_STYLES', style)
|
||||
|
||||
Vendored
+2
@@ -21,6 +21,8 @@ declare module 'filenamify/browser'
|
||||
declare module 'katex'
|
||||
declare module 'luckyexcel'
|
||||
declare module 'xterm-theme'
|
||||
declare module 'parse-author'
|
||||
declare module 'js-untar'
|
||||
declare module 'path-browserify' {
|
||||
import path from 'path'
|
||||
export default path
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getSelectionInfo, whenEditorReady } from '@fe/services/editor'
|
||||
import { getLanguage, setLanguage } from '@fe/services/i18n'
|
||||
import { fetchSettings } from '@fe/services/setting'
|
||||
import { getPurchased } from '@fe/others/premium'
|
||||
import * as extension from '@fe/others/extension'
|
||||
import { setTheme } from '@fe/services/theme'
|
||||
import { toggleOutline } from '@fe/services/layout'
|
||||
import * as view from '@fe/services/view'
|
||||
@@ -120,3 +121,7 @@ store.watch(() => store.state.currentFile, (val) => {
|
||||
}, { immediate: true })
|
||||
|
||||
fetchSettings()
|
||||
|
||||
registerHook('STARTUP', () => {
|
||||
setTimeout(extension.init, 500)
|
||||
}, true)
|
||||
|
||||
@@ -392,3 +392,24 @@ export async function rpc (code: string) {
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchInstalledExtensions (): Promise<{id: string, enabled: boolean}[]> {
|
||||
const { data } = await fetchHttp('/api/extensions')
|
||||
return data
|
||||
}
|
||||
|
||||
export async function installExtension (id: string, url: string): Promise<any> {
|
||||
return fetchHttp(`/api/extensions/install?id=${encodeURIComponent(id)}&url=${encodeURIComponent(url)}`, { method: 'POST' })
|
||||
}
|
||||
|
||||
export async function uninstallExtension (id: string): Promise<any> {
|
||||
return fetchHttp(`/api/extensions/uninstall?id=${encodeURIComponent(id)}`, { method: 'POST' })
|
||||
}
|
||||
|
||||
export async function enableExtension (id: string): Promise<any> {
|
||||
return fetchHttp(`/api/extensions/enable?id=${encodeURIComponent(id)}`, { method: 'POST' })
|
||||
}
|
||||
|
||||
export async function disableExtension (id: string): Promise<any> {
|
||||
return fetchHttp(`/api/extensions/disable?id=${encodeURIComponent(id)}`, { method: 'POST' })
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ export type ThemeName = 'system' | 'dark' | 'light'
|
||||
export type LanguageName = 'system' | Language
|
||||
export type ExportType = 'pdf' | 'docx' | 'html' | 'rst' | 'adoc'
|
||||
export type SettingGroup = 'repos' | 'appearance' | 'editor' | 'image' | 'proxy' | 'other' | 'openai'
|
||||
export type RegistryHostname = 'registry.npmjs.org' | 'registry.npmmirror.com'
|
||||
|
||||
export type RenderEnv = {
|
||||
source: string,
|
||||
@@ -134,6 +135,7 @@ export type BuildInSettings = {
|
||||
'proxy.server': string,
|
||||
'proxy.pac-url': string,
|
||||
'proxy.bypass-list': string,
|
||||
'extension.registry': RegistryHostname,
|
||||
'keep-running-after-closing-window': boolean,
|
||||
'plantuml-api': string,
|
||||
}
|
||||
@@ -151,6 +153,7 @@ export type BuildInActions = {
|
||||
'view.exit-presentation': () => void,
|
||||
'doc.show-history': (doc?: Doc) => void
|
||||
'doc.hide-history': () => void,
|
||||
'extension.show-manager': (id?: string) => void,
|
||||
'layout.toggle-view': (visible?: boolean) => void,
|
||||
'layout.toggle-side': (visible?: boolean) => void,
|
||||
'layout.toggle-xterm': (visible?: boolean) => void,
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<Premium />
|
||||
<ControlCenter />
|
||||
<DocHistory />
|
||||
<ExtensionManager />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -54,6 +55,7 @@ import ControlCenter from '@fe/components/ControlCenter.vue'
|
||||
import DocHistory from '@fe/components/DocHistory.vue'
|
||||
import ActionBar from '@fe/components/ActionBar.vue'
|
||||
import Outline from '@fe/components/Outline.vue'
|
||||
import ExtensionManager from '@fe/components/ExtensionManager.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'x-main',
|
||||
@@ -74,6 +76,7 @@ export default defineComponent({
|
||||
DocHistory,
|
||||
ActionBar,
|
||||
Outline,
|
||||
ExtensionManager,
|
||||
},
|
||||
setup () {
|
||||
const store = useStore<AppState>()
|
||||
|
||||
@@ -224,6 +224,7 @@ const data = {
|
||||
'copy-content': 'Copy Content',
|
||||
'doc-history': 'Document History',
|
||||
'share-preview': 'Share Preview',
|
||||
'extension-manager': 'Extension Manager',
|
||||
},
|
||||
'document-info': {
|
||||
'selected': 'Selected',
|
||||
@@ -527,6 +528,39 @@ const data = {
|
||||
'args-json': 'Custom Arguments',
|
||||
'args-json-desc': 'Query parameters, JSON string like {"temperature": 0.3}',
|
||||
},
|
||||
'extension': {
|
||||
'extension-manager': 'Extension Manager',
|
||||
'all': 'All',
|
||||
'installed': 'Installed',
|
||||
'official': 'Official',
|
||||
'unofficial': 'Unofficial',
|
||||
'unknown': 'Unknown',
|
||||
'author': 'Author',
|
||||
'origin': 'Origin',
|
||||
'unpacked-size': 'Unpacked Size',
|
||||
'latest-version': 'Latest Version',
|
||||
'installed-version': 'Installed Version',
|
||||
'homepage': 'Homepage',
|
||||
'download': 'Download',
|
||||
'toast-loaded': 'Extension Loaded',
|
||||
'upgradable': 'Upgradable',
|
||||
'incompatible': 'Incompatible',
|
||||
'not-installed': 'Not Installed',
|
||||
'enabled': 'Enabled',
|
||||
'disabled': 'Disabled',
|
||||
'reload-required': 'Reload Required',
|
||||
'no-extension': 'No Extension',
|
||||
'reload': 'Reload',
|
||||
'install': 'Install',
|
||||
'uninstall': 'Uninstall',
|
||||
'installing': 'Installing',
|
||||
'uninstalling': 'Uninstalling',
|
||||
'upgrade': 'Upgrade',
|
||||
'disable': 'Disable',
|
||||
'enable': 'Enable',
|
||||
'uninstall-confirm': 'Are you sure want to uninstall [%s]?',
|
||||
'registry': 'Registry',
|
||||
},
|
||||
}
|
||||
|
||||
export type BaseLanguage = typeof data
|
||||
|
||||
@@ -225,6 +225,7 @@ const data: BaseLanguage = {
|
||||
'copy-content': '复制内容',
|
||||
'doc-history': '文档历史版本',
|
||||
'share-preview': '分享预览',
|
||||
'extension-manager': '扩展管理',
|
||||
},
|
||||
'document-info': {
|
||||
'selected': '已选择',
|
||||
@@ -528,6 +529,39 @@ const data: BaseLanguage = {
|
||||
'args-json': '自定义参数',
|
||||
'args-json-desc': '请求参数,JSON 字符串如 {"temperature": 0.3}',
|
||||
},
|
||||
'extension': {
|
||||
'extension-manager': '扩展管理',
|
||||
'all': '全部',
|
||||
'installed': '已安装',
|
||||
'official': '官方',
|
||||
'unofficial': '非官方',
|
||||
'unknown': '未知',
|
||||
'author': '作者',
|
||||
'origin': '来源',
|
||||
'unpacked-size': '解包大小',
|
||||
'latest-version': '最新版本',
|
||||
'installed-version': '已安装版本',
|
||||
'homepage': '主页',
|
||||
'download': '下载',
|
||||
'toast-loaded': '扩展加载成功',
|
||||
'upgradable': '可升级',
|
||||
'incompatible': '不兼容',
|
||||
'not-installed': '未安装',
|
||||
'enabled': '已启用',
|
||||
'disabled': '已禁用',
|
||||
'reload-required': '需要重载',
|
||||
'no-extension': '没有扩展',
|
||||
'reload': '重载',
|
||||
'install': '安装',
|
||||
'installing': '正在安装',
|
||||
'uninstalling': '正在卸载',
|
||||
'uninstall': '卸载',
|
||||
'upgrade': '升级',
|
||||
'disable': '禁用',
|
||||
'enable': '启用',
|
||||
'uninstall-confirm': '你确定要卸载扩展 [%s] 吗?',
|
||||
'registry': '仓库源',
|
||||
},
|
||||
}
|
||||
|
||||
export default data
|
||||
|
||||
@@ -67,6 +67,9 @@ export default defineConfig({
|
||||
'/custom-css': {
|
||||
target: 'http://localhost:3044'
|
||||
},
|
||||
'/extension': {
|
||||
target: 'http://localhost:3044'
|
||||
},
|
||||
'/api': {
|
||||
target: 'http://localhost:3044'
|
||||
},
|
||||
|
||||
@@ -1540,6 +1540,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
||||
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
|
||||
|
||||
"@types/tar-stream@^2.2.2":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/tar-stream/-/tar-stream-2.2.2.tgz#be9d0be9404166e4b114151f93e8442e6ab6fb1d"
|
||||
integrity sha512-1AX+Yt3icFuU6kxwmPakaiGrJUwG44MpuiqPg4dSolRFk6jmvs4b3IbUol9wKDLIgU76gevn3EwE8y/DkSJCZQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/tough-cookie@*":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d"
|
||||
@@ -2180,6 +2187,11 @@ at-least-node@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
|
||||
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
|
||||
|
||||
author-regex@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/author-regex/-/author-regex-1.0.0.tgz#d08885be6b9bbf9439fe087c76287245f0a81450"
|
||||
integrity sha1-0IiFvmubv5Q5/gh8dihyRfCoFFA=
|
||||
|
||||
autoprefixer@^10.2.6:
|
||||
version "10.2.6"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.2.6.tgz#aadd9ec34e1c98d403e01950038049f0eb252949"
|
||||
@@ -2314,6 +2326,15 @@ binary-split@^1.0.5:
|
||||
dependencies:
|
||||
through2 "^2.0.3"
|
||||
|
||||
bl@^4.0.3:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
|
||||
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
|
||||
dependencies:
|
||||
buffer "^5.5.0"
|
||||
inherits "^2.0.4"
|
||||
readable-stream "^3.4.0"
|
||||
|
||||
blob@0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
|
||||
@@ -2456,7 +2477,7 @@ buffer-from@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
|
||||
|
||||
buffer@^5.1.0:
|
||||
buffer@^5.1.0, buffer@^5.5.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
|
||||
@@ -4111,7 +4132,7 @@ encodeurl@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
|
||||
|
||||
end-of-stream@^1.1.0:
|
||||
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
|
||||
@@ -4927,6 +4948,11 @@ front-matter@^4.0.2:
|
||||
dependencies:
|
||||
js-yaml "^3.13.1"
|
||||
|
||||
fs-constants@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
|
||||
|
||||
fs-extra@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1"
|
||||
@@ -5482,7 +5508,7 @@ inflight@^1.0.4:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3:
|
||||
inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
@@ -6292,6 +6318,11 @@ js-tokens@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
js-untar@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-untar/-/js-untar-2.0.0.tgz#b452d28dedd3b0be92c2ac9a7d70f612a93c7453"
|
||||
integrity sha512-7CsDLrYQMbLxDt2zl9uKaPZSdmJMvGGQ7wo9hoB3J+z/VcO2w63bXFgHVnjF1+S9wD3zAu8FBVj7EYWjTQ3Z7g==
|
||||
|
||||
js-yaml@^3.13.1:
|
||||
version "3.14.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
|
||||
@@ -7419,6 +7450,13 @@ parent-module@^1.0.0:
|
||||
dependencies:
|
||||
callsites "^3.0.0"
|
||||
|
||||
parse-author@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-author/-/parse-author-2.0.0.tgz#d3460bf1ddd0dfaeed42da754242e65fb684a81f"
|
||||
integrity sha1-00YL8d3Q367tQtp1QkLmX7aEqB8=
|
||||
dependencies:
|
||||
author-regex "^1.0.0"
|
||||
|
||||
parse-json@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
|
||||
@@ -7900,7 +7938,7 @@ read-pkg@^5.2.0:
|
||||
parse-json "^5.0.0"
|
||||
type-fest "^0.6.0"
|
||||
|
||||
readable-stream@3, readable-stream@^3.0.0:
|
||||
readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.1.1, readable-stream@^3.4.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
||||
@@ -8712,6 +8750,17 @@ tapable@^0.1.8:
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4"
|
||||
integrity sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=
|
||||
|
||||
tar-stream@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
|
||||
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
|
||||
dependencies:
|
||||
bl "^4.0.3"
|
||||
end-of-stream "^1.4.1"
|
||||
fs-constants "^1.0.0"
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^3.1.1"
|
||||
|
||||
temp-file@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/temp-file/-/temp-file-3.4.0.tgz#766ea28911c683996c248ef1a20eea04d51652c7"
|
||||
|
||||
Reference in New Issue
Block a user