fix: upgrade to vue3.2 (setup & ref sugar)

This commit is contained in:
mutoe 2021-08-15 17:41:36 +08:00
parent dd5a938b44
commit 1747d93281
No known key found for this signature in database
GPG Key ID: ABE5E78D073FC208
25 changed files with 97 additions and 96 deletions

View File

@ -19,6 +19,7 @@
"no-labels": "off",
"comma-dangle": ["warn", "always-multiline"],
"func-call-spacing": "off",
"prefer-const": "off",
"@typescript-eslint/promise-function-async": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/no-unused-vars": "off"

View File

@ -29,14 +29,16 @@ For more information on how to this works with other frontends/backends, head ov
- [x] [Vue router](https://next.router.vuejs.org/)
- [x] [Harlem](https://github.com/andrewcourtice/harlem) ([await Vuex v5](https://github.com/mutoe/vue3-realworld-example-app/issues/15))
- [x] Unit test ([Vue Test Utils](https://github.com/vuejs/vue-test-utils-next))
- [ ] Unit test ([Vue Testing Library](https://testing-library.com/docs/vue-testing-library/intro))
- [x] E2E test ([Cypress](https://docs.cypress.io))
- [x] [Vue tsc](https://github.com/johnsoncodehk/vue-tsc)
#### What works in [script-setup branch](https://github.com/mutoe/vue3-realworld-example-app/tree/script-setup) (based on the master branch)
- [x] [Script setup sugar](https://github.com/vuejs/rfcs/blob/sfc-improvements/active-rfcs/0000-sfc-script-setup.md)
- [x] [Script ref sugar](https://github.com/vuejs/rfcs/blob/ref-sugar/active-rfcs/0000-ref-sugar.md)
- [x] Unit test ([Vue Testing Library](https://testing-library.com/docs/vue-testing-library/intro))
- [x] [SFC script setup](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup)
- [x] [Script ref sugar (take 2)](https://github.com/vuejs/rfcs/discussions/369)
- ~~[x] Unit test ([Vue Testing Library](https://testing-library.com/docs/vue-testing-library/intro))~~
- [ ] [Cypress component test](https://docs.cypress.io/guides/component-testing/introduction#What-is-Component-Testing)
> _[Why we have the second branch?](https://github.com/mutoe/vue3-realworld-example-app/commit/c0c983dba08cb31fc96bbc3eb7f15faf469d0624#commitcomment-47600736)_

View File

@ -8,11 +8,11 @@
"serve": "vite preview",
"lint:script": "eslint \"{src/**/*.{ts,vue},cypress/**/*.js}\"",
"lint:tsc": "vue-tsc --noEmit",
"lint": "concurrently 'yarn lint:tsc' 'yarn lint:script'",
"lint": "concurrently 'yarn build' 'yarn lint:tsc' 'yarn lint:script'",
"test:unit": "jest",
"test:e2e": "yarn build && concurrently -k \"yarn serve\" \"cypress run -c baseUrl=http://localhost:5000\"",
"test:e2e:ci": "cypress run -C cypress.prod.json",
"test": "yarn test:unit && yarn test:e2e"
"test": "yarn test:e2e"
},
"dependencies": {
"@harlem/core": "^1.3.2",

View File

@ -10,14 +10,14 @@
<script lang="ts" setup>
import type { AppRouteNames } from 'src/router'
import { defineProps, useContext } from 'vue'
import { RouterLink } from 'vue-router'
import type { RouteParams } from 'vue-router'
import { useAttrs } from 'vue'
const props = defineProps<{
name: AppRouteNames
params?: RouteParams
}>()
const { attrs } = useContext()
const attrs = useAttrs()
</script>

View File

@ -34,7 +34,6 @@
<script lang="ts" setup>
import type { AppRouteNames } from 'src/router'
import { user } from 'src/store/user'
import { computed } from 'vue'
import type { RouteParams } from 'vue-router'
interface NavLink {
@ -45,10 +44,10 @@ interface NavLink {
display: 'all' | 'anonym' | 'authorized'
}
ref: username = computed(() => user.value?.username)
ref: displayStatus = computed(() => username ? 'authorized' : 'anonym')
const username = $computed(() => user.value?.username)
const displayStatus = $computed(() => username ? 'authorized' : 'anonym')
ref: allNavLinks = computed<NavLink[]>(() => [
const allNavLinks = $computed<NavLink[]>(() => [
{
name: 'global-feed',
title: 'Home',
@ -84,7 +83,7 @@ ref: allNavLinks = computed<NavLink[]>(() => [
},
])
ref: navLinks = computed(() => allNavLinks.filter(
const navLinks = $computed(() => allNavLinks.filter(
l => l.display === displayStatus || l.display === 'all',
))
</script>

View File

@ -17,18 +17,17 @@
<script lang="ts" setup>
import { limit } from 'src/services'
import { computed, defineEmit, defineProps } from 'vue'
const props = defineProps<{
page: number
count: number
}>()
const emit = defineEmit<{
const emit = defineEmits<{
(e: 'page-change', index: number): void
}>()
ref: pagesCount = computed(() => Math.ceil(props.count / limit))
const pagesCount = $computed(() => Math.ceil(props.count / limit))
const isActive = (index: number) => props.page === index
const onPageChange = (index: number) => emit('page-change', index)

View File

@ -45,14 +45,13 @@
<script lang="ts" setup>
import marked from 'src/plugins/marked'
import { getArticle } from 'src/services/article/getArticle'
import { computed, reactive } from 'vue'
import { useRoute } from 'vue-router'
import ArticleDetailMeta from './ArticleDetailMeta.vue'
const route = useRoute()
const slug = route.params.slug as string
const article = reactive<Article>(await getArticle(slug))
ref: articleHandledBody = computed(() => marked(article.body))
let article = $ref<Article>(await getArticle(slug))
const articleHandledBody = $computed(() => marked(article.body))
const updateArticle = (newArticle: Article) => {
Object.assign(article, newArticle)
}

View File

@ -44,18 +44,17 @@
</template>
<script lang="ts" setup>
import { computed, defineEmit, defineProps } from 'vue'
const props = defineProps<{
comment: ArticleComment
username?: string
}>()
const emit = defineEmit<{
const emit = defineEmits<{
(e: 'remove-comment'): boolean
}>()
ref: showRemove = computed(() => (
const showRemove = $computed(() => (
props.username !== undefined && props.username === props.comment.author.username
))
</script>

View File

@ -17,7 +17,6 @@
import { getCommentsByArticle } from 'src/services/comment/getComments'
import { deleteComment } from 'src/services/comment/postComment'
import { user } from 'src/store/user'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import ArticleDetailComment from './ArticleDetailComment.vue'
import ArticleDetailCommentsForm from './ArticleDetailCommentsForm.vue'
@ -25,9 +24,9 @@ import ArticleDetailCommentsForm from './ArticleDetailCommentsForm.vue'
const route = useRoute()
const slug = route.params.slug as string
ref: username = computed(() => user.value?.username)
const username = $computed(() => user.value?.username)
ref: comments = [] as ArticleComment[]
let comments = $ref<ArticleComment[]>([])
const addComment = async (comment: ArticleComment) => {
comments.unshift(comment)

View File

@ -5,7 +5,6 @@ import registerGlobalComponents from 'src/plugins/global-components'
import { router } from 'src/router'
import { postComment } from 'src/services/comment/postComment'
import fixtures from 'src/utils/test/fixtures'
import { ref } from 'vue'
jest.mock('src/composable/useProfile')
jest.mock('src/services/comment/postComment')
@ -18,13 +17,13 @@ describe('# ArticleDetailCommentsForm', () => {
await router.push({ name: 'article', params: { slug: fixtures.article.slug } })
mockPostComment.mockResolvedValue(fixtures.comment2)
mockUseProfile.mockReturnValue({
profile: ref(fixtures.author),
profile: fixtures.author,
updateProfile: jest.fn(),
})
})
it('should display sign in button when user not logged', () => {
mockUseProfile.mockReturnValue({ profile: ref(null), updateProfile: jest.fn() })
mockUseProfile.mockReturnValue({ profile: null, updateProfile: jest.fn() })
const { container } = render(ArticleDetailCommentsForm, {
global: { plugins: [registerGlobalComponents, router] },
props: { articleSlug: fixtures.article.slug },

View File

@ -41,20 +41,19 @@
import { useProfile } from 'src/composable/useProfile'
import { postComment } from 'src/services/comment/postComment'
import { checkAuthorization, user } from 'src/store/user'
import { computed, defineEmit, defineProps } from 'vue'
const props = defineProps<{
articleSlug: string
}>()
const emit = defineEmit<{
const emit = defineEmits<{
(e: 'add-comment', comment: ArticleComment): void
}>()
ref: username = computed(() => checkAuthorization(user) ? user.value.username : '')
const { profile } = useProfile({ username: $username })
const username = $computed(() => checkAuthorization(user) ? user.value.username : '')
const { profile } = useProfile({ username })
ref: comment = ''
let comment = $ref('')
const submitComment = async () => {
const newComment = await postComment(props.articleSlug, comment)

View File

@ -69,36 +69,39 @@ import { useFollow } from 'src/composable/useFollowProfile'
import { routerPush } from 'src/router'
import { deleteArticle } from 'src/services/article/deleteArticle'
import { checkAuthorization, user } from 'src/store/user'
import { computed, defineEmit, defineProps, toRefs } from 'vue'
const props = defineProps<{
article: Article
}>()
const emit = defineEmit<{
const emit = defineEmits<{
(e: 'update', article: Article): void
}>()
const { article } = toRefs(props)
ref: displayEditButton = computed(() => checkAuthorization(user) && user.value.username === article.value.author.username)
ref: displayFollowButton = computed(() => checkAuthorization(user) && user.value.username !== article.value.author.username)
const { article } = $fromRefs(props)
const displayEditButton = $computed(() => checkAuthorization(user) && user.value.username === article.author.username)
const displayFollowButton = $computed(() => checkAuthorization(user) && user.value.username !== article.author.username)
const isFavorited = $computed(() => article.favorited)
const articleSlug = $computed(() => article.slug)
const { favoriteProcessGoing, favoriteArticle } = useFavoriteArticle({
isFavorited: computed(() => article.value.favorited),
articleSlug: computed(() => article.value.slug),
isFavorited,
articleSlug,
onUpdate: newArticle => emit('update', newArticle),
})
const onDelete = async () => {
await deleteArticle(article.value.slug)
await deleteArticle(article.slug)
await routerPush('global-feed')
}
const { followProcessGoing, toggleFollow } = useFollow({
following: computed(() => article.value.author.following),
username: computed(() => article.value.author.username),
const following = $computed(() => article.author.following)
const username = $computed(() => article.author.username)
const { toggleFollow, followProcessGoing } = useFollow({
following,
username,
onUpdate: (author: Profile) => {
const newArticle = { ...article.value, author }
const newArticle = { ...article, author }
emit('update', newArticle)
},
})

View File

@ -52,19 +52,21 @@
<script lang="ts" setup>
import { useFavoriteArticle } from 'src/composable/useFavoriteArticle'
import { computed, defineEmit, defineProps } from 'vue'
const props = defineProps<{
article: Article;
}>()
const emit = defineEmit<{
const emit = defineEmits<{
(e: 'update', article: Article): void
}>()
const isFavorited = $computed(() => props.article.favorited)
const articleSlug = $computed(() => props.article.slug)
const { favoriteProcessGoing, favoriteArticle } = useFavoriteArticle({
isFavorited: computed(() => props.article.favorited),
articleSlug: computed(() => props.article.slug),
isFavorited,
articleSlug,
onUpdate: (newArticle: Article): void => emit('update', newArticle),
})
</script>

View File

@ -26,7 +26,6 @@
import type { ArticlesType } from 'src/composable/useArticles'
import type { AppRouteNames } from 'src/router'
import { isAuthorized } from 'src/store/user'
import { computed, defineProps } from 'vue'
import type { RouteParams } from 'vue-router'
const props = defineProps<{
@ -47,7 +46,7 @@ interface ArticlesListNavLink {
icon?: string
}
ref: allLinks = computed<ArticlesListNavLink[]>(() => [
const allLinks = $computed<ArticlesListNavLink[]>(() => [
{
name: 'global-feed',
routeName: 'global-feed',
@ -79,7 +78,7 @@ ref: allLinks = computed<ArticlesListNavLink[]>(() => [
},
])
ref: show = computed<Record<ArticlesType, boolean>>(() => ({
const show = $computed<Record<ArticlesType, boolean>>(() => ({
'global-feed': props.useGlobalFeed ?? false,
'my-feed': (props.useMyFeed && isAuthorized.value) ?? false,
'tag-feed': (props.useTagFeed && props.tag !== '') ?? false,
@ -87,5 +86,5 @@ ref: show = computed<Record<ArticlesType, boolean>>(() => ({
'user-favorites-feed': (props.useUserFavorited && props.username !== '') ?? false,
}))
ref: links = computed<ArticlesListNavLink[]>(() => allLinks.filter(link => show[link.name]))
const links = $computed<ArticlesListNavLink[]>(() => allLinks.filter(link => show[link.name]))
</script>

View File

@ -3,11 +3,10 @@ import { deleteFavoriteArticle, postFavoriteArticle } from 'src/services/article
import type { AuthorizationError } from 'src/types/error'
import createAsyncProcess from 'src/utils/create-async-process'
import type { Either } from 'src/utils/either'
import { ComputedRef } from 'vue'
interface useFavoriteArticleProps {
isFavorited: ComputedRef<boolean>
articleSlug: ComputedRef<string>
isFavorited: boolean
articleSlug: string
onUpdate: (newArticle: Article) => void
}
@ -15,10 +14,10 @@ interface useFavoriteArticleProps {
export const useFavoriteArticle = ({ isFavorited, articleSlug, onUpdate }: useFavoriteArticleProps) => {
const favoriteArticle = async (): Promise<void> => {
let response: Either<AuthorizationError, Article>
if (isFavorited.value) {
response = await deleteFavoriteArticle(articleSlug.value)
if (isFavorited) {
response = await deleteFavoriteArticle(articleSlug)
} else {
response = await postFavoriteArticle(articleSlug.value)
response = await postFavoriteArticle(articleSlug)
}
if (response.isOk()) onUpdate(response.value)

View File

@ -3,11 +3,10 @@ import { deleteFollowProfile, postFollowProfile } from 'src/services/profile/fol
import type { AuthorizationError } from 'src/types/error'
import createAsyncProcess from 'src/utils/create-async-process'
import type { Either } from 'src/utils/either'
import type { ComputedRef } from 'vue'
interface UseFollowProps {
username: ComputedRef<string>
following: ComputedRef<boolean>
username: string
following: boolean
onUpdate: (profile: Profile) => void
}
@ -16,10 +15,10 @@ export function useFollow ({ username, following, onUpdate }: UseFollowProps) {
async function toggleFollow (): Promise<void> {
let response: Either<AuthorizationError, Profile>
if (following.value) {
response = await deleteFollowProfile(username.value)
if (following) {
response = await deleteFollowProfile(username)
} else {
response = await postFollowProfile(username.value)
response = await postFollowProfile(username)
}
if (response.isOk()) onUpdate(response.value)

View File

@ -1,26 +1,26 @@
import { getProfile } from 'src/services/profile/getProfile'
import { ComputedRef, ref, watch } from 'vue'
import { watch } from 'vue'
interface UseProfileProps {
username: ComputedRef<string>
username: string
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type
export function useProfile ({ username }: UseProfileProps) {
const profile = ref<Profile | null>(null)
let profile = $ref<Profile | null>(null)
async function fetchProfile (): Promise<void> {
updateProfile(null)
if (!username.value) return
const profileData = await getProfile(username.value)
if (!username) return
const profileData = await getProfile(username)
updateProfile(profileData)
}
function updateProfile (profileData: Profile | null): void {
profile.value = profileData
profile = profileData
}
watch(username, fetchProfile, { immediate: true })
watch($raw(username), fetchProfile, { immediate: true })
return {
profile,

View File

@ -68,7 +68,7 @@
<script lang="ts" setup>
import { getArticle } from 'src/services/article/getArticle'
import { postArticle, putArticle } from 'src/services/article/postArticle'
import { computed, onMounted, reactive } from 'vue'
import { onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
interface FormState {
@ -80,16 +80,16 @@ interface FormState {
const route = useRoute()
const router = useRouter()
ref: slug = computed<string>(() => route.params.slug as string)
const slug = $computed<string>(() => route.params.slug as string)
const form = reactive<FormState>({
let form = $ref<FormState>({
title: '',
description: '',
body: '',
tagList: [],
})
ref: newTag = '' as string
let newTag = $ref('')
const addTag = () => {
form.tagList.push(newTag.trim())
newTag = ''

View File

@ -65,15 +65,14 @@ import { routerPush } from 'src/router'
import { postLogin } from 'src/services/auth/postLogin'
import type { PostLoginErrors, PostLoginForm } from 'src/services/auth/postLogin'
import { updateUser } from 'src/store/user'
import { reactive } from 'vue'
ref: formRef = null as HTMLFormElement | null
ref: form = reactive<PostLoginForm>({
let formRef = $ref<HTMLFormElement | null>(null)
let form = $ref<PostLoginForm>({
email: '',
password: '',
})
ref: errors = {} as PostLoginErrors
let errors = $ref<PostLoginErrors>({})
const login = async () => {
if (!formRef?.checkValidity()) return

View File

@ -75,18 +75,19 @@ import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
ref: username = computed<string>(() => route.params.username as string)
const username = $computed<string>(() => route.params.username as string)
const { profile, updateProfile } = useProfile({ username: $username })
const { profile, updateProfile } = useProfile({ username })
const following = $computed<boolean>(() => profile?.following ?? false)
const { followProcessGoing, toggleFollow } = useFollow({
following: computed<boolean>(() => profile.value?.following ?? false),
username: $username,
following,
username,
onUpdate: (newProfileData: Profile) => updateProfile(newProfileData),
})
ref: showEdit = computed<boolean>(() => checkAuthorization(user) && user.value.username === username)
ref: showFollow = computed<boolean>(() => user.value?.username !== username)
const showEdit = $computed<boolean>(() => checkAuthorization(user) && user.value.username === username)
const showFollow = $computed<boolean>(() => user.value?.username !== username)
</script>
<style scoped>

View File

@ -72,16 +72,15 @@ import { routerPush } from 'src/router'
import { postRegister } from 'src/services/auth/postRegister'
import type { PostRegisterErrors, PostRegisterForm } from 'src/services/auth/postRegister'
import { updateUser } from 'src/store/user'
import { reactive } from 'vue'
ref: formRef = null as HTMLFormElement | null
ref: form = reactive<PostRegisterForm>({
let formRef = $ref<HTMLFormElement | null>(null)
let form = $ref<PostRegisterForm>({
username: '',
email: '',
password: '',
})
ref: errors = {} as PostRegisterErrors
let errors = $ref<PostRegisterErrors>({})
const register = async () => {
if (!formRef?.checkValidity()) return

View File

@ -78,9 +78,9 @@ import { routerPush } from 'src/router'
import { putProfile } from 'src/services/profile/putProfile'
import type { PutProfileForm } from 'src/services/profile/putProfile'
import { checkAuthorization, updateUser, user } from 'src/store/user'
import { computed, onMounted, reactive } from 'vue'
import { onMounted } from 'vue'
ref: form = reactive<PutProfileForm>({})
let form = $ref<PutProfileForm>({})
const onSubmit = async () => {
const filteredForm = Object.entries(form).reduce((a, [k, v]) => (v === null ? a : { ...a, [k]: v }), {})
@ -103,7 +103,7 @@ onMounted(async () => {
form.email = user.value.email
})
ref: isButtonDisabled = computed(() => (
const isButtonDisabled = $computed(() => (
form.image === user.value?.image &&
form.username === user.value?.username &&
form.bio === user.value?.bio &&

View File

@ -1,4 +1,3 @@
import { request } from '../services'
import params2query from './params-to-query'
describe('# params2query', () => {

View File

@ -15,6 +15,7 @@
"noUnusedLocals": true,
},
"include": [
"src"
"src",
"vite.config.js"
]
}

View File

@ -6,11 +6,15 @@ import analyzer from 'rollup-plugin-analyzer'
export default defineConfig({
resolve: {
alias: {
'src': resolve(__dirname, 'src'),
src: resolve(__dirname, 'src'),
},
},
plugins: [
vue(),
vue({
script: {
refSugar: true,
},
}),
analyzer({ summaryOnly: true }),
],
})