mirror of
https://github.com/Luzifer/share.git
synced 2024-12-20 10:31:16 +00:00
Port frontend to Vue 3 / Bootstrap 5.3
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
2c2605d016
commit
549f0d1f36
14 changed files with 4849 additions and 1437 deletions
10
.eslintrc.js
10
.eslintrc.js
|
@ -8,7 +8,7 @@
|
|||
const Module = require('module')
|
||||
|
||||
const hacks = [
|
||||
'babel-eslint',
|
||||
'@babel/eslint-parser',
|
||||
'eslint-plugin-vue',
|
||||
]
|
||||
|
||||
|
@ -34,7 +34,7 @@ module.exports = {
|
|||
},
|
||||
|
||||
extends: [
|
||||
'plugin:vue/recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'eslint:recommended', // https://eslint.org/docs/rules/
|
||||
],
|
||||
|
||||
|
@ -44,12 +44,14 @@ module.exports = {
|
|||
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
parser: 'babel-eslint',
|
||||
parser: '@typescript-eslint/parser',
|
||||
requireConfigFile: false,
|
||||
},
|
||||
|
||||
plugins: [
|
||||
// required to lint *.vue files
|
||||
'vue',
|
||||
'@typescript-eslint',
|
||||
],
|
||||
|
||||
reportUnusedDisableDirectives: true,
|
||||
|
@ -134,6 +136,7 @@ module.exports = {
|
|||
'switch-colon-spacing': ['error'],
|
||||
'template-curly-spacing': ['error', 'never'],
|
||||
'unicode-bom': ['error', 'never'],
|
||||
'vue/comment-directive': 'off',
|
||||
'vue/new-line-between-multi-line-property': ['error'],
|
||||
'vue/no-empty-component-block': ['error'],
|
||||
'vue/no-reserved-component-names': ['error'],
|
||||
|
@ -141,6 +144,7 @@ module.exports = {
|
|||
'vue/no-unused-properties': ['error'],
|
||||
'vue/no-unused-refs': ['error'],
|
||||
'vue/no-useless-mustaches': ['error'],
|
||||
'vue/no-v-html': 'off', // Disabled for this project
|
||||
'vue/order-in-components': ['off'], // Collides with sort-keys
|
||||
'vue/require-name-property': ['error'],
|
||||
'vue/v-for-delimiter-style': ['error'],
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
frontend/fa-*
|
||||
frontend/app.*
|
||||
node_modules
|
||||
share
|
||||
src/node_modules
|
||||
|
|
|
@ -4,7 +4,12 @@ COPY . /src/share
|
|||
WORKDIR /src/share
|
||||
|
||||
RUN set -ex \
|
||||
&& apk add --update git \
|
||||
&& apk add --update \
|
||||
nodejs \
|
||||
npm \
|
||||
git \
|
||||
make \
|
||||
&& make frontend \
|
||||
&& go install \
|
||||
-ldflags "-X main.version=$(git describe --tags --always || echo dev)" \
|
||||
-mod=readonly \
|
||||
|
|
24
Makefile
24
Makefile
|
@ -7,24 +7,12 @@ lint:
|
|||
node:12-alpine \
|
||||
npx eslint --ext .js --fix frontend/app.js
|
||||
|
||||
assets: frontend/bundle.css
|
||||
assets: frontend/bundle.js
|
||||
.PHONY: frontend
|
||||
frontend: node_modules
|
||||
node ci/build.mjs
|
||||
|
||||
frontend/bundle.css:
|
||||
./ci/combine.sh $@ \
|
||||
npm/bootstrap@4/dist/css/bootstrap.min.css \
|
||||
npm/bootstrap-vue@2/dist/bootstrap-vue.min.css \
|
||||
npm/bootswatch@5/dist/darkly/bootstrap.min.css \
|
||||
gh/highlightjs/cdn-release@11.2.0/build/styles/androidstudio.min.css
|
||||
node_modules:
|
||||
npm ci
|
||||
|
||||
frontend/bundle.js:
|
||||
./ci/combine.sh $@ \
|
||||
npm/axios@0.21.1 \
|
||||
npm/vue@2 \
|
||||
npm/vue-i18n@8.25.0/dist/vue-i18n.min.js \
|
||||
npm/bootstrap-vue@2/dist/bootstrap-vue.min.js \
|
||||
npm/showdown@1 \
|
||||
gh/highlightjs/cdn-release@11.2.0/build/highlight.min.js
|
||||
|
||||
publish:
|
||||
publish: frontend
|
||||
bash ci/build.sh
|
||||
|
|
34
ci/build.mjs
Normal file
34
ci/build.mjs
Normal file
|
@ -0,0 +1,34 @@
|
|||
import esbuild from 'esbuild'
|
||||
import { sassPlugin } from 'esbuild-sass-plugin'
|
||||
import vuePlugin from 'esbuild-plugin-vue3'
|
||||
|
||||
const buildOpts = {
|
||||
assetNames: '[name]-[hash]',
|
||||
bundle: true,
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'dev'),
|
||||
},
|
||||
entryPoints: ['frontend-src/main.ts'],
|
||||
legalComments: 'none',
|
||||
loader: {
|
||||
'.ttf': 'file',
|
||||
'.woff2': 'file',
|
||||
},
|
||||
minify: true,
|
||||
outfile: 'frontend/app.js',
|
||||
plugins: [
|
||||
sassPlugin(),
|
||||
vuePlugin(),
|
||||
],
|
||||
target: [
|
||||
'chrome109',
|
||||
'edge116',
|
||||
'es2020',
|
||||
'firefox115',
|
||||
'safari15',
|
||||
],
|
||||
}
|
||||
|
||||
export { buildOpts }
|
||||
|
||||
esbuild.build(buildOpts)
|
220
frontend-src/display.vue
Normal file
220
frontend-src/display.vue
Normal file
|
@ -0,0 +1,220 @@
|
|||
<template>
|
||||
<div>
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<a
|
||||
class="navbar-brand"
|
||||
href="#"
|
||||
@click.prevent=""
|
||||
>
|
||||
<i class="fas fa-fw fa-share-alt-square mr-1" /> Share
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="card"
|
||||
>
|
||||
<div class="card-body text-center">
|
||||
<h2><i class="fas fa-spinner fa-pulse" /></h2>
|
||||
{{ $t('loading') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="error"
|
||||
class="card text-bg-danger"
|
||||
>
|
||||
<div class="card-body text-center">
|
||||
<h2><i class="fas fa-exclamation-circle" /></h2>
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="fileType.startsWith('image/')"
|
||||
class="card"
|
||||
>
|
||||
<div class="card-body text-center">
|
||||
<a :href="path">
|
||||
<img
|
||||
:src="path"
|
||||
class="img-fluid"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="fileType.startsWith('video/')"
|
||||
class="card"
|
||||
>
|
||||
<div class="card-body text-center">
|
||||
<div class="ratio ratio-16x9">
|
||||
<video controls>
|
||||
<source :src="path">
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="fileType.startsWith('audio/')"
|
||||
class="card"
|
||||
>
|
||||
<div class="card-body text-center">
|
||||
<audio
|
||||
controls
|
||||
:src="path"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="fileType.startsWith('text/markdown')"
|
||||
class="card"
|
||||
>
|
||||
<div
|
||||
class="card-body"
|
||||
v-html="renderMarkdown(text)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="fileType.startsWith('text/')"
|
||||
class="card"
|
||||
>
|
||||
<div class="card-body">
|
||||
<pre><code>{{ text }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="card"
|
||||
>
|
||||
<div class="card-body text-center">
|
||||
<h2><i class="fas fa-cloud-download-alt" /></h2>
|
||||
<button class="btn btn-success">
|
||||
{{ fileName }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import hljs from 'highlight.js'
|
||||
import showdown from 'showdown'
|
||||
|
||||
const rewrites = {
|
||||
'application/javascript': 'text/javascript',
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
fileName: '',
|
||||
fileType: null,
|
||||
loading: true,
|
||||
path: '',
|
||||
text: '',
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
hashChange() {
|
||||
const hash = window.location.hash
|
||||
|
||||
if (hash.length > 0) {
|
||||
this.path = hash.substring(1)
|
||||
} else {
|
||||
this.error = this.$i18n.t('fileNotFound')
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
renderMarkdown(text) {
|
||||
return new showdown.Converter().makeHtml(text)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.onhashchange = this.hashChange
|
||||
this.hashChange()
|
||||
},
|
||||
|
||||
name: 'ShareContentDisplay',
|
||||
|
||||
watch: {
|
||||
fileType(v) {
|
||||
// Rewrite known file types not matching the expectations above
|
||||
if (rewrites[v]) {
|
||||
this.fileType = rewrites[v]
|
||||
return
|
||||
}
|
||||
|
||||
// Load text files directly and highlight them
|
||||
if (v.startsWith('text/')) {
|
||||
this.loading = true
|
||||
fetch(this.path)
|
||||
.then(resp => resp.text())
|
||||
.then(text => {
|
||||
this.text = text
|
||||
|
||||
if (this.text.length < 200 * 1024 && v !== 'text/plain') {
|
||||
// Only highlight up to 200k and not on text/plain
|
||||
window.setTimeout(() => hljs.initHighlighting(), 100)
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
.catch(err => console.log(err))
|
||||
}
|
||||
},
|
||||
|
||||
path() {
|
||||
if (this.path.indexOf('://') >= 0) {
|
||||
// Strictly disallow loading files having any protocol in them
|
||||
this.error = this.$i18n.t('notPermitted')
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
|
||||
fetch(this.path, {
|
||||
method: 'HEAD',
|
||||
})
|
||||
.then(resp => {
|
||||
this.loading = false
|
||||
|
||||
switch (resp.status) {
|
||||
case 200:
|
||||
break
|
||||
case 403:
|
||||
this.error = this.$i18n.t('notPermitted')
|
||||
return
|
||||
case 404:
|
||||
this.error = this.$i18n.t('fileNotFound')
|
||||
return
|
||||
default:
|
||||
this.error = this.$i18n.t('genericError', { status: resp.status })
|
||||
return
|
||||
}
|
||||
|
||||
this.fileType = resp?.headers?.get('content-type') || 'application/octet-stream'
|
||||
this.fileName = this.path.substring(this.path.lastIndexOf('/') + 1)
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
39
frontend-src/main.ts
Normal file
39
frontend-src/main.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/* eslint-disable sort-imports */
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.css'
|
||||
import '@fortawesome/fontawesome-free/css/all.css'
|
||||
|
||||
import { createApp, h } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDisplay from './display.vue'
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
fileNotFound: 'The requested file has not been found.',
|
||||
genericError: 'Something went wrong (Status {status})',
|
||||
loading: 'Loading file details...',
|
||||
notPermitted: 'Access to this file was denied.',
|
||||
},
|
||||
de: {
|
||||
fileNotFound: 'Die angegebene Datei wurde nicht gefunden.',
|
||||
genericError: 'Irgendwas lief schief... (Status {status})',
|
||||
loading: 'Lade Datei-Informationen...',
|
||||
notPermitted: 'Der Zugriff auf diese Datei wurde verweigert.',
|
||||
},
|
||||
}
|
||||
|
||||
const app = createApp({
|
||||
name: 'Share',
|
||||
render() {
|
||||
return h(ContentDisplay)
|
||||
},
|
||||
})
|
||||
|
||||
app.use(createI18n({
|
||||
fallbackLocale: 'en',
|
||||
locale: new URLSearchParams(window.location.search).get('hl') || navigator.languages?.[0].split('-')[0] || navigator.language?.split('-')[0] || 'en',
|
||||
messages,
|
||||
}))
|
||||
|
||||
app.mount('#app')
|
123
frontend/app.js
123
frontend/app.js
|
@ -1,123 +0,0 @@
|
|||
/* global axios, hljs, showdown, Vue */
|
||||
|
||||
const rewrites = {
|
||||
'application/javascript': 'text/javascript',
|
||||
}
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
fileNotFound: 'The requested file has not been found.',
|
||||
genericError: 'Something went wrong (Status {status})',
|
||||
loading: 'Loading file details...',
|
||||
notPermitted: 'Access to this file was denied.',
|
||||
},
|
||||
de: {
|
||||
fileNotFound: 'Die angegebene Datei wurde nicht gefunden.',
|
||||
genericError: 'Irgendwas lief schief... (Status {status})',
|
||||
loading: 'Lade Datei-Informationen...',
|
||||
notPermitted: 'Der Zugriff auf diese Datei wurde verweigert.',
|
||||
},
|
||||
}
|
||||
|
||||
new Vue({
|
||||
data: {
|
||||
error: null,
|
||||
fileName: '',
|
||||
fileType: null,
|
||||
loading: true,
|
||||
path: '',
|
||||
text: '',
|
||||
},
|
||||
|
||||
el: '#app',
|
||||
|
||||
i18n: new VueI18n({
|
||||
fallbackLocale: 'en',
|
||||
locale: new URLSearchParams(window.location.search).get('hl') || navigator.languages?.[0].split('-')[0] || navigator.language?.split('-')[0] || 'en',
|
||||
messages,
|
||||
}),
|
||||
|
||||
methods: {
|
||||
hashChange() {
|
||||
const hash = window.location.hash
|
||||
|
||||
if (hash.length > 0) {
|
||||
this.path = hash.substring(1)
|
||||
} else {
|
||||
this.error = this.$i18n.t('fileNotFound')
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
renderMarkdown(text) {
|
||||
return new showdown.Converter().makeHtml(text)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.onhashchange = this.hashChange
|
||||
this.hashChange()
|
||||
},
|
||||
|
||||
name: 'App',
|
||||
|
||||
watch: {
|
||||
fileType(v) {
|
||||
// Rewrite known file types not matching the expectations above
|
||||
if (rewrites[v]) {
|
||||
this.fileType = rewrites[v]
|
||||
return
|
||||
}
|
||||
|
||||
// Load text files directly and highlight them
|
||||
if (v.startsWith('text/')) {
|
||||
this.loading = true
|
||||
axios.get(this.path)
|
||||
.then(resp => {
|
||||
this.text = resp.data
|
||||
|
||||
if (this.text.length < 200 * 1024 && v !== 'text/plain') {
|
||||
// Only highlight up to 200k and not on text/plain
|
||||
window.setTimeout(() => hljs.initHighlighting(), 100)
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
.catch(err => console.log(err))
|
||||
}
|
||||
},
|
||||
|
||||
path() {
|
||||
if (this.path.indexOf('://') >= 0) {
|
||||
// Strictly disallow loading files having any protocol in them
|
||||
this.error = this.$i18n.t('notPermitted')
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
|
||||
axios.head(this.path)
|
||||
.then(resp => {
|
||||
let contentType = 'application/octet-stream'
|
||||
if (resp && resp.headers && resp.headers['content-type']) {
|
||||
contentType = resp.headers['content-type']
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
this.fileType = contentType
|
||||
this.fileName = this.path.substring(this.path.lastIndexOf('/') + 1)
|
||||
})
|
||||
.catch(err => {
|
||||
switch (err.response.status) {
|
||||
case 403:
|
||||
this.error = this.$i18n.t('notPermitted')
|
||||
break
|
||||
case 404:
|
||||
this.error = this.$i18n.t('fileNotFound')
|
||||
break
|
||||
default:
|
||||
this.error = this.$i18n.t('genericError', { status: err.response.status })
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
File diff suppressed because one or more lines are too long
1160
frontend/bundle.js
1160
frontend/bundle.js
File diff suppressed because one or more lines are too long
|
@ -1,15 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<link rel="stylesheet" href="bundle.css">
|
||||
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
|
||||
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="app.css">
|
||||
|
||||
<style>
|
||||
[v-cloak] { display: none; }
|
||||
|
@ -19,98 +16,8 @@
|
|||
<title>Share</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" v-cloak>
|
||||
<b-navbar
|
||||
variant="primary"
|
||||
type="dark"
|
||||
>
|
||||
<b-navbar-brand href="#">
|
||||
<i class="fas fa-fw fa-share-alt-square mr-1"></i> Share
|
||||
</b-navbar-brand>
|
||||
</b-navbar>
|
||||
<div id="app" v-cloak></div>
|
||||
|
||||
<b-container class="mt-4">
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-card v-if="loading">
|
||||
<b-card-text class="text-center">
|
||||
<h2><i class="fas fa-spinner fa-pulse"></i></h2>
|
||||
{{ $t('loading') }}
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
|
||||
<template v-else>
|
||||
<b-card
|
||||
v-if="error"
|
||||
bg-variant="danger"
|
||||
text-variant="white"
|
||||
>
|
||||
<b-card-text class="text-center">
|
||||
<h2><i class="fas fa-exclamation-circle"></i></h2>
|
||||
{{ error }}
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
|
||||
<b-card v-else-if="fileType.startsWith('image/')">
|
||||
<b-card-text class="text-center">
|
||||
<a :href="path">
|
||||
<b-img
|
||||
:src="path"
|
||||
fluid
|
||||
/>
|
||||
</a>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
|
||||
<b-card v-else-if="fileType.startsWith('video/')">
|
||||
<b-embed
|
||||
type="video"
|
||||
:src="path"
|
||||
allowfullscreen
|
||||
controls
|
||||
/>
|
||||
</b-card>
|
||||
|
||||
<b-card v-else-if="fileType.startsWith('audio/')">
|
||||
<b-card-text class="text-center">
|
||||
<audio
|
||||
:src="path"
|
||||
controls
|
||||
/>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
|
||||
<b-card
|
||||
v-else-if="fileType.startsWith('text/markdown')"
|
||||
no-body
|
||||
>
|
||||
<b-card-body
|
||||
v-html="renderMarkdown(text)"
|
||||
/>
|
||||
</b-card>
|
||||
|
||||
<b-card v-else-if="fileType.startsWith('text/')">
|
||||
<pre><code>{{ text }}</code></pre>
|
||||
</b-card>
|
||||
|
||||
<b-card v-else>
|
||||
<b-card-text class="text-center">
|
||||
<h2><i class="fas fa-cloud-download-alt"></i></h2>
|
||||
<b-button
|
||||
:href="path"
|
||||
variant="success"
|
||||
>
|
||||
{{ fileName }}
|
||||
</b-button>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
</template>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</div>
|
||||
|
||||
<script src="bundle.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
11
main.go
11
main.go
|
@ -135,13 +135,18 @@ func doCLIUpload() error {
|
|||
}
|
||||
|
||||
func doBootstrap() error {
|
||||
for _, asset := range []string{"index.html", "app.js", "bundle.css", "bundle.js"} {
|
||||
content, err := frontend.ReadFile(strings.Join([]string{"frontend", asset}, "/"))
|
||||
files, err := frontend.ReadDir("frontend")
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing embedded files: %w", err)
|
||||
}
|
||||
|
||||
for _, asset := range files {
|
||||
content, err := frontend.ReadFile(strings.Join([]string{"frontend", asset.Name()}, "/"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading baked asset")
|
||||
}
|
||||
|
||||
if _, err := executeUpload(asset, bytes.NewReader(content), false, "", true); err != nil {
|
||||
if _, err := executeUpload(asset.Name(), bytes.NewReader(content), false, "", true); err != nil {
|
||||
return errors.Wrapf(err, "uploading bootstrap asset %q", asset)
|
||||
}
|
||||
}
|
||||
|
|
4500
package-lock.json
generated
Normal file
4500
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
23
package.json
Normal file
23
package.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.23.10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.0",
|
||||
"@typescript-eslint/parser": "^7.3.0",
|
||||
"esbuild": "^0.20.2",
|
||||
"esbuild-plugin-vue3": "^0.4.2",
|
||||
"esbuild-sass-plugin": "^3.2.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.23.0",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"name": "share",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"bootstrap": "^5.3.3",
|
||||
"highlight.js": "^11.9.0",
|
||||
"showdown": "^2.1.0",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.10.2"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue