chore: update eslint rules
This commit is contained in:
parent
d604eaa4ef
commit
ac3c831e2b
10
.eslintrc.js
10
.eslintrc.js
|
|
@ -1,10 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
extraFileExtensions: ['.vue'],
|
||||
},
|
||||
extends: '@mutoe/eslint-config-preset-vue',
|
||||
}
|
||||
|
|
@ -6,13 +6,13 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
directory: /
|
||||
schedule:
|
||||
interval: monthly
|
||||
allow:
|
||||
- dependency-type: production
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: ".github/workflows"
|
||||
directory: .github/workflows
|
||||
schedule:
|
||||
interval: monthly
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
language: [javascript]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
|
@ -55,7 +55,7 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
|
@ -102,7 +102,7 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
|
@ -55,7 +55,7 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
|
@ -102,7 +102,7 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ Instead of having the Bootstrap theme included locally, we recommend loading the
|
|||
|
||||
Alternatively, if you want to make modifications to the theme, check out the [theme's repo](https://github.com/gothinkster/conduit-bootstrap-template).
|
||||
|
||||
|
||||
# Templates
|
||||
|
||||
- [Layout](#layout)
|
||||
|
|
@ -48,10 +47,8 @@ Alternatively, if you want to make modifications to the theme, check out the [th
|
|||
- [Create/Edit Article](#createedit-article)
|
||||
- [Article](#article)
|
||||
|
||||
|
||||
## Layout
|
||||
|
||||
|
||||
### Header
|
||||
|
||||
```html
|
||||
|
|
@ -93,7 +90,6 @@ Alternatively, if you want to make modifications to the theme, check out the [th
|
|||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
```
|
||||
|
||||
### Footer
|
||||
|
|
@ -319,7 +315,6 @@ Alternatively, if you want to make modifications to the theme, check out the [th
|
|||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -400,7 +395,6 @@ Alternatively, if you want to make modifications to the theme, check out the [th
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
```
|
||||
|
||||
### Article
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
- [Demo](https://vue3-realworld-example-app-mutoe.vercel.app)
|
||||
- [RealWorld](https://github.com/gothinkster/realworld)
|
||||
|
||||
|
||||
This codebase was created to demonstrate a fully fledged fullstack application built with **Vue3** including CRUD operations, authentication, routing, pagination, and more.
|
||||
|
||||
We've gone to great lengths to adhere to the **Vue3** community styleguides & best practices.
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'@mutoe/eslint-config-preset-ts',
|
||||
'plugin:cypress/recommended',
|
||||
],
|
||||
env: {
|
||||
'cypress/globals': true,
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['support/**/*.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { ROUTES } from './constant'
|
||||
|
||||
describe('Article', () => {
|
||||
describe('article', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' })
|
||||
cy.intercept('GET', /articles\/.+/, { fixture: 'article.json' })
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { ROUTES } from './constant'
|
||||
|
||||
describe('Auth', () => {
|
||||
describe('auth', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', /users/, { fixture: 'user.json' }).as('getUser')
|
||||
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
|
||||
cy.intercept('GET', /articles/, { fixture: 'articles.json' }).as('getArticles')
|
||||
})
|
||||
|
||||
describe('Login and logout', () => {
|
||||
describe('login and logout', () => {
|
||||
it('should login success when submit a valid login form', () => {
|
||||
cy.login()
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ describe('Auth', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Register', () => {
|
||||
describe('register', () => {
|
||||
it('should call register API and jump to home page when submit a valid form', () => {
|
||||
cy.intercept('POST', /users$/, { fixture: 'user.json' }).as('registerRequest')
|
||||
cy.visit(ROUTES.REGISTER)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ROUTES } from './constant'
|
||||
|
||||
describe('Favorite', () => {
|
||||
describe('favorite', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' }).as('getArticles')
|
||||
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { ROUTES } from './constant'
|
||||
|
||||
describe.only('Follow', () => {
|
||||
describe.only('follow', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' }).as('getArticles')
|
||||
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
|
||||
cy.intercept('GET', /profiles\/\S+/, { fixture: 'profile.json' }).as('getProfile')
|
||||
cy.fixture('article.json').then(article => {
|
||||
cy.fixture('article.json').then((article) => {
|
||||
article.article.author.username = 'foo'
|
||||
cy.intercept('GET', /articles\/\S+/, { statusCode: 200, body: article }).as('getArticle')
|
||||
})
|
||||
|
|
@ -21,7 +21,7 @@ describe.only('Follow', () => {
|
|||
})
|
||||
|
||||
it('should call follow user api when click follow user button', () => {
|
||||
cy.fixture('profile.json').then(profile => {
|
||||
cy.fixture('profile.json').then((profile) => {
|
||||
profile.profile.following = true
|
||||
cy.intercept('POST', /profiles\/\S+\/follow/, { statusCode: 200, body: profile }).as('followUser')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ROUTES } from './constant'
|
||||
|
||||
describe('Homepage', () => {
|
||||
describe('homepage', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', /articles\?tag=butt/, { fixture: 'articles-of-tag.json' }).as('getArticlesOfTag')
|
||||
cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' }).as('getArticles')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ROUTES } from './constant'
|
||||
|
||||
describe('Tag', () => {
|
||||
describe('tag', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', /articles\?tag=butt/, { fixture: 'articles-of-tag.json' }).as('getArticlesOfTag')
|
||||
cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' }).as('getArticles')
|
||||
|
|
@ -31,7 +31,7 @@ describe('Tag', () => {
|
|||
.should('have.class', 'router-link-active')
|
||||
.should('have.class', 'router-link-exact-active')
|
||||
|
||||
cy.get('a.tag-pill.tag-default:last').invoke('text').then(tag => {
|
||||
cy.get('a.tag-pill.tag-default:last').invoke('text').then((tag) => {
|
||||
const path = `#/tag/${tag}`
|
||||
|
||||
cy.url()
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"baseUrl": ".",
|
||||
"types": ["cypress", "@testing-library/cypress"],
|
||||
"isolatedModules": false
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../fixtures/**/*",
|
||||
"../support/commands.ts",
|
||||
"../support/e2e.ts"
|
||||
],
|
||||
"exclude": ["../../src"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"isolatedModules": false,
|
||||
"target": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"types": ["cypress", "@testing-library/cypress"]
|
||||
}
|
||||
"exclude": [
|
||||
"../../src"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable ts/no-unsafe-member-access,ts/no-unsafe-assignment */
|
||||
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
|
|
@ -27,7 +29,7 @@ declare global {
|
|||
}
|
||||
|
||||
Cypress.Commands.add('login', (username = 'plumrx') => {
|
||||
cy.fixture('user.json').then(authResponse => {
|
||||
cy.fixture('user.json').then((authResponse) => {
|
||||
authResponse.user.username = username
|
||||
cy.intercept('POST', /users\/login$/, { statusCode: 200, body: authResponse })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import defineConfig from '@mutoe/eslint-config'
|
||||
|
||||
export default defineConfig({
|
||||
typescript: {
|
||||
tsconfigPath: [
|
||||
'tsconfig.json',
|
||||
'tsconfig.node.json',
|
||||
'cypress/e2e/tsconfig.json',
|
||||
],
|
||||
},
|
||||
vue: {
|
||||
sfcBlocks: {
|
||||
defaultLanguage: {
|
||||
script: 'ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
cypress: true,
|
||||
},
|
||||
ignores: [
|
||||
'src/services/api.ts',
|
||||
],
|
||||
})
|
||||
34
package.json
34
package.json
|
|
@ -2,22 +2,29 @@
|
|||
"name": "vue3-realworld-example-app",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prepare": "simple-git-hooks",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview --port 4137",
|
||||
"lint:script": "eslint \"{src/**/*.{ts,vue},cypress/**/*.ts}\"",
|
||||
"lint:tsc": "vue-tsc --noEmit",
|
||||
"lint": "concurrently \"npm run lint:tsc\" \"npm run lint:script\"",
|
||||
"test:unit": "vitest run",
|
||||
"test:e2e": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress open --e2e -c baseUrl=http://localhost:4137\"",
|
||||
"test:e2e:local": "cypress open --e2e -c baseUrl=http://localhost:5173",
|
||||
"test:e2e:ci": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress run --e2e -c baseUrl=http://localhost:4137\"",
|
||||
"test:e2e:prod": "cypress run --e2e -c baseUrl=https://vue3-realworld-example-app-mutoe.vercel.app",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "eslint --fix .",
|
||||
"test": "npm run test:unit && npm run test:e2e:ci",
|
||||
"test:e2e": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress open --e2e -c baseUrl=http://localhost:4137\"",
|
||||
"test:e2e:ci": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress run --e2e -c baseUrl=http://localhost:4137\"",
|
||||
"test:e2e:local": "cypress open --e2e -c baseUrl=http://localhost:5173",
|
||||
"test:e2e:prod": "cypress run --e2e -c baseUrl=https://vue3-realworld-example-app-mutoe.vercel.app",
|
||||
"test:unit": "vitest run",
|
||||
"generate:api": "curl -sL https://raw.githubusercontent.com/gothinkster/realworld/main/api/openapi.yml -o ./src/services/openapi.yml && sta -p ./src/services/openapi.yml -o ./src/services -n api.ts"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "npm exec lint-staged",
|
||||
"pre-push": "npm run lint && npm run build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"insane": "^2.6.2",
|
||||
"marked": "^11.0.0",
|
||||
|
|
@ -26,7 +33,8 @@
|
|||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mutoe/eslint-config-preset-vue": "~3.5.3",
|
||||
"@eslint/eslintrc": "^2.1.4",
|
||||
"@mutoe/eslint-config": "^2.4.5",
|
||||
"@pinia/testing": "^0.1.3",
|
||||
"@testing-library/cypress": "^10.0.1",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
|
|
@ -48,13 +56,5 @@
|
|||
"vitest": "^1.0.0-beta.5",
|
||||
"vitest-dom": "^0.1.1",
|
||||
"vue-tsc": "^1.8.22"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{ts,vue,js}": "eslint --fix",
|
||||
"cypress/**/*.{ts,js}": "eslint --fix"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "npm exec lint-staged",
|
||||
"pre-push": "npm run lint && npm run build"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1232
pnpm-lock.yaml
1232
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -92,5 +92,4 @@ const allNavLinks = computed<NavLink[]>(() => [
|
|||
const navLinks = computed(() => allNavLinks.value.filter(
|
||||
l => l.display === displayStatus.value || l.display === 'all',
|
||||
))
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
<li
|
||||
v-for="pageNumber in pagesCount"
|
||||
:key="pageNumber"
|
||||
:class="['page-item', { active: isActive(pageNumber) }]"
|
||||
class="page-item"
|
||||
:class="[{ active: isActive(pageNumber) }]"
|
||||
>
|
||||
<a
|
||||
:aria-label="`Go to page ${pageNumber}`"
|
||||
|
|
@ -24,15 +25,14 @@ interface Props {
|
|||
count: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'page-change', index: number): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'pageChange', index: number): void
|
||||
}>()
|
||||
|
||||
const { count, page } = toRefs(props)
|
||||
const pagesCount = computed(() => Math.ceil(count.value / limit))
|
||||
const isActive = (index: number) => page.value === index
|
||||
const onPageChange = (index: number) => emit('page-change', index)
|
||||
const onPageChange = (index: number) => emit('pageChange', index)
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ const article: Article = reactive(await api.articles.getArticle(slug).then(res =
|
|||
|
||||
const articleHandledBody = computed(() => marked(article.body))
|
||||
|
||||
const updateArticle = (newArticle: Article) => {
|
||||
function updateArticle(newArticle: Article) {
|
||||
Object.assign(article, newArticle)
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ describe('# ArticleDetailComment', () => {
|
|||
expect(getByRole('button', { name: 'Delete comment' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should emit remove comment when click remove comment button', () => {
|
||||
it('should emit remove comment when click remove comment button', async () => {
|
||||
const onRemoveComment = vi.fn()
|
||||
const { getByRole } = render(ArticleDetailComment, renderOptions({
|
||||
props: {
|
||||
|
|
@ -36,7 +36,7 @@ describe('# ArticleDetailComment', () => {
|
|||
},
|
||||
}))
|
||||
|
||||
fireEvent.click(getByRole('button', { name: 'Delete comment' }))
|
||||
await fireEvent.click(getByRole('button', { name: 'Delete comment' }))
|
||||
|
||||
expect(onRemoveComment).toHaveBeenCalled()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -54,12 +54,11 @@ interface Props {
|
|||
comment: Comment
|
||||
username?: string
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
interface Emits {
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'remove-comment'): boolean
|
||||
}
|
||||
const emit = defineEmits<Emits>()
|
||||
}>()
|
||||
|
||||
const showRemove = computed(() => props.username !== undefined && props.username === props.comment.author.username)
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -32,15 +32,14 @@ const username = computed(() => user.value?.username)
|
|||
|
||||
const comments = ref<Comment[]>([])
|
||||
|
||||
const addComment = async (comment: Comment) => {
|
||||
async function addComment(comment: Comment) {
|
||||
comments.value.unshift(comment)
|
||||
}
|
||||
|
||||
const removeComment = async (commentId: number) => {
|
||||
async function removeComment(commentId: number) {
|
||||
await api.articles.deleteArticleComment(slug, commentId)
|
||||
comments.value = comments.value.filter(c => c.id !== commentId)
|
||||
}
|
||||
|
||||
comments.value = await api.articles.getArticleComments(slug).then(res => res.data.comments)
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -49,12 +49,11 @@ import { useUserStore } from 'src/store/user'
|
|||
interface Props {
|
||||
articleSlug: string
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'add-comment', comment: Comment): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'addComment', comment: Comment): void
|
||||
}>()
|
||||
|
||||
const { user } = storeToRefs(useUserStore())
|
||||
|
||||
|
|
@ -63,12 +62,11 @@ const { profile } = useProfile({ username })
|
|||
|
||||
const comment = ref('')
|
||||
|
||||
const submitComment = async () => {
|
||||
async function submitComment() {
|
||||
const newComment = await api.articles
|
||||
.createArticleComment(props.articleSlug, { comment: { body: comment.value } })
|
||||
.then(res => res.data.comment)
|
||||
emit('add-comment', newComment)
|
||||
emit('addComment', newComment)
|
||||
comment.value = ''
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ const { favoriteProcessGoing, favoriteArticle } = useFavoriteArticle({
|
|||
onUpdate: newArticle => emit('update', newArticle),
|
||||
})
|
||||
|
||||
const onDelete = async () => {
|
||||
async function onDelete() {
|
||||
await api.articles.deleteArticle(article.value.slug)
|
||||
await routerPush('global-feed')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,5 +52,4 @@ const {
|
|||
} = useArticles()
|
||||
|
||||
await fetchArticles()
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -73,5 +73,4 @@ const {
|
|||
articleSlug: computed(() => props.article.slug),
|
||||
onUpdate: (newArticle: Article): void => emit('update', newArticle),
|
||||
})
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -99,5 +99,4 @@ const show = computed<Record<ArticlesType, boolean>>(() => ({
|
|||
}))
|
||||
|
||||
const links = computed<ArticlesListNavLink[]>(() => allLinks.value.filter(link => show.value[link.name]))
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -20,16 +20,20 @@ export function useArticles () {
|
|||
if (articlesType.value === 'my-feed') {
|
||||
responsePromise = api.articles.getArticlesFeed(pageToOffset(page.value))
|
||||
.then(res => res.data)
|
||||
} else if (articlesType.value === 'tag-feed' && tag.value) {
|
||||
}
|
||||
else if (articlesType.value === 'tag-feed' && tag.value) {
|
||||
responsePromise = api.articles.getArticles({ tag: tag.value, ...pageToOffset(page.value) })
|
||||
.then(res => res.data)
|
||||
} else if (articlesType.value === 'user-feed' && username.value) {
|
||||
}
|
||||
else if (articlesType.value === 'user-feed' && username.value) {
|
||||
responsePromise = api.articles.getArticles({ author: username.value, ...pageToOffset(page.value) })
|
||||
.then(res => res.data)
|
||||
} else if (articlesType.value === 'user-favorites-feed' && username.value) {
|
||||
}
|
||||
else if (articlesType.value === 'user-favorites-feed' && username.value) {
|
||||
responsePromise = api.articles.getArticles({ favorited: username.value, ...pageToOffset(page.value) })
|
||||
.then(res => res.data)
|
||||
} else if (articlesType.value === 'global-feed') {
|
||||
}
|
||||
else if (articlesType.value === 'global-feed') {
|
||||
responsePromise = api.articles.getArticles(pageToOffset(page.value))
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
|
@ -55,11 +59,10 @@ export function useArticles () {
|
|||
const { active: articlesDownloading, run: runWrappedFetchArticles } = useAsync(fetchArticles)
|
||||
|
||||
watch(metaChanged, async () => {
|
||||
if (page.value === 1) {
|
||||
if (page.value === 1)
|
||||
await runWrappedFetchArticles()
|
||||
} else {
|
||||
else
|
||||
changePage(1)
|
||||
}
|
||||
})
|
||||
|
||||
watch(page, runWrappedFetchArticles)
|
||||
|
|
@ -81,7 +84,7 @@ export type ArticlesType = 'global-feed' | 'my-feed' | 'tag-feed' | 'user-feed'
|
|||
|
||||
export const articlesTypes: ArticlesType[] = ['global-feed', 'my-feed', 'tag-feed', 'user-feed', 'user-favorites-feed']
|
||||
|
||||
export const isArticlesType = (type: any): type is ArticlesType => articlesTypes.includes(type)
|
||||
export const isArticlesType = (type: unknown): type is ArticlesType => articlesTypes.includes(type as ArticlesType)
|
||||
|
||||
const routeNameToArticlesType: Partial<Record<AppRouteNames, ArticlesType>> = {
|
||||
'global-feed': 'global-feed',
|
||||
|
|
@ -106,9 +109,10 @@ function useArticlesMeta (): UseArticlesMetaReturn {
|
|||
|
||||
watch(
|
||||
() => route.name,
|
||||
routeName => {
|
||||
(routeName) => {
|
||||
const possibleArticlesType = routeNameToArticlesType[routeName as AppRouteNames]
|
||||
if (!isArticlesType(possibleArticlesType)) return
|
||||
if (!isArticlesType(possibleArticlesType))
|
||||
return
|
||||
|
||||
articlesType.value = possibleArticlesType
|
||||
},
|
||||
|
|
@ -117,20 +121,18 @@ function useArticlesMeta (): UseArticlesMetaReturn {
|
|||
|
||||
watch(
|
||||
() => route.params.username,
|
||||
usernameParam => {
|
||||
if (usernameParam !== username.value) {
|
||||
(usernameParam) => {
|
||||
if (usernameParam !== username.value)
|
||||
username.value = typeof usernameParam === 'string' ? usernameParam : ''
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => route.params.tag,
|
||||
tagParam => {
|
||||
if (tagParam !== tag.value) {
|
||||
(tagParam) => {
|
||||
if (tagParam !== tag.value)
|
||||
tag.value = typeof tagParam === 'string' ? tagParam : ''
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ interface useFavoriteArticleProps {
|
|||
onUpdate: (newArticle: Article) => void
|
||||
}
|
||||
|
||||
export const useFavoriteArticle = ({ isFavorited, articleSlug, onUpdate }: useFavoriteArticleProps) => {
|
||||
export function useFavoriteArticle({ isFavorited, articleSlug, onUpdate }: useFavoriteArticleProps) {
|
||||
const favoriteArticle = async () => {
|
||||
const requestor = isFavorited.value ? api.articles.deleteArticleFavorite : api.articles.createArticleFavorite
|
||||
const article = await requestor(articleSlug.value).then(res => res.data.article)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ export function useProfile ({ username }: UseProfileProps) {
|
|||
|
||||
async function fetchProfile(): Promise<void> {
|
||||
updateProfile(null)
|
||||
if (!username.value) return
|
||||
if (!username.value)
|
||||
return
|
||||
|
||||
const profileData = await api.profiles.getProfileByUsername(username.value).then(res => res.data.profile)
|
||||
updateProfile(profileData)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export const CONFIG = {
|
||||
API_HOST: import.meta.env.VITE_API_HOST || '',
|
||||
API_HOST: String(import.meta.env.VITE_API_HOST) || '',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,11 +98,11 @@ const form: FormState = reactive({
|
|||
})
|
||||
|
||||
const newTag = ref<string>('')
|
||||
const addTag = () => {
|
||||
function addTag() {
|
||||
form.tagList.push(newTag.value.trim())
|
||||
newTag.value = ''
|
||||
}
|
||||
const removeTag = (tag: string) => {
|
||||
function removeTag(tag: string) {
|
||||
form.tagList = form.tagList.filter(t => t !== tag)
|
||||
}
|
||||
|
||||
|
|
@ -116,18 +116,18 @@ async function fetchArticle (slug: string) {
|
|||
form.tagList = article.tagList
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (slug.value) fetchArticle(slug.value)
|
||||
onMounted(async () => {
|
||||
if (slug.value)
|
||||
await fetchArticle(slug.value)
|
||||
})
|
||||
|
||||
const onSubmit = async () => {
|
||||
async function onSubmit() {
|
||||
let article: Article
|
||||
if (slug.value) {
|
||||
if (slug.value)
|
||||
article = await api.articles.updateArticle(slug.value, { article: form }).then(res => res.data.article)
|
||||
} else {
|
||||
else
|
||||
article = await api.articles.createArticle({ article: form }).then(res => res.data.article)
|
||||
}
|
||||
|
||||
return router.push({ name: 'article', params: { slug: article.slug } })
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ describe('# Login page', () => {
|
|||
|
||||
it('should not trigger api call when user submit a invalid form', async () => {
|
||||
const { getByRole, getByPlaceholderText } = render(Login, renderOptions())
|
||||
const formElement = getByRole('form', { name: 'Login form' }) as HTMLFormElement
|
||||
const formElement = getByRole<HTMLFormElement>('form', { name: 'Login form' })
|
||||
vi.spyOn(formElement, 'checkValidity')
|
||||
|
||||
expect(getByRole('button', { name: 'Sign in' })).toHaveProperty('disabled', true)
|
||||
|
|
|
|||
|
|
@ -80,16 +80,18 @@ const { updateUser } = useUserStore()
|
|||
|
||||
const errors = ref()
|
||||
|
||||
const login = async () => {
|
||||
async function login() {
|
||||
errors.value = {}
|
||||
|
||||
if (!formRef.value?.checkValidity()) return
|
||||
if (!formRef.value?.checkValidity())
|
||||
return
|
||||
|
||||
try {
|
||||
const result = await api.users.login({ user: form })
|
||||
updateUser(result.data.user)
|
||||
await routerPush('global-feed')
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
if (isFetchError(error)) {
|
||||
errors.value = error.error?.errors
|
||||
return
|
||||
|
|
@ -97,5 +99,4 @@ const login = async () => {
|
|||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ const { user, isAuthorized } = storeToRefs(useUserStore())
|
|||
|
||||
const showEdit = computed<boolean>(() => isAuthorized && user.value?.username === username.value)
|
||||
const showFollow = computed<boolean>(() => user.value?.username !== username.value)
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ describe('# Register form', () => {
|
|||
|
||||
it('should not trigger api call when user submit a invalid form', async () => {
|
||||
const { getByRole, getByPlaceholderText } = render(Register, renderOptions())
|
||||
const formElement = getByRole('form', { name: 'Registration form' }) as HTMLFormElement
|
||||
const formElement = getByRole<HTMLFormElement>('form', { name: 'Registration form' })
|
||||
vi.spyOn(formElement, 'checkValidity')
|
||||
|
||||
expect(getByRole('button', { name: 'Sign up' })).toHaveProperty('disabled', true)
|
||||
|
|
|
|||
|
|
@ -89,19 +89,20 @@ const { updateUser } = useUserStore()
|
|||
|
||||
const errors = ref()
|
||||
|
||||
const register = async () => {
|
||||
async function register() {
|
||||
errors.value = {}
|
||||
|
||||
if (!formRef.value?.checkValidity()) return
|
||||
if (!formRef.value?.checkValidity())
|
||||
return
|
||||
|
||||
try {
|
||||
const result = await api.users.createUser({ user: form })
|
||||
updateUser(result.data.user)
|
||||
await routerPush('global-feed')
|
||||
} catch (error) {
|
||||
if (isFetchError(error)) {
|
||||
}
|
||||
catch (error) {
|
||||
if (isFetchError(error))
|
||||
errors.value = error.error?.errors
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -97,29 +97,30 @@ const form: UpdateUser = reactive({})
|
|||
const userStore = useUserStore()
|
||||
const errors = ref()
|
||||
|
||||
const onSubmit = async () => {
|
||||
async function onSubmit() {
|
||||
errors.value = {}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line unicorn/no-array-reduce
|
||||
// eslint-disable-next-line unicorn/no-array-reduce, ts/no-unsafe-assignment
|
||||
const filteredForm = Object.entries(form).reduce((form, [k, v]) => v === null ? form : Object.assign(form, { [k]: v }), {})
|
||||
const userData = await api.user.updateCurrentUser({ user: filteredForm }).then(res => res.data.user)
|
||||
userStore.updateUser(userData)
|
||||
await routerPush('profile', { username: userData.username })
|
||||
} catch (error) {
|
||||
if (isFetchError(error)) {
|
||||
}
|
||||
catch (error) {
|
||||
if (isFetchError(error))
|
||||
errors.value = error.error?.errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onLogout = async () => {
|
||||
async function onLogout() {
|
||||
userStore.updateUser(null)
|
||||
await routerPush('global-feed')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!userStore.isAuthorized) return await routerPush('login')
|
||||
if (!userStore.isAuthorized)
|
||||
return await routerPush('login')
|
||||
|
||||
form.image = userStore.user?.image
|
||||
form.username = userStore.user?.username
|
||||
|
|
@ -134,5 +135,4 @@ const isButtonDisabled = computed(() =>
|
|||
&& form.email === userStore.user?.email
|
||||
&& !form.password,
|
||||
)
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
/* eslint-disable array-element-newline */
|
||||
/* eslint-disable antfu/consistent-list-newline */
|
||||
import insane from 'insane'
|
||||
import { marked } from 'marked'
|
||||
|
||||
export default (markdown?: string): string => {
|
||||
if (!markdown) return ''
|
||||
if (!markdown)
|
||||
return ''
|
||||
const html = marked(markdown)
|
||||
|
||||
// eslint-disable-next-line ts/no-unsafe-return,ts/no-unsafe-call
|
||||
return insane(html, {
|
||||
allowedTags: ['a', 'article', 'b', 'blockquote', 'br', 'caption', 'code', 'del', 'details', 'div', 'em',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', 'kbd', 'li', 'main',
|
||||
|
|
@ -30,9 +32,9 @@ export default (markdown?: string): string => {
|
|||
},
|
||||
filter: ({ tag, attrs }: { tag: string, attrs: Record<string, string> }) => {
|
||||
// Display checklist
|
||||
if (tag === 'input') {
|
||||
if (tag === 'input')
|
||||
return attrs.type === 'checkbox' && attrs.disabled === ''
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@ import { userStorage } from 'src/store/user'
|
|||
|
||||
export default function (): void {
|
||||
const token = userStorage.get()?.token
|
||||
if (token !== undefined) api.setSecurityData(token)
|
||||
if (token !== undefined)
|
||||
api.setSecurityData(token)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const limit = 10
|
|||
|
||||
export const api = new Api({
|
||||
baseUrl: `${CONFIG.API_HOST}/api`,
|
||||
securityWorker: token => token ? { headers: { Authorization: `Bearer ${token}` } } : {},
|
||||
securityWorker: token => token ? { headers: { Authorization: `Bearer ${String(token)}` } } : {},
|
||||
baseApiParams: {
|
||||
headers: {
|
||||
'content-type': ContentType.Json,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable @typescript-eslint/triple-slash-reference */
|
||||
/// <reference types="vitest-dom/extend-expect" />
|
||||
import 'vitest-dom/extend-expect'
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ export const useUserStore = defineStore('user', () => {
|
|||
userStorage.set(userData)
|
||||
api.setSecurityData(userData.token)
|
||||
user.value = userData
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
userStorage.remove()
|
||||
api.setSecurityData(null)
|
||||
user.value = null
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/consistent-type-imports */
|
||||
|
||||
import type AppLink from 'src/components/AppLink.vue'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export interface GlobalComponents {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const dateFilter = (dateString: string): string => {
|
||||
export function dateFilter(dateString: string): string {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ export default class Storage<T = unknown> {
|
|||
get(): T | null {
|
||||
try {
|
||||
const value = window[this.storageType].getItem(this.key) ?? ''
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
return JSON.parse(value) as T
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ import { afterAll, afterEach, beforeAll } from 'vitest'
|
|||
import AppLink from 'src/components/AppLink.vue'
|
||||
import { routes } from 'src/router'
|
||||
|
||||
export const createTestRouter = (base?: string): Router => createRouter({
|
||||
export function createTestRouter(base?: string): Router {
|
||||
return createRouter({
|
||||
routes,
|
||||
history: createMemoryHistory(base),
|
||||
})
|
||||
}
|
||||
|
||||
interface RenderOptionsArgs {
|
||||
props: Record<string, unknown>
|
||||
|
|
@ -52,17 +54,19 @@ export function renderOptions (args: Partial<RenderOptionsArgs> = {}): RenderOpt
|
|||
...args.initialState,
|
||||
},
|
||||
stubActions: args.stubActions ?? false,
|
||||
})],
|
||||
}),
|
||||
],
|
||||
components: { AppLink },
|
||||
},
|
||||
}
|
||||
|
||||
const { initialRoute } = args
|
||||
|
||||
if (!initialRoute) return result
|
||||
if (!initialRoute)
|
||||
return result
|
||||
|
||||
return new Promise((resolve) => {
|
||||
router.replace(initialRoute).then(() => resolve(result))
|
||||
void router.replace(initialRoute).then(() => resolve(result))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +78,7 @@ export function asyncWrapper (component: ReturnType<typeof defineComponent>, pro
|
|||
{ id: 'root' },
|
||||
h(Suspense, null, {
|
||||
default() {
|
||||
// eslint-disable-next-line ts/no-unsafe-argument
|
||||
return h(component, props)
|
||||
},
|
||||
fallback: h('div', 'Loading...'),
|
||||
|
|
@ -98,11 +103,13 @@ async function waitForServerRequest (server: SetupServer, method: string, url: s
|
|||
})
|
||||
|
||||
server.events.on('response:mocked', ({ requestId: reqId }) => {
|
||||
if (reqId === expectedRequestId) resolve(expectedRequest)
|
||||
if (reqId === expectedRequestId)
|
||||
resolve(expectedRequest)
|
||||
})
|
||||
|
||||
server.events.on('request:unhandled', ({ request: req, requestId: reqId }) => {
|
||||
if (reqId === expectedRequestId) reject(new Error(`The ${req.method} ${req.url} request was unhandled.`))
|
||||
if (reqId === expectedRequestId)
|
||||
reject(new Error(`The ${req.method} ${req.url} request was unhandled.`))
|
||||
})
|
||||
})
|
||||
flush && await flushPromises()
|
||||
|
|
@ -141,14 +148,18 @@ type Listener =
|
|||
|
||||
export function setupMockServer(...listeners: Listener[]) {
|
||||
const parseArgs = (args: Listener): [string, string, number, (object | null)] => {
|
||||
if (args.length === 4) return args
|
||||
if (args.length === 4)
|
||||
return args
|
||||
if (args.length === 3) {
|
||||
if (typeof args[1] === 'number') return ['all', args[0], args[1], args[2] as object] // ['all', path, 200, object]
|
||||
if (typeof args[2] === 'number') return [args[0], args[1], args[2], null] // [method, path, status, null]
|
||||
if (typeof args[1] === 'number')
|
||||
return ['all', args[0], args[1], args[2] as object] // ['all', path, 200, object]
|
||||
if (typeof args[2] === 'number')
|
||||
return [args[0], args[1], args[2], null] // [method, path, status, null]
|
||||
return [args[0], args[1], 200, args[2]] // [method, path, 200, object]
|
||||
}
|
||||
if (args.length === 2) {
|
||||
if (typeof args[1] === 'string') return [args[0], args[1], 200, null]
|
||||
if (typeof args[1] === 'string')
|
||||
return [args[0], args[1], 200, null]
|
||||
return ['all', args[0], 200, args[1]]
|
||||
}
|
||||
return ['all', args[0], 200, null]
|
||||
|
|
|
|||
|
|
@ -17,14 +17,16 @@ export default function useAsync<T extends (...args: unknown[]) => unknown> (fn:
|
|||
try {
|
||||
const result = await fn(...args)
|
||||
return result as ReturnType<T>
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
if (isFetchError(error) && error.status === 401) {
|
||||
userStorage.remove()
|
||||
await routerPush('login')
|
||||
throw new Error('Unauthorized or token expired')
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
active.value = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,31 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"useDefineForClassFields": true,
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"cypress.config.ts",
|
||||
".eslintrc.js"
|
||||
"eslint.config.js"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/// <reference types="vitest" />
|
||||
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { URL, fileURLToPath } from 'node:url'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
import analyzer from 'rollup-plugin-analyzer'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
|
|
|
|||
Loading…
Reference in New Issue