wip: migrate client to typescript

pull/18/head
Evan You 4 years ago
parent cb0a832315
commit a5e3d5d2f5

@ -13,7 +13,7 @@ if (!command || command === 'dev') {
if (root) { if (root) {
argv.root = root argv.root = root
} }
require('../dist') require('../dist/node')
.createServer(argv) .createServer(argv)
.then((server) => { .then((server) => {
server.listen(port, () => { server.listen(port, () => {
@ -24,7 +24,7 @@ if (!command || command === 'dev') {
console.error(chalk.red(`failed to start server. error:\n`), err) console.error(chalk.red(`failed to start server. error:\n`), err)
}) })
} else if (command === 'build') { } else if (command === 'build') {
require('../dist') require('../dist/node')
.build(argv) .build(argv)
.catch((err) => { .catch((err) => {
console.error(chalk.red(`build error:\n`), err) console.error(chalk.red(`build error:\n`), err)

@ -1,21 +0,0 @@
import { inject } from 'vue'
/**
* @typedef {import('vue').Ref<import('src').PageData>} PageDataRef
*/
/**
* @type {import('vue').InjectionKey<PageDataRef>}
*/
export const pageDataSymbol = Symbol()
/**
* @returns {PageDataRef}
*/
export function usePageData() {
const data = inject(pageDataSymbol)
if (!data) {
throw new Error('usePageData() is called without provider.')
}
return data
}

@ -1,25 +0,0 @@
import serialized from '@siteData'
import { hot } from 'vite/hmr'
import { ref, readonly } from 'vue'
/**
* @param {string} data
* @returns {any}
*/
const parse = (data) => readonly(JSON.parse(data))
/**
* @type {import('vue').Ref<import('src').SiteData>}
*/
export const siteDataRef = ref(parse(serialized))
export function useSiteData() {
return siteDataRef
}
// hmr
if (__DEV__) {
hot.accept('/@siteData', (m) => {
siteDataRef.value = parse(m.default)
})
}

@ -1,18 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"lib": ["ESNext", "DOM"],
"moduleResolution": "node",
"checkJs": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"noImplicitAny": true,
"paths": {
"/@app/*": ["app/*"],
"/@theme/*": ["theme-default/*"],
"vitepress": ["app/exports.js"],
"src": ["../dist/index.d.ts"]
}
},
"include": ["."]
}

@ -1,20 +0,0 @@
import Layout from './Layout.vue'
/**
* @typedef {{
* app: import('vue').App
* router: import('../app/router').Router
* siteData: import('vue').Ref<object>
* }} EnhanceAppContext
*
* @type {{
* Layout: import('vue').ComponentOptions
* NotFound?: import('vue').ComponentOptions
* enhanceApp?: (ctx: EnhanceAppContext) => void
* }}
*/
const Theme = {
Layout
}
export default Theme

@ -2,15 +2,16 @@
"name": "vitepress", "name": "vitepress",
"version": "0.1.1", "version": "0.1.1",
"description": "", "description": "",
"main": "dist/index.js", "main": "dist/node/index.js",
"types": "dist/index.d.ts", "types": "index.d.ts",
"bin": { "bin": {
"vitepress": "bin/vitepress.js" "vitepress": "bin/vitepress.js"
}, },
"files": [ "files": [
"bin", "bin",
"lib", "lib",
"dist" "dist",
"types"
], ],
"keywords": [ "keywords": [
"vite", "vite",
@ -26,10 +27,13 @@
}, },
"homepage": "https://github.com/vuejs/vitepress/tree/master/#readme", "homepage": "https://github.com/vuejs/vitepress/tree/master/#readme",
"scripts": { "scripts": {
"dev": "tsc -w -p src", "dev": "run-p dev-client dev-node",
"build": "rm -rf dist && tsc -p src", "dev-client": "tsc -w -p src/client",
"dev-node": "tsc -w -p src/node",
"build": "rm -rf dist && tsc -p src/client && tsc -p src/node",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"prepublishOnly": "yarn build && yarn changelog" "prepublishOnly": "yarn build && yarn changelog",
"postpublish": "git add CHANGELOG.md && git commit -m 'chore: changelog [ci skip]'"
}, },
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
@ -75,6 +79,7 @@
"@types/node": "^13.13.4", "@types/node": "^13.13.4",
"conventional-changelog-cli": "^2.0.31", "conventional-changelog-cli": "^2.0.31",
"lint-staged": "^10.2.1", "lint-staged": "^10.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"typescript": "^3.8.3", "typescript": "^3.8.3",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"

@ -1,20 +1,13 @@
import { watchEffect } from 'vue' import { watchEffect } from 'vue'
import { siteDataRef } from './siteData' import { siteDataRef } from './siteData'
import { PageDataRef } from './pageData'
import { HeadConfig } from '../../../../types/shared'
/** export function useUpdateHead(pageDataRef: PageDataRef) {
* @param {import('./pageData').PageDataRef} pageDataRef const metaTags: HTMLElement[] = Array.from(document.querySelectorAll('meta'))
*/
export function useUpdateHead(pageDataRef) {
/**
* @type {HTMLElement[]}
*/
const metaTags = Array.from(document.querySelectorAll('meta'))
let isFirstUpdate = true let isFirstUpdate = true
/** const updateHeadTags = (newTags: HeadConfig[]) => {
* @param {import('src').HeadConfig[]} newTags
*/
const updateHeadTags = (newTags) => {
if (!__DEV__ && isFirstUpdate) { if (!__DEV__ && isFirstUpdate) {
// in production, the initial meta tags are already pre-rendered so we // in production, the initial meta tags are already pre-rendered so we
// skip the first update. // skip the first update.
@ -38,20 +31,20 @@ export function useUpdateHead(pageDataRef) {
const pageTitle = pageData && pageData.title const pageTitle = pageData && pageData.title
document.title = (pageTitle ? pageTitle + ` | ` : ``) + siteData.title document.title = (pageTitle ? pageTitle + ` | ` : ``) + siteData.title
updateHeadTags([ updateHeadTags([
['meta', { [
'meta',
{
name: 'description', name: 'description',
content: siteData.description content: siteData.description
}], }
],
...siteData.head, ...siteData.head,
...(pageData && pageData.frontmatter.head || []) ...((pageData && pageData.frontmatter.head) || [])
]) ])
}) })
} }
/** function createHeadElement([tag, attrs, innerHTML]: HeadConfig) {
* @param {import('src').HeadConfig} item
*/
function createHeadElement([tag, attrs, innerHTML]) {
const el = document.createElement(tag) const el = document.createElement(tag)
for (const key in attrs) { for (const key in attrs) {
el.setAttribute(key, attrs[key]) el.setAttribute(key, attrs[key])

@ -0,0 +1,14 @@
import { PageData } from '../../../../types/shared'
import { inject, InjectionKey, Ref } from 'vue'
export type PageDataRef = Ref<PageData>
export const pageDataSymbol: InjectionKey<PageDataRef> = Symbol()
export function usePageData(): PageDataRef {
const data = inject(pageDataSymbol)
if (!data) {
throw new Error('usePageData() is called without provider.')
}
return data
}

@ -0,0 +1,19 @@
import serialized from '@siteData'
import { ref, readonly, Ref } from 'vue'
import { SiteData } from '../../../../types/shared'
import { hot } from 'vite/hmr'
const parse = (data: string) => readonly(JSON.parse(data) as SiteData)
export const siteDataRef: Ref<SiteData> = ref(parse(serialized))
export function useSiteData() {
return siteDataRef
}
// hmr
if (__DEV__) {
hot.accept('/@siteData', (m) => {
siteDataRef.value = parse(m.default)
})
}

@ -1,9 +1,18 @@
// exports in this file are exposed to themes and md files via 'vitepress' // exports in this file are exposed to themes and md files via 'vitepress'
// so the user can do `import { usePageData } from 'vitepress'` // so the user can do `import { usePageData } from 'vitepress'`
// theme types
export * from './theme'
// composables
export { useSiteData } from './composables/siteData' export { useSiteData } from './composables/siteData'
export { usePageData } from './composables/pageData' export { usePageData } from './composables/pageData'
export { useRouter, useRoute } from './router' export { useRouter, useRoute } from './router'
// components
export { Content } from './components/Content' export { Content } from './components/Content'
import Debug from './components/Debug.vue'
import _Debug from './components/Debug.vue'
import { ComponentOptions } from 'vue'
const Debug = _Debug as ComponentOptions
export { Debug } export { Debug }

@ -35,10 +35,7 @@ export function createApp() {
} }
let isInitialPageLoad = inBrowser let isInitialPageLoad = inBrowser
/** let initialPath: string
* @type string
*/
let initialPath
const router = createRouter((route) => { const router = createRouter((route) => {
let pagePath = route.path.replace(/\.html$/, '') let pagePath = route.path.replace(/\.html$/, '')

@ -1,46 +1,31 @@
import { reactive, inject, nextTick, markRaw } from 'vue' import { reactive, inject, nextTick, markRaw } from 'vue'
import type { Component, InjectionKey } from 'vue'
/** export interface Route {
* @typedef {import('vue').Component} Component path: string
* contentComponent: Component | null
* @typedef {{ }
* path: string
* contentComponent: Component | null export interface Router {
* }} Route route: Route
* go: (href?: string) => Promise<void>
* @typedef {{ }
* route: Route
* go: (href?: string) => Promise<void>
* }} Router
*/
/** export const RouterSymbol: InjectionKey<Router> = Symbol()
* @type {import('vue').InjectionKey<Router>}
*/
export const RouterSymbol = Symbol()
/** const getDefaultRoute = (): Route => ({
* @returns {Route}
*/
const getDefaultRoute = () => ({
path: '/', path: '/',
contentComponent: null contentComponent: null
}) })
/** export function createRouter(
* @param {(route: Route) => Component | Promise<Component>} loadComponent loadComponent: (route: Route) => Component | Promise<Component>,
* @param {Component} [fallbackComponent] fallbackComponent?: Component
* @returns {Router} ): Router {
*/
export function createRouter(loadComponent, fallbackComponent) {
const route = reactive(getDefaultRoute()) const route = reactive(getDefaultRoute())
const inBrowser = typeof window !== 'undefined' const inBrowser = typeof window !== 'undefined'
/** function go(href?: string) {
* @param {string} [href]
* @returns {Promise<void>}
*/
function go(href) {
href = href || (inBrowser ? location.href : '/') href = href || (inBrowser ? location.href : '/')
if (inBrowser) { if (inBrowser) {
// save scroll position before changing url // save scroll position before changing url
@ -50,12 +35,7 @@ export function createRouter(loadComponent, fallbackComponent) {
return loadPage(href) return loadPage(href)
} }
/** async function loadPage(href: string, scrollPosition = 0) {
* @param {string} href
* @param {number} scrollPosition
* @returns {Promise<void>}
*/
async function loadPage(href, scrollPosition = 0) {
// we are just using URL to parse the pathname and hash - the base doesn't // we are just using URL to parse the pathname and hash - the base doesn't
// matter and is only passed to support same-host hrefs. // matter and is only passed to support same-host hrefs.
const targetLoc = new URL(href, `http://vuejs.org`) const targetLoc = new URL(href, `http://vuejs.org`)
@ -77,10 +57,7 @@ export function createRouter(loadComponent, fallbackComponent) {
await nextTick() await nextTick()
if (targetLoc.hash && !scrollPosition) { if (targetLoc.hash && !scrollPosition) {
/** const target = document.querySelector(targetLoc.hash) as HTMLElement
* @type {HTMLElement | null}
*/
const target = document.querySelector(targetLoc.hash)
if (target) { if (target) {
scrollPosition = target.offsetTop scrollPosition = target.offsetTop
} }
@ -108,11 +85,8 @@ export function createRouter(loadComponent, fallbackComponent) {
if (inBrowser) { if (inBrowser) {
window.addEventListener( window.addEventListener(
'click', 'click',
/**
* @param {*} e
*/
(e) => { (e) => {
const link = e.target.closest('a') const link = (e.target as Element).closest('a')
if (link) { if (link) {
const { href, target } = link const { href, target } = link
const targetUrl = new URL(href) const targetUrl = new URL(href)
@ -142,32 +116,18 @@ export function createRouter(loadComponent, fallbackComponent) {
{ capture: true } { capture: true }
) )
window.addEventListener( window.addEventListener('popstate', (e) => {
'popstate',
/**
* @param {*} e
*/
(e) => {
loadPage(location.href, (e.state && e.state.scrollPosition) || 0) loadPage(location.href, (e.state && e.state.scrollPosition) || 0)
} })
)
} }
/** return {
* @type {Router}
*/
const router = {
route, route,
go go
} }
return router
} }
/** export function useRouter(): Router {
* @return {Router}
*/
export function useRouter() {
const router = inject(RouterSymbol) const router = inject(RouterSymbol)
if (!router) { if (!router) {
throw new Error('useRouter() is called without provider.') throw new Error('useRouter() is called without provider.')
@ -176,9 +136,6 @@ export function useRouter() {
return router return router
} }
/** export function useRoute(): Route {
* @returns {Route}
*/
export function useRoute() {
return useRouter().route return useRouter().route
} }

@ -0,0 +1,15 @@
import { App, Ref, ComponentOptions } from 'vue'
import { Router } from './router'
import { SiteData } from '../../../types/shared'
export interface EnhanceAppContext {
app: App
router: Router
siteData: Ref<SiteData>
}
export interface Theme {
Layout: ComponentOptions
NotFound?: ComponentOptions
enhanceApp?: (ctx: EnhanceAppContext) => void
}

@ -0,0 +1,22 @@
import { App, Ref, ComponentOptions } from 'vue'
import Layout from './Layout.vue'
import { Router } from '/@app/router'
import { SiteData } from '../../../types/shared'
export interface EnhanceAppContext {
app: App
router: Router
siteData: Ref<SiteData>
}
export interface Theme {
Layout: ComponentOptions
NotFound?: ComponentOptions
enhanceApp?: (ctx: EnhanceAppContext) => void
}
const theme: Theme = {
Layout
}
export default theme

@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "../../dist/client",
"module": "esnext",
"lib": ["ESNext", "DOM"],
"paths": {
"/@app/*": ["app/*"],
"/@theme/*": ["theme-default/*"],
"vitepress": ["app/exports.ts"]
}
},
"include": [".", "../../types/shared.d.ts"]
}

@ -1,22 +0,0 @@
// string.js slugify drops non ascii chars so we have to
// use a custom implementation here
const removeDiacritics = require('diacritics').remove
// eslint-disable-next-line no-control-regex
const rControl = /[\u0000-\u001f]/g
const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'<>,.?/]+/g
export const slugify = (str: string): string => {
return removeDiacritics(str)
// Remove control characters
.replace(rControl, '')
// Replace special characters
.replace(rSpecial, '-')
// Remove continous separators
.replace(/\-{2,}/g, '-')
// Remove prefixing and trailing separtors
.replace(/^\-+|\-+$/g, '')
// ensure it doesn't start with a number (#121)
.replace(/^(\d)/, '_$1')
// lowercase
.toLowerCase()
}

@ -1,6 +1,7 @@
import path from 'path' import path from 'path'
import fs from 'fs-extra' import fs from 'fs-extra'
import { SiteConfig, HeadConfig } from '../config' import { SiteConfig } from '../config'
import { HeadConfig } from '../../../types/shared'
import { BuildResult } from 'vite' import { BuildResult } from 'vite'
import { renderToString } from '@vue/server-renderer' import { renderToString } from '@vue/server-renderer'
import { OutputChunk, OutputAsset } from 'rollup' import { OutputChunk, OutputAsset } from 'rollup'

@ -4,14 +4,10 @@ import chalk from 'chalk'
import globby from 'globby' import globby from 'globby'
import { createResolver, APP_PATH } from './resolver' import { createResolver, APP_PATH } from './resolver'
import { Resolver } from 'vite' import { Resolver } from 'vite'
import { Header } from './markdown/plugins/header' import { SiteData, HeadConfig } from '../../types/shared'
const debug = require('debug')('vitepress:config') const debug = require('debug')('vitepress:config')
export type HeadConfig =
| [string, Record<string, string>]
| [string, Record<string, string>, string]
export interface UserConfig<ThemeConfig = any> { export interface UserConfig<ThemeConfig = any> {
base?: string base?: string
title?: string title?: string
@ -32,21 +28,6 @@ export interface SiteConfig<ThemeConfig = any> {
pages: string[] pages: string[]
} }
export interface SiteData<ThemeConfig = any> {
title: string
description: string
base: string
head: HeadConfig[]
themeConfig: ThemeConfig
}
export interface PageData {
title: string
frontmatter: Record<string, any>
headers: Header[]
lastUpdated: number
}
const resolve = (root: string, file: string) => const resolve = (root: string, file: string) =>
path.join(root, `.vitepress`, file) path.join(root, `.vitepress`, file)
@ -59,7 +40,7 @@ export async function resolveConfig(
const userThemeDir = resolve(root, 'theme') const userThemeDir = resolve(root, 'theme')
const themeDir = (await fs.pathExists(userThemeDir)) const themeDir = (await fs.pathExists(userThemeDir))
? userThemeDir ? userThemeDir
: path.join(__dirname, '../lib/theme-default') : path.join(__dirname, '../client/theme-default')
const config: SiteConfig = { const config: SiteConfig = {
root, root,

@ -10,7 +10,8 @@ import { snippetPlugin } from './plugins/snippet'
import { hoistPlugin } from './plugins/hoist' import { hoistPlugin } from './plugins/hoist'
import { preWrapperPlugin } from './plugins/preWrapper' import { preWrapperPlugin } from './plugins/preWrapper'
import { linkPlugin } from './plugins/link' import { linkPlugin } from './plugins/link'
import { extractHeaderPlugin, Header } from './plugins/header' import { extractHeaderPlugin } from './plugins/header'
import { Header } from '../../../types/shared'
const emoji = require('markdown-it-emoji') const emoji = require('markdown-it-emoji')
const anchor = require('markdown-it-anchor') const anchor = require('markdown-it-anchor')

@ -32,12 +32,7 @@ export const componentPlugin = (md: MarkdownIt) => {
md.block.ruler.at('html_block', htmlBlock) md.block.ruler.at('html_block', htmlBlock)
} }
const htmlBlock: RuleBlock = ( const htmlBlock: RuleBlock = (state, startLine, endLine, silent): boolean => {
state,
startLine,
endLine,
silent
): boolean => {
let i, nextLine, lineText let i, nextLine, lineText
let pos = state.bMarks[startLine] + state.tShift[startLine] let pos = state.bMarks[startLine] + state.tShift[startLine]
let max = state.eMarks[startLine] let max = state.eMarks[startLine]

@ -3,23 +3,14 @@ import { MarkdownParsedData } from '../markdown'
import { deeplyParseHeader } from '../../utils/parseHeader' import { deeplyParseHeader } from '../../utils/parseHeader'
import { slugify } from './slugify' import { slugify } from './slugify'
export interface Header { export const extractHeaderPlugin = (md: MarkdownIt, include = ['h2', 'h3']) => {
level: number
title: string
slug: string
}
export const extractHeaderPlugin = (
md: MarkdownIt & { __data: MarkdownParsedData },
include = ['h2', 'h3']
) => {
md.renderer.rules.heading_open = (tokens, i, options, env, self) => { md.renderer.rules.heading_open = (tokens, i, options, env, self) => {
const token = tokens[i] const token = tokens[i]
if (include.includes(token.tag)) { if (include.includes(token.tag)) {
const title = tokens[i + 1].content const title = tokens[i + 1].content
const idAttr = token.attrs!.find(([name]) => name === 'id') const idAttr = token.attrs!.find(([name]) => name === 'id')
const slug = idAttr && idAttr[1] const slug = idAttr && idAttr[1]
const data = md.__data const data = (md as any).__data as MarkdownParsedData
const headers = data.headers || (data.headers = []) const headers = data.headers || (data.headers = [])
headers.push({ headers.push({
level: parseInt(token.tag.slice(1), 10), level: parseInt(token.tag.slice(1), 10),

@ -21,14 +21,16 @@ export const highlightLinePlugin = (md: MarkdownIt) => {
const lineNumbers = RE.exec(rawInfo)![1] const lineNumbers = RE.exec(rawInfo)![1]
.split(',') .split(',')
.map(v => v.split('-').map(v => parseInt(v, 10))) .map((v) => v.split('-').map((v) => parseInt(v, 10)))
const code = options.highlight const code = options.highlight
? options.highlight(token.content, langName) ? options.highlight(token.content, langName)
: token.content : token.content
const rawCode = code.replace(wrapperRE, '') const rawCode = code.replace(wrapperRE, '')
const highlightLinesCode = rawCode.split('\n').map((split, index) => { const highlightLinesCode = rawCode
.split('\n')
.map((split, index) => {
const lineNumber = index + 1 const lineNumber = index + 1
const inRange = lineNumbers.some(([start, end]) => { const inRange = lineNumbers.some(([start, end]) => {
if (start && end) { if (start && end) {
@ -40,10 +42,10 @@ export const highlightLinePlugin = (md: MarkdownIt) => {
return `<div class="highlighted">&nbsp;</div>` return `<div class="highlighted">&nbsp;</div>`
} }
return '<br>' return '<br>'
}).join('') })
.join('')
const highlightLinesWrapperCode = const highlightLinesWrapperCode = `<div class="highlight-lines">${highlightLinesCode}</div>`
`<div class="highlight-lines">${highlightLinesCode}</div>`
return highlightLinesWrapperCode + code return highlightLinesWrapperCode + code
} }

@ -3,12 +3,13 @@ import { MarkdownParsedData } from '../markdown'
// hoist <script> and <style> tags out of the returned html // hoist <script> and <style> tags out of the returned html
// so that they can be placed outside as SFC blocks. // so that they can be placed outside as SFC blocks.
export const hoistPlugin = (md: MarkdownIt & { __data: MarkdownParsedData }) => { export const hoistPlugin = (md: MarkdownIt) => {
const RE = /^<(script|style)(?=(\s|>|$))/i const RE = /^<(script|style)(?=(\s|>|$))/i
md.renderer.rules.html_block = (tokens, idx) => { md.renderer.rules.html_block = (tokens, idx) => {
const content = tokens[idx].content const content = tokens[idx].content
const hoistedTags = md.__data.hoistedTags || (md.__data.hoistedTags = []) const data = (md as any).__data as MarkdownParsedData
const hoistedTags = data.hoistedTags || (data.hoistedTags = [])
if (RE.test(content.trim())) { if (RE.test(content.trim())) {
hoistedTags.push(content) hoistedTags.push(content)
return '' return ''

@ -9,7 +9,7 @@ import { URL } from 'url'
const indexRE = /(^|.*\/)index.md(#?.*)$/i const indexRE = /(^|.*\/)index.md(#?.*)$/i
export const linkPlugin = ( export const linkPlugin = (
md: MarkdownIt & { __data: MarkdownParsedData }, md: MarkdownIt,
externalAttrs: Record<string, string> externalAttrs: Record<string, string>
) => { ) => {
md.renderer.rules.link_open = (tokens, idx, options, env, self) => { md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
@ -57,7 +57,7 @@ export const linkPlugin = (
} }
// export it for existence check // export it for existence check
const data = md.__data const data = (md as any).__data as MarkdownParsedData
const links = data.links || (data.links = []) const links = data.links || (data.links = [])
links.push(url.replace(/\.html$/, '')) links.push(url.replace(/\.html$/, ''))

@ -0,0 +1,24 @@
// string.js slugify drops non ascii chars so we have to
// use a custom implementation here
const removeDiacritics = require('diacritics').remove
// eslint-disable-next-line no-control-regex
const rControl = /[\u0000-\u001f]/g
const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'<>,.?/]+/g
export const slugify = (str: string): string => {
return (
removeDiacritics(str)
// Remove control characters
.replace(rControl, '')
// Replace special characters
.replace(rSpecial, '-')
// Remove continous separators
.replace(/\-{2,}/g, '-')
// Remove prefixing and trailing separtors
.replace(/^\-+|\-+$/g, '')
// ensure it doesn't start with a number (#121)
.replace(/^(\d)/, '_$1')
// lowercase
.toLowerCase()
)
}

@ -3,7 +3,7 @@ import matter from 'gray-matter'
import LRUCache from 'lru-cache' import LRUCache from 'lru-cache'
import { createMarkdownRenderer, MarkdownOptions } from './markdown/markdown' import { createMarkdownRenderer, MarkdownOptions } from './markdown/markdown'
import { deeplyParseHeader } from './utils/parseHeader' import { deeplyParseHeader } from './utils/parseHeader'
import { PageData } from './config' import { PageData } from '../../types/shared'
const debug = require('debug')('vitepress:md') const debug = require('debug')('vitepress:md')
const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 }) const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 })

@ -1,8 +1,7 @@
import path from 'path' import path from 'path'
import { Resolver } from 'vite' import { Resolver } from 'vite'
// built ts files are placed into /dist export const APP_PATH = path.join(__dirname, '../client/app')
export const APP_PATH = path.join(__dirname, '../lib/app')
// special virtual file // special virtual file
// we can't directly import '/@siteData' becase // we can't directly import '/@siteData' becase

@ -129,6 +129,7 @@ function createVitePressPlugin({
// serve our index.html after vite history fallback // serve our index.html after vite history fallback
if (ctx.url.endsWith('.html')) { if (ctx.url.endsWith('.html')) {
await cachedRead(ctx, path.join(APP_PATH, 'index.html')) await cachedRead(ctx, path.join(APP_PATH, 'index.html'))
ctx.status = 200
} }
}) })
} }

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../../dist/node",
"module": "commonjs",
"lib": ["ESNext", "DOM"],
"sourceMap": true
},
"include": [".", "../../types/shared.d.ts"]
}

@ -12,17 +12,22 @@
const parseEmojis = (str: string) => { const parseEmojis = (str: string) => {
const emojiData = require('markdown-it-emoji/lib/data/full.json') const emojiData = require('markdown-it-emoji/lib/data/full.json')
return String(str).replace(/:(.+?):/g, (placeholder, key) => emojiData[key] || placeholder) return String(str).replace(
/:(.+?):/g,
(placeholder, key) => emojiData[key] || placeholder
)
} }
const unescapeHtml = (html: string) => String(html) const unescapeHtml = (html: string) =>
String(html)
.replace(/&quot;/g, '"') .replace(/&quot;/g, '"')
.replace(/&#39;/g, '\'') .replace(/&#39;/g, "'")
.replace(/&#x3A;/g, ':') .replace(/&#x3A;/g, ':')
.replace(/&lt;/g, '<') .replace(/&lt;/g, '<')
.replace(/&gt;/g, '>') .replace(/&gt;/g, '>')
const removeMarkdownTokens = (str: string) => String(str) const removeMarkdownTokens = (str: string) =>
String(str)
.replace(/\[(.*)\]\(.*\)/, '$1') // []() .replace(/\[(.*)\]\(.*\)/, '$1') // []()
.replace(/(`|\*{1,3}|_)(.*?[^\\])\1/g, '$2') // `{t}` | *{t}* | **{t}** | ***{t}*** | _{t}_ .replace(/(`|\*{1,3}|_)(.*?[^\\])\1/g, '$2') // `{t}` | *{t}* | **{t}** | ***{t}*** | _{t}_
.replace(/(\\)(\*|_|`|\!)/g, '$2') // remove escape char '\' .replace(/(\\)(\*|_|`|\!)/g, '$2') // remove escape char '\'
@ -56,7 +61,4 @@ export const parseHeader = compose(
// Also clean the html that isn't wrapped by code. // Also clean the html that isn't wrapped by code.
// Because we want to support using VUE components in headers. // Because we want to support using VUE components in headers.
// e.g. https://vuepress.vuejs.org/guide/using-vue.html#badge // e.g. https://vuepress.vuejs.org/guide/using-vue.html#badge
export const deeplyParseHeader = compose( export const deeplyParseHeader = compose(removeNonCodeWrappedHTML, parseHeader)
removeNonCodeWrappedHTML,
parseHeader
)

@ -1,21 +0,0 @@
{
"compilerOptions": {
"outDir": "../dist",
"module": "commonjs",
"lib": ["ESNext", "DOM"],
"sourceMap": false,
"target": "esnext",
"moduleResolution": "node",
"esModuleInterop": true,
"declaration": true,
"allowJs": true,
"checkJs": true,
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"noImplicitAny": true,
"removeComments": false,
"preserveSymlinks": true
},
"include": ["."]
}

@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "esnext",
"moduleResolution": "node",
"strict": true,
"declaration": true,
"noUnusedLocals": true,
"esModuleInterop": true
}
}

3
types/index.d.ts vendored

@ -0,0 +1,3 @@
export * from './shared'
export * from '../dist/client/app/exports'
export * from '../dist/node/index'

26
types/shared.d.ts vendored

@ -0,0 +1,26 @@
// types shared between server and client.
export interface SiteData<ThemeConfig = any> {
title: string
description: string
base: string
head: HeadConfig[]
themeConfig: ThemeConfig
}
export type HeadConfig =
| [string, Record<string, string>]
| [string, Record<string, string>, string]
export interface PageData {
title: string
frontmatter: Record<string, any>
headers: Header[]
lastUpdated: number
}
export interface Header {
level: number
title: string
slug: string
}

@ -502,6 +502,11 @@ at-least-node@^1.0.0:
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
big.js@^5.2.2: big.js@^5.2.2:
version "5.2.2" version "5.2.2"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
@ -522,6 +527,14 @@ boolbase@^1.0.0, boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
braces@^3.0.1, braces@~3.0.2: braces@^3.0.1, braces@~3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@ -797,6 +810,11 @@ compare-func@^1.3.1:
array-ify "^1.0.0" array-ify "^1.0.0"
dot-prop "^3.0.0" dot-prop "^3.0.0"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
consolidate@^0.15.1: consolidate@^0.15.1:
version "0.15.1" version "0.15.1"
resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7" resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7"
@ -1016,6 +1034,17 @@ cross-spawn@^5.0.1:
shebang-command "^1.2.0" shebang-command "^1.2.0"
which "^1.2.9" which "^1.2.9"
cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
dependencies:
nice-try "^1.0.4"
path-key "^2.0.1"
semver "^5.5.0"
shebang-command "^1.2.0"
which "^1.2.9"
cross-spawn@^7.0.0: cross-spawn@^7.0.0:
version "7.0.2" version "7.0.2"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.2.tgz#d0d7dcfa74e89115c7619f4f721a94e1fdb716d6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.2.tgz#d0d7dcfa74e89115c7619f4f721a94e1fdb716d6"
@ -2540,6 +2569,11 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
memorystream@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
meow@^3.3.0: meow@^3.3.0:
version "3.7.0" version "3.7.0"
resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
@ -2628,6 +2662,13 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
minimist-options@^3.0.1: minimist-options@^3.0.1:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954"
@ -2687,6 +2728,11 @@ neo-async@^2.6.0:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-forge@0.9.0: node-forge@0.9.0:
version "0.9.0" version "0.9.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
@ -2722,6 +2768,21 @@ normalize-url@^3.0.0:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==
npm-run-all@^4.1.5:
version "4.1.5"
resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba"
integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==
dependencies:
ansi-styles "^3.2.1"
chalk "^2.4.1"
cross-spawn "^6.0.5"
memorystream "^0.3.1"
minimatch "^3.0.4"
pidtree "^0.3.0"
read-pkg "^3.0.0"
shell-quote "^1.6.1"
string.prototype.padend "^3.0.0"
npm-run-path@^2.0.0: npm-run-path@^2.0.0:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@ -2936,7 +2997,7 @@ path-is-absolute@1.0.1:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
path-key@^2.0.0: path-key@^2.0.0, path-key@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
@ -2992,6 +3053,11 @@ picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.2:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
pidtree@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a"
integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==
pify@^2.0.0, pify@^2.3.0: pify@^2.0.0, pify@^2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@ -3695,7 +3761,7 @@ semver-compare@^1.0.0:
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
"semver@2 || 3 || 4 || 5": "semver@2 || 3 || 4 || 5", semver@^5.5.0:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -3744,6 +3810,11 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-quote@^1.6.1:
version "1.7.2"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
signal-exit@^3.0.0, signal-exit@^3.0.2: signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@ -3876,6 +3947,14 @@ string-width@^4.1.0, string-width@^4.2.0:
is-fullwidth-code-point "^3.0.0" is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
string.prototype.padend@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.0.tgz#dc08f57a8010dc5c153550318f67e13adbb72ac3"
integrity sha512-3aIv8Ffdp8EZj8iLwREGpQaUZiPyrWrpzMBHvkiSW/bK/EGve9np07Vwy7IJ5waydpGXzQZu/F8Oze2/IWkBaA==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.17.0-next.1"
string.prototype.trimend@^1.0.0: string.prototype.trimend@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"

Loading…
Cancel
Save