Merge branch 'master' into script-setup

This commit is contained in:
mutoe 2021-04-05 13:17:11 +08:00
commit 0067598ee5
No known key found for this signature in database
GPG Key ID: 5DBC40174529043B
21 changed files with 1343 additions and 2984 deletions

View File

@ -19,6 +19,7 @@
"no-unused-vars": "off",
"comma-dangle": ["warn", "always-multiline"],
"@typescript-eslint/promise-function-async": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/no-unused-vars": "off"
},
"overrides": [

View File

@ -5,12 +5,14 @@
version: 2
updates:
- package-ecosystem: "npm"
- package-ecosystem: npm
directory: "/"
schedule:
interval: "weekly"
interval: monthly
allow:
- dependency-type: production
- package-ecosystem: "github-actions"
- package-ecosystem: github-actions
directory: ".github/workflows"
schedule:
interval: "weekly"
interval: monthly

67
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,67 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '43 3 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
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
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -1,53 +0,0 @@
name: Deploy
on:
workflow_run:
workflows: ["Test"]
branches: [master]
types: [completed]
jobs:
deploy:
# https://github.community/t/workflow-run-success-type-reference/133194
if: ${{ github.event.workflow_run.conclusion == 'success' }}
env:
CI: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
# https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: Get yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn install --skip-integrity-check --non-interactive --no-progress
- name: Build
run: |
yarn build --base=/vue3-realworld-example-app/
echo > dist/.nojekyll
- name: Deploy
uses: s0/git-publish-subdir-action@master
env:
REPO: self
BRANCH: gh-pages
FOLDER: dist
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -125,7 +125,7 @@ jobs:
with:
record: true
config: baseUrl=http://localhost:5000
start: yarn serve dist
start: yarn serve
wait-on: http://localhost:5000
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -2,20 +2,18 @@ import { ROUTES } from '../constants'
describe('Article', () => {
beforeEach(() => {
cy.intercept('GET', /articles\/\S+$/, { fixture: 'article.json' })
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' })
cy.intercept('GET', /tags$/, { fixture: 'articles_of_tag.json' })
cy.intercept('GET', /profiles\/\S+/, { fixture: 'profile.json' })
cy.intercept('DELETE', /articles\/\S+$/, { statusCode: 200, body: {} }).as('deleteArticle')
cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' })
cy.intercept('GET', /articles\/.+/, { fixture: 'article.json' })
cy.intercept('GET', /tags/, { fixture: 'articles_of_tag.json' })
cy.intercept('GET', /profiles\/.+/, { fixture: 'profile.json' })
cy.intercept('DELETE', /articles\/.+/, { statusCode: 200, body: {} }).as('deleteArticle')
})
describe('post article', () => {
before(() => {
it('jump to post detail page when submit create article form', () => {
cy.login()
cy.visit('/')
})
it('jump to post detail page when submit create article form', () => {
cy.intercept('POST', /articles$/, { fixture: 'article.json' })
cy.get('[href="#/article/create"]').click()
@ -45,12 +43,10 @@ describe('Article', () => {
})
describe('delete article', () => {
before(() => {
it('delete article', () => {
cy.login()
cy.visit(ROUTES.ARTICLE)
})
it('delete article', () => {
cy.get('.article-actions button.btn-outline-danger')
.contains('Delete Article')
.click()

View File

@ -4,12 +4,12 @@ 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')
cy.intercept('GET', /articles\/.+/, { fixture: 'article.json' }).as('getArticle')
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
})
it('should can access home page', () => {
cy.visit(ROUTES.HOME)
cy.wait('@getArticles')
cy.get('h1.logo-font')
.should('contain.text', 'conduit')
@ -38,7 +38,6 @@ describe('Homepage', () => {
it('should highlight Home nav-item top menu bar when page load', () => {
cy.visit(ROUTES.HOME)
cy.wait('@getArticles')
cy.get('ul.nav.navbar-nav.pull-xs-right a.nav-link')
.contains('Home')

View File

@ -5,53 +5,51 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"lint:script": "eslint \"{src/**/*.{ts,vue},cypress/**/*.js}\"",
"lint:vti": "vti diagnostics",
"lint:vti": "echo \"vti diagnostics\"",
"lint": "concurrently 'yarn tsc' 'yarn lint:script' 'yarn lint:vti'",
"test:unit": "jest --coverage",
"test:e2e": "yarn build && concurrently -k \"serve dist\" \"cypress run -c baseUrl=http://localhost:5000\"",
"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"
},
"dependencies": {
"@harlem/core": "^1.1.0",
"deepmerge": "^4.2.2",
"dompurify": "^2.2.6",
"@harlem/core": "^1.2.3",
"insane": "^2.6.2",
"marked": "^2.0.1",
"vue": "^3.0.6",
"vue-router": "^4.0.4"
"vue": "^3.0.11",
"vue-router": "^4.0.5"
},
"devDependencies": {
"@babel/core": "^7.13.1",
"@testing-library/jest-dom": "^5.11.9",
"@babel/core": "^7.13.14",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/vue": "^6.4.0",
"@types/dompurify": "^2.2.1",
"@types/jest": "^26.0.20",
"@types/marked": "^1.2.2",
"@typescript-eslint/eslint-plugin": "^4.15.2",
"@typescript-eslint/parser": "^4.15.2",
"@vitejs/plugin-vue": "^1.1.4",
"@vue/compiler-sfc": "^3.0.5",
"@types/jest": "^26.0.22",
"@typescript-eslint/eslint-plugin": "^4.20.0",
"@typescript-eslint/parser": "^4.20.0",
"@vitejs/plugin-vue": "^1.2.1",
"@vue/compiler-sfc": "^3.0.11",
"babel-jest": "^26.6.3",
"concurrently": "^6.0.0",
"cypress": "^6.5.0",
"eslint": "^7.21.0",
"cypress": "^6.8.0",
"eslint": "^7.23.0",
"eslint-config-standard": "^16.0.2",
"eslint-config-standard-with-typescript": "^20.0.0",
"eslint-plugin-cypress": "^2.11.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-vue": "^7.6.0",
"husky": "^5.1.1",
"eslint-plugin-vue": "^7.8.0",
"husky": "^4.3.8",
"jest": "^26.6.3",
"jsdom": "^16.4.0",
"jsdom": "^16.5.2",
"lint-staged": "^10.5.4",
"serve": "^11.3.2",
"ts-jest": "^26.5.1",
"typescript": "~4.2.2",
"vite": "^2.0.2",
"vti": "^0.0.24",
"rollup-plugin-analyzer": "^4.0.0",
"ts-jest": "^26.5.4",
"typescript": "^4.2.3",
"vite": "^2.1.5",
"vti": "^0.1.1",
"vue-jest": "^5.0.0-alpha.8"
},
"husky": {

View File

@ -18,8 +18,8 @@ describe('# ArticleDetail', () => {
})
})
it.skip('should render markdown body correctly', async () => {
mockGetArticle.mockResolvedValue(fixtures.article)
it('should render markdown body correctly', async () => {
mockGetArticle.mockResolvedValue({ ...fixtures.article, body: fixtures.markdown })
const { container } = render(asyncComponentWrapper(ArticleDetail), {
global: { plugins: [registerGlobalComponents, router] },
})
@ -27,12 +27,21 @@ describe('# ArticleDetail', () => {
expect(container.querySelector('.article-content')).toMatchSnapshot()
})
it.skip('should render markdown (zh-CN) body correctly', async () => {
mockGetArticle.mockResolvedValue(fixtures.article)
it('should render markdown (zh-CN) body correctly', async () => {
mockGetArticle.mockResolvedValue({ ...fixtures.article, body: fixtures.markdownCN })
const { container } = render(asyncComponentWrapper(ArticleDetail), {
global: { plugins: [registerGlobalComponents, router] },
})
expect(container.querySelector('.article-content')).toMatchSnapshot()
})
it('should filter the xss content in Markdown body', async () => {
mockGetArticle.mockResolvedValue({ ...fixtures.article, body: fixtures.markdownXss })
const { container } = render(asyncComponentWrapper(ArticleDetail), {
global: { plugins: [registerGlobalComponents, router] },
})
expect(container.querySelector('.article-content')?.textContent).not.toContain('alert')
})
})

View File

@ -43,8 +43,7 @@
</template>
<script lang="ts" setup>
import DOMPurify from 'dompurify'
import md2html from 'marked'
import marked from 'src/plugins/marked'
import { getArticle } from 'src/services/article/getArticle'
import { computed, reactive } from 'vue'
import { useRoute } from 'vue-router'
@ -53,7 +52,7 @@ import ArticleDetailMeta from './ArticleDetailMeta.vue'
const route = useRoute()
const slug = route.params.slug as string
const article = reactive<Article>(await getArticle(slug))
const articleHandledBody = computed(() => md2html(article.body, { sanitizer: DOMPurify.sanitize }))
const articleHandledBody = computed(() => marked(article.body))
const updateArticle = (newArticle: Article) => {
Object.assign(article, newArticle)
}

View File

@ -2,6 +2,7 @@ import { render } from '@testing-library/vue'
import { flushPromises } from '@vue/test-utils'
import { GlobalMountOptions } from '@vue/test-utils/dist/types'
import ArticlesList from 'src/components/ArticlesList.vue'
import registerGlobalComponents from 'src/plugins/global-components'
import { router } from 'src/router'
import { getArticles } from 'src/services/article/getArticles'
import fixtures from 'src/utils/test/fixtures'
@ -9,18 +10,18 @@ import fixtures from 'src/utils/test/fixtures'
jest.mock('src/services/article/getArticles')
describe('# ArticlesList', () => {
const mockFetchArticles = getArticles as jest.MockedFunction<typeof getArticles>
const globalMountOptions: GlobalMountOptions = {
plugins: [router],
plugins: [registerGlobalComponents, router],
}
const mockFetchArticles = getArticles as jest.MockedFunction<typeof getArticles>
beforeEach(async () => {
mockFetchArticles.mockResolvedValue({ articles: [fixtures.article], articlesCount: 1 })
await router.push('/')
})
it.skip('should render correctly', async () => {
it('should render correctly', async () => {
const wrapper = render(ArticlesList, {
global: globalMountOptions,
})

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ export function useProfile ({ username }: UseProfileProps) {
async function fetchProfile (): Promise<void> {
updateProfile(null)
if (!username.value) return
const profileData = await getProfile(username.value)
updateProfile(profileData)
}

43
src/plugins/marked.ts Normal file
View File

@ -0,0 +1,43 @@
import marked from 'marked'
import insane from 'insane'
export default (markdown: string): string => {
const html = marked(markdown, {
// Fixme: ts-jest import.meta not support
// baseUrl: import.meta.env.BASE_URL,
})
return insane(html, {
allowedTags: ['a', 'article', 'b', 'blockquote', 'br', 'caption', 'code', 'del', 'details', 'div', 'em',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'main', 'ol',
'p', 'pre', 'section', 'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'table',
'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul', 'input'],
allowedAttributes: {
a: ['href', 'name', 'target', 'title'],
iframe: ['allowfullscreen', 'frameborder', 'src'],
img: ['src', 'alt', 'title'],
i: ['class'],
h1: ['id'],
h2: ['id'],
h3: ['id'],
h4: ['id'],
h5: ['id'],
h6: ['id'],
ol: ['start'],
code: ['class'],
th: ['align', 'rowspan'],
td: ['align'],
input: ['disabled', 'type', 'checked'],
},
filter: ({
tag,
attrs,
}: any) => {
// Display checklist
if (tag === 'input') {
return attrs.type === 'checkbox' && attrs.disabled === ''
}
return true
},
})
}

1
src/shimes-vue.d.ts vendored
View File

@ -7,6 +7,7 @@ declare module '*.vue' {
interface ImportMeta {
env: {
BASE_URL: string
VITE_API_HOST: string
}
}

View File

@ -0,0 +1,2 @@
declare module 'insane';
declare module 'marked';

View File

@ -1,7 +1,4 @@
import merge from 'deepmerge'
import { NetworkError } from '../types/error'
import { NetworkError } from 'src/types/error'
import { Either, fail, success } from './either'
import params2query from './params-to-query'
@ -21,12 +18,12 @@ export default class FetchRequest {
private readonly options: FetchRequestOptions
constructor (options: Partial<FetchRequestOptions> = {}) {
this.options = merge(this.defaultOptions, options)
this.options = Object.assign({}, this.defaultOptions, options)
}
private readonly generateFinalUrl = (url: string, options: Partial<FetchRequestOptions> = {}): string => {
const prefix = options.prefix ?? this.options.prefix
const params = merge(this.options.params, options.params ?? {})
const params = Object.assign({}, this.options.params, options.params ?? {})
let finalUrl = `${prefix}${url}`
if (Object.keys(params).length > 0) finalUrl += `?${params2query(params)}`
@ -35,7 +32,7 @@ export default class FetchRequest {
}
private readonly generateFinalHeaders = (options: Partial<FetchRequestOptions> = {}): FetchRequestOptions['headers'] => {
return merge(this.options.headers, options.headers ?? {})
return Object.assign({}, this.options.headers, options.headers ?? {})
}
private readonly handleResponse = <T>(response: Response): Promise<Either<NetworkError, T>> => {

File diff suppressed because one or more lines are too long

View File

@ -1,65 +1,15 @@
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ESNext",
"module": "ESNext",
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
"jsx": "preserve",
"noEmit": true,
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
"isolatedModules": true,
/* Strict Type-Checking Options */
"strict": true,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"moduleResolution": "node",
"baseUrl": ".",
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},

View File

@ -1,6 +1,7 @@
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { defineConfig } from 'vite'
import analyzer from 'rollup-plugin-analyzer'
export default defineConfig({
resolve: {
@ -8,5 +9,8 @@ export default defineConfig({
'src': resolve(__dirname, 'src'),
},
},
plugins: [vue()],
plugins: [
vue(),
analyzer({ summaryOnly: true }),
],
})

1447
yarn.lock

File diff suppressed because it is too large Load Diff