diff --git a/docs/cores/index.md b/docs/cores/index.md index f8b8278360..2924f8fa61 100644 --- a/docs/cores/index.md +++ b/docs/cores/index.md @@ -11,25 +11,92 @@ nav: NocoBase 核心主要围绕三点: -- 数据的存储 —— 结构和关系 -- 数据的行为 —— 操作和事件 -- 数据的形态 —— 页面和区块 +- 数据的结构 +- 数据的行为 +- 数据的形态 -由此构建了 +由此抽象了三类配置协议 -- @nocobase/database:提供灵活且强大的数据库构造器 -- @nocobase/resourcer:为数据提供资源操作方法 -- @nocobase/blocks:为数据提供的 UI 模块 +- Collection:用于描述数据的结构和关系 +- Resourcer:用于描述数据资源和操作方法 +- UI Schema:用于描述用户界面(组件树结构) -更近一步 +## Collection -- @nocobase/database 和 @nocobase/resourcer 组装成了 @nocobase/server,也就是最核心的 NocoBase -- @nocobase/blocks 是前端最重要部分,提供了现代化的 Block-styled Editor/Renderer +基于 Sequelize ModelOptions - +```ts +{ + name: 'posts', + fields: [ + {type: 'string', name: 'title'}, + {type: 'text', name: 'content'}, + ], +} +``` -## 数据的存储 —— 结构和关系 +## Resourcer -## 数据的行为 —— 操作和事件 +基于资源(resource)和操作方法(action)设计,将 REST 和 RPC 思想融合起来 -## 数据的形态 —— 页面和区块 \ No newline at end of file +```ts +{ + name: 'posts', + actions: { + list: { + filter: {}, // 过滤 + fields: [], // 输出哪些字段 + sort: '-created_at', // 排序 + page: 1, + perPage: 20, + // ... + }, + get: { + filter: {}, + fields: [], + // ... + }, + create: { + fields: [], + values: {}, + // ... + }, + update: { + fields: [], + values: {}, + // ... + }, + destroy: { + filter: {}, + // ... + }, + }, +} +``` + +## UI Schema + +基于 Formily Schema 2.0 + +```ts +{ + type: 'object', + // 'x-component': 'Form', + properties: { + title: { + type: 'string', + title: '标题', + 'x-component': 'Input', + }, + content: { + type: 'string', + title: '标题', + 'x-component': 'Input.TextArea', + }, + }, +} +``` + +更进一步,构建了整个 NocoBase 架构: + + \ No newline at end of file diff --git a/docs/cores/packages/blocks.md b/docs/cores/packages/blocks.md deleted file mode 100644 index 8e91ae2755..0000000000 --- a/docs/cores/packages/blocks.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: '@nocobase/blocks' -order: 6 ---- - -# @nocobase/blocks 未实现 - - - -因为前端代码的打包还有许多细节问题,核心代码暂时也都放在 @nocobase/app 里了。 - -v0.4 版本 components 分为了 Action、Field、View 三部分,下一把版本打算都整合到 Block 里。 - - - -在 NocoBase 中,区块是所有 HTML 元素片段(包括 React/Vue 等框架的自定义元素)的总称。区块可以任意组合或嵌套,为了方便使用,内置了常用的一些区块,大致分类有: - -- Database - 数据 -- Fields - 字段 -- Buttons - 按钮 -- Media - 多媒体 -- Design - 设计 -- 自定义区块 - -## Database - 数据 - -数据类型的区块需要绑定数据源。 - -### table -### form -### descriptions -### calendar -### kanban - -## Fields - 字段 - -字段是一种特殊的子区块,只用于数据类型区块中,有两种形态:静态纯展示和动态可输入。 - -### boolean -### checkbox -### cascader -### time -### markdown -### string -### icon -### textarea -### number -### remoteSelect -### drawerSelect -### colorSelect -### subTable -### icon - -## Buttons - 按钮 - -### create -### update -### destroy -### filter -### print -### export -### button - -## Media - 多媒体 - -### markdown - -## Design - 设计 - -### grid -### drawer -### page - -## 自定义区块 diff --git a/docs/cores/packages/client.md b/docs/cores/packages/client.md deleted file mode 100644 index e5fe3b26e7..0000000000 --- a/docs/cores/packages/client.md +++ /dev/null @@ -1,256 +0,0 @@ ---- -title: '@nocobase/client' -order: 6 ---- - -# @nocobase/client 未实现 - -## 介绍 - -提供适配 Ant Design 组件的 NocoBase 客户端。 - - - -@nocobase/client 可以用于任意 React 框架中,不过还存在许多难点和细节未解决。 - - - -## Loaders - -### TemplateLoader 待完善 - -- routes:路由表,大杂烩。类型包括:layout、page、redirect、url、menuGroup,**页面就是 type=page 的 route** -- templates:模板 -- pathname:URL 路径 - -为了适应无代码需求,提供了一种 URL 和 Template 映射规则,如以下例子: - -```ts -const routes = [ - { - type: 'layout', - name: 'auth', // 因为 /login、/register 没有统一的前缀,所以这里没有配置 path - template: 'AuthLayout', // 用于处理 login、register 等页面的布局 - children: [ - { - type: 'page', - name: 'login', - path: '/login', - template: 'BlockLoader', // 用于解析 blocks 配置参数 - blocks: [], - }, - { - type: 'page', - name: 'register', - path: '/register', - template: 'BlockLoader', - blocks: [], - } - ] - }, - { - type: 'layout', - name: 'admin', - path: '/admin', // /admin 下的任意 uri 都转到这里 - template: 'AdminLoader', - redirect: '/admin/welcome', - // admin layout 提供了 top/left 布局的菜单,菜单由 children 组成 - // 通过解析 /admin/:name,找到对应子页面 - // 「菜单和页面配置」就是这部分的内容 - children: [ - { - type: 'page', - name: 'welcome', - template: 'BlockLoader', - title: '欢迎', - blocks: [], - }, - { - type: 'url', - url: 'https://www.nocobase.com/', - title: 'xxx', - }, - { - type: 'menuGroup', - title: 'xxx', - }, - ], - }, - { - // 配置跳转 - type: 'redirect', - path: '/', - redirect: '/admin', - }, -]; -``` - -
-
-
- -### BlockLoader 未实现 - -区块驱动器。 - -### AdminLoader 待完善 - -一种 top/left 菜单结构的 Admin 布局。菜单由其对应的 children 组成,通过 `/admin/:name` 映射到对应子页面,「菜单和页面配置」就是这部分的内容。 - -### ShareLoader 未实现 - -分享模块,细节待补充 - -## Blocks - -将页面内部的各个块元素进行提炼,抽象了 block(区块)的概念。 - -### Grid - 布局 - -```ts -{ - type: 'grid', - span: 12, - blocks: [ - { - col: 1, - order: 1, - }, - { - col: 2, - order: 1, - }, - { - col: 1, - order: 2, - }, - ], -} -``` - -### Descriptions - 详情 - -```ts -{ - type: 'descriptions', - fields: [], - actions: [], -} -``` - -### Form - 表单 - -```ts -{ - type: 'form', - fields: [], - // 表单提交反馈信息,细节待定 - returnType, - redirect, - message, -} -``` - -### Table - 表单 - -```ts -{ - type: 'table', - defaultPerPage: 20, - draggable: false, - filter: {}, - sort: [], - detailsOpenMode: 'drawer', - actions: [], - fields: [], - details: [], - labelField, -} -``` - -### Calendar - 日历 - -```ts -{ - type: 'calendar', - filter: {}, - detailsOpenMode: 'drawer', - actions: [], - details: [], - labelField, -} -``` - -### Kanban - 看板 - -```ts -{ - type: 'kanban', - groupField, - labelField, - fields, - filter, - actions, - detailsOpenMode, - details, -} -``` - -### Markdown - -```ts -{ - type: 'markdown', - content: '', -} -``` - -## Actions - -操作按钮 - -### create - 新增 - -### update - 编辑 - -### destroy - 删除 - -### filter - 筛选 - -### print - 打印 - -### export - 导出 - -## Fields - -字段控件 - -### boolean -### cascader -### checkbox -### checkboxes -### colorSelect -### date -### drawerSelect -### filter -### icon -### markdown -### number -### password -### percent -### radio -### remoteSelect -### string -### select -### subTable -### textarea -### time -### upload diff --git a/docs/guide/index.md b/docs/guide/index.md index 17445d8012..5c37d8d24a 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -13,7 +13,7 @@ NocoBase 是一个开源免费的无代码、低代码开发平台。 无论是 ## 架构 - + ### 微内核 @@ -41,11 +41,3 @@ NocoBase 采用微内核架构,框架只保留核心的概念,具体各类 - 直接写在代码里,多用于处理动态配置 - 保存在文件里,多用于系统表配置或纯开发配置 - 保存在数据表里,多用于业务表配置 - -## 核心点在哪里? - -NocoBase 本质上是对数据的信息化处理,包括三点核心: - -- 数据的存储 —— 结构和关系 -- 数据的行为 —— 方法和事件 -- 数据的形态 —— 页面和区块 diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000000..51695bfc20 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["packages/", ".env"], + "ignore": ["packages/app"], + "ext": "ts", + "exec": "ts-node -r dotenv/config ./packages/api/src/index.ts" +} \ No newline at end of file diff --git a/package.json b/package.json index a52ebf86ac..f57b61f0ec 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "scripts": { "bootstrap": "lerna bootstrap", "clean": "lerna clean", + "start-server": "nodemon", "start-docs": "dumi dev", "build-docs": "dumi build", "build2": "lerna run build", @@ -44,6 +45,7 @@ "koa-bodyparser": "^4.3.0", "lerna": "^3.22.0", "mockjs": "^1.1.0", + "nodemon": "^2.0.12", "pg": "^8.6.0", "pg-hstore": "^2.3.3", "prettier": "^2.3.0", diff --git a/packages/api/package.json b/packages/api/package.json index 4fbaa19193..8cd3a05ee9 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -19,7 +19,7 @@ "@nocobase/plugin-file-manager": "^0.4.0-alpha.7", "@nocobase/plugin-pages": "^0.4.0-alpha.7", "@nocobase/plugin-permissions": "^0.4.0-alpha.7", - "@nocobase/plugin-routes": "^0.4.0-alpha.7", + "@nocobase/plugin-ui-router": "^0.4.0-alpha.7", "@nocobase/plugin-ui-schema": "^0.4.0-alpha.7", "@nocobase/plugin-users": "^0.4.0-alpha.7", "@nocobase/server": "^0.4.0-alpha.7", diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts index 53adaaa339..dedd4e86f4 100644 --- a/packages/api/src/app.ts +++ b/packages/api/src/app.ts @@ -39,7 +39,7 @@ const api = Api.create({ const plugins = [ '@nocobase/plugin-collections', - '@nocobase/plugin-routes', + '@nocobase/plugin-ui-router', '@nocobase/plugin-ui-schema', // '@nocobase/plugin-action-logs', // '@nocobase/plugin-pages', diff --git a/packages/api/src/migrations/init.ts b/packages/api/src/migrations/init.ts index 6b28fe18fb..64e596464f 100644 --- a/packages/api/src/migrations/init.ts +++ b/packages/api/src/migrations/init.ts @@ -27,25 +27,27 @@ import * as uiSchema from './ui-schema'; exact: true, }, { + type: 'route', path: '/admin/:name(.+)?', component: 'AdminLayout', title: `后台`, uiSchema: uiSchema.menu, }, { + type: 'route', component: 'AuthLayout', children: [ { - name: 'login', + type: 'route', path: '/login', - component: 'DefaultPage', + component: 'RouteSchemaRenderer', title: `登录`, uiSchema: uiSchema.login, }, { - name: 'register', + type: 'route', path: '/register', - component: 'DefaultPage', + component: 'RouteSchemaRenderer', title: `注册`, uiSchema: uiSchema.register, }, diff --git a/packages/api/src/migrations/ui-schema/login.ts b/packages/api/src/migrations/ui-schema/login.ts index d06e4c780d..b69843ac97 100644 --- a/packages/api/src/migrations/ui-schema/login.ts +++ b/packages/api/src/migrations/ui-schema/login.ts @@ -2,7 +2,6 @@ import { ISchema } from '@formily/react'; export const login: ISchema = { key: 'dtf9j0b8p9u', - name: 'dtf9j0b8p9u', type: 'object', properties: { email: { diff --git a/packages/api/src/migrations/ui-schema/register.ts b/packages/api/src/migrations/ui-schema/register.ts index a129f5e3a3..993d56a2fa 100644 --- a/packages/api/src/migrations/ui-schema/register.ts +++ b/packages/api/src/migrations/ui-schema/register.ts @@ -1,6 +1,5 @@ export const register = { key: '46qlxqam3xk', - name: '46qlxqam3xk', type: 'object', properties: { username: { diff --git a/packages/client/src/components/RouteSwitch/__tests__/RouteSwitch.test.tsx b/packages/client/src/components/RouteSwitch/__tests__/RouteSwitch.test.tsx deleted file mode 100644 index 5e1384ddc9..0000000000 --- a/packages/client/src/components/RouteSwitch/__tests__/RouteSwitch.test.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import { MemoryRouter as Router, useParams, Link } from 'react-router-dom'; -import { RouteSwitch } from '../'; - -const components = { - Index: () =>
index
, - Home: ({ children }) => ( -
-

Home

- {children} -
- ), - Blog: ({ children }) => ( -
-

Blog

- {children} -
- ), - BlogPost: () => { - let { slug } = useParams(); - return
Now showing post {slug}
; - }, - Login: () =>
login
, - Register: () =>
register
, -}; - -const routes = [ - { - type: 'redirect', - from: '/blog/123', - to: '/blog/1234', - }, - // { - // component: 'Home', - // routes: [ - // { - // path: '/blog/123555', - // component: () =>
/blog/123555
, - // }, - // ], - // }, - { - path: '/blog', - component: 'Blog', - routes: [ - { - path: '/blog/:slug', - // exact: true, - component: 'BlogPost', - }, - ], - }, - { - component: 'Home', - routes: [ - // { - // path: '/blog/123555', - // component: () =>
/blog/123555
, - // }, - { - path: '/login', - component: 'Login', - }, - { - path: '/register', - component: 'Register', - }, - { - path: '/', - // exact: true, - component: 'Index', - }, - ], - }, -]; - -it('route component', () => { - const t = renderer - .create( - -
test
, - }, - ]} - /> -
, - ) - .toJSON(); - expect(t).toMatchSnapshot(); -}); - -it('pathname=/', () => { - const t = renderer - .create( - - - , - ) - .toJSON(); - expect(t).toMatchSnapshot(); -}); - -it('pathname=/login', () => { - const t = renderer - .create( - - - , - ) - .toJSON(); - expect(t).toMatchSnapshot(); -}); - -it('pathname=/blog/123', () => { - const t = renderer - .create( - - - , - ) - .toJSON(); - expect(t).toMatchSnapshot(); -}); diff --git a/packages/client/src/components/RouteSwitch/__tests__/__snapshots__/RouteSwitch.test.tsx.snap b/packages/client/src/components/RouteSwitch/__tests__/__snapshots__/RouteSwitch.test.tsx.snap deleted file mode 100644 index 46917a102b..0000000000 --- a/packages/client/src/components/RouteSwitch/__tests__/__snapshots__/RouteSwitch.test.tsx.snap +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`pathname=/ 1`] = ` -
-

- Home -

-
- index -
-
-`; - -exports[`pathname=/blog/123 1`] = ` -
-

- Blog -

-
- Now showing post - 1234 -
-
-`; - -exports[`pathname=/login 1`] = ` -
-

- Home -

-
- login -
-
-`; - -exports[`route component 1`] = ` -
- test -
-`; diff --git a/packages/client/src/components/RouteSwitch/demos/demo1.tsx b/packages/client/src/components/RouteSwitch/demos/demo1.tsx deleted file mode 100644 index 7b996c0c3c..0000000000 --- a/packages/client/src/components/RouteSwitch/demos/demo1.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import { - Link, - useLocation, - useRouteMatch, - MemoryRouter as Router, -} from 'react-router-dom'; -import { - RouteSwitch, - AuthLayout, - AdminLayout, - PageTemplate, -} from '@nocobase/client'; - -const routes = [ - { - path: '/admin/:slug(.+)?', - component: 'AdminLayout', - }, - { - component: 'AuthLayout', - routes: [ - { - name: 'login', - path: '/login', - component: 'PageTemplate', - title: '登录', - }, - { - name: 'register', - path: '/register', - component: 'PageTemplate', - title: '注册', - }, - ], - }, - { - type: 'redirect', - from: '/', - to: '/admin', - exact: true, - }, -]; - -const components = { - AuthLayout, - AdminLayout, - PageTemplate, -}; - -function App() { - const location = useLocation(); - return ( -
-
{location.pathname}
- - -
- ); -} - -export default () => { - return ( - - - - ); -}; diff --git a/packages/client/src/components/RouteSwitch/index.md b/packages/client/src/components/RouteSwitch/index.md deleted file mode 100644 index c8f6e86862..0000000000 --- a/packages/client/src/components/RouteSwitch/index.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: RouteSwitch - 路由转换器 -nav: - title: 组件 - path: /client -group: - order: 3 - title: 其他 - path: /client/others ---- - -# RouteSwitch - 路由转换器 - -## 代码演示 - - diff --git a/packages/client/src/components/RouteSwitch/index.tsx b/packages/client/src/components/RouteSwitch/index.tsx deleted file mode 100644 index 81807d69ea..0000000000 --- a/packages/client/src/components/RouteSwitch/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { createContext, useContext } from 'react'; -import { Switch, Route as ReactRoute, Redirect } from 'react-router-dom'; - -export const RouteComponentContext = createContext({}); - -export interface RouteSwitchProps { - routes?: any[]; - components?: any; -} - -export function RouteSwitch(props: RouteSwitchProps) { - const { routes } = props; - if (!Array.isArray(routes)) { - return null; - } - const components = props.components || useContext(RouteComponentContext); - return ( - - - {routes.map((route, key) => { - if (route.type === 'redirect') { - return ( - - ); - } - if (route.component) { - let path = route.path; - if (!path && Array.isArray(route.routes)) { - path = route.routes.map((r) => r.path); - } - return ( - { - const Component = - typeof route.component === 'string' - ? components[route.component] - : route.component; - if (!Component) { - return null; - } - return ( - - - - ); - }} - /> - ); - } - })} - - - ); -} - -export default RouteSwitch; diff --git a/packages/client/src/components/admin-layout/index.md b/packages/client/src/components/admin-layout/index.md new file mode 100644 index 0000000000..276c9f236e --- /dev/null +++ b/packages/client/src/components/admin-layout/index.md @@ -0,0 +1,14 @@ +--- +title: AdminLayout - 后台布局 +nav: + title: 组件 + path: /client +group: + order: 2 + title: Templates + path: /client/templates +--- + +# AdminLayout - 后台布局 + +内置的后台布局模板,提供了基础的菜单和路由切换。 diff --git a/packages/client/src/components/admin-layout/index.tsx b/packages/client/src/components/admin-layout/index.tsx new file mode 100644 index 0000000000..4fe3e23423 --- /dev/null +++ b/packages/client/src/components/admin-layout/index.tsx @@ -0,0 +1,92 @@ +import React, { useContext, useEffect, useRef, useState } from 'react'; +import { + Button, + Spin, + Layout, + PageHeader, + Modal, + Menu, + Collapse, + Dropdown, +} from 'antd'; +import isEmpty from 'lodash/isEmpty'; +import { + Link, + useLocation, + useRouteMatch, + useHistory, + Redirect, +} from 'react-router-dom'; +import { SchemaRenderer } from '../../schemas'; +import { useRequest } from 'ahooks'; +import './style.less'; + +function LayoutWithMenu({ schema }) { + const match = useRouteMatch(); + const location = useLocation(); + const sideMenuRef = useRef(); + const [activeKey, setActiveKey] = useState(match.params.name); + const onSelect = (info) => { + console.log('LayoutWithMenu', schema); + setActiveKey(info.key); + }; + console.log({ match }); + return ( + + + + + + + + {activeKey && } + + + + ); +} + +function Content({ activeKey }) { + const { data = {}, loading } = useRequest( + `ui_schemas:getTree/${activeKey}?filter[parentId]=${activeKey}`, + { + refreshDeps: [activeKey], + formatResult: (result) => result?.data, + }, + ); + + if (loading) { + return ; + } + + return ; +} + +export function AdminLayout({ route }: any) { + const { data = {}, loading } = useRequest( + `ui_schemas:getTree/${route.uiSchemaKey}`, + { + refreshDeps: [route], + formatResult: (result) => result?.data, + }, + ); + + if (loading) { + return ; + } + + return ; +} + +export default AdminLayout; diff --git a/packages/client/src/components/admin-layout/style.less b/packages/client/src/components/admin-layout/style.less new file mode 100644 index 0000000000..0134008839 --- /dev/null +++ b/packages/client/src/components/admin-layout/style.less @@ -0,0 +1,49 @@ +// .fields-collapse { +// border: 1px solid #f0f0f0; +// border-bottom: 0; +// > .ant-collapse-item { +// border-bottom: 1px solid #f0f0f0; +// } +// .ant-collapse-content { +// &.ant-collapse-content-active { +// border-top: 1px solid #f0f0f0; +// } +// } +// > .ant-collapse-item > .ant-collapse-content { +// background: #fff; +// } +// > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box { +// padding: 16px; +// padding-bottom: 1px; +// } +// } + +.ant-collapse { + border: 1px solid #d9d9d9 !important; + border-bottom: 0 !important; + > .ant-collapse-item { + border-bottom: 1px solid #d9d9d9 !important; + } + .ant-collapse-content { + border-top: 1px solid #d9d9d9 !important; + } + .ant-collapse-content > .ant-collapse-content-box { + padding: 24px !important; + padding-bottom: 0 !important; + } +} + +.database-sider { + background: #fafafa; + padding-top: 16px; + .ant-menu { + background: #fafafa; + border-right: 0; + } + .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected { + background: #fff; + &::after { + display: none; + } + } +} diff --git a/packages/client/src/components/auth-layout/index.md b/packages/client/src/components/auth-layout/index.md new file mode 100644 index 0000000000..7cc91617fe --- /dev/null +++ b/packages/client/src/components/auth-layout/index.md @@ -0,0 +1,14 @@ +--- +title: AuthLayout - 登录布局 +nav: + title: 组件 + path: /client +group: + order: 2 + title: Templates + path: /client/templates +--- + +# AuthLayout - 登录布局 + +内置的登录、注册页布局 \ No newline at end of file diff --git a/packages/client/src/components/auth-layout/index.tsx b/packages/client/src/components/auth-layout/index.tsx new file mode 100644 index 0000000000..578364a91d --- /dev/null +++ b/packages/client/src/components/auth-layout/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { useLocation, useHistory } from 'react-router-dom'; + +export function AuthLayout({ children, route }: any) { + const location = useLocation(); + const history = useHistory(); + return ( +
+

NocoBase

+ {children} +
+ ); +} + +export default AuthLayout; diff --git a/packages/client/src/components/route-schema-renderer/index.tsx b/packages/client/src/components/route-schema-renderer/index.tsx new file mode 100644 index 0000000000..9b40888e12 --- /dev/null +++ b/packages/client/src/components/route-schema-renderer/index.tsx @@ -0,0 +1,25 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { Spin } from 'antd'; +import { Helmet } from 'react-helmet'; +import { useRequest } from 'ahooks'; +import { SchemaRenderer } from '../../schemas'; + +export function RouteSchemaRenderer({ route }) { + const { data = {}, loading } = useRequest( + `ui_schemas:getTree/${route.uiSchemaKey}`, + { + refreshDeps: [route], + formatResult: (result) => result?.data, + }, + ); + if (loading) { + return ; + } + return ( +
+ +
+ ); +} + +export default RouteSchemaRenderer; diff --git a/packages/client/src/components/router-config/demos/demo1.tsx b/packages/client/src/components/router-config/demos/demo1.tsx new file mode 100644 index 0000000000..b50a55b773 --- /dev/null +++ b/packages/client/src/components/router-config/demos/demo1.tsx @@ -0,0 +1,49 @@ +import React, { useMemo } from 'react'; +import { + Link, + useLocation, + useRouteMatch, + MemoryRouter as Router, +} from 'react-router-dom'; +import { createRouteSwitch, RouteRedirectProps } from '..'; + +const RouteSwitch = createRouteSwitch({ + components: { + Home: (props) => { + console.log({ props }); + return
Home {props.children}
+ }, + Login: () =>
Login
, + }, +}); + +export default () => { + const routes: Array = [ + { + type: 'route', + path: '/login', + exact: true, + component: 'Login', + }, + { + type: 'route', + path: '/home', + component: 'Home', + routes: [ + { + type: 'route', + path: '/home/123', + exact: true, + component: 'Login', + }, + ], + }, + ]; + return ( +
+ + + +
+ ); +}; diff --git a/packages/client/src/components/router-config/demos/demo2.tsx b/packages/client/src/components/router-config/demos/demo2.tsx new file mode 100644 index 0000000000..8d79e1f137 --- /dev/null +++ b/packages/client/src/components/router-config/demos/demo2.tsx @@ -0,0 +1,65 @@ +import React, { useMemo } from 'react'; +import { + Link, + useLocation, + useRouteMatch, + MemoryRouter as Router, +} from 'react-router-dom'; +import { createRouteSwitch, RouteRedirectProps } from '..'; +import { AdminLayout } from '../../admin-layout'; +import { AuthLayout } from '../../auth-layout'; +import { RouteSchemaRenderer } from '../../route-schema-renderer'; + +const RouteSwitch = createRouteSwitch({ + components: { + AdminLayout, + AuthLayout, + RouteSchemaRenderer, + }, +}); + +const routes: Array = [ + { + type: 'redirect', + from: '/', + to: '/admin', + exact: true, + }, + { + type: 'route', + path: '/admin/:name(.+)?', + component: 'AdminLayout', + title: `后台`, + uiSchemaKey: 'qqzzjakwkwl', + }, + { + type: 'route', + component: 'AuthLayout', + children: [ + { + type: 'route', + path: '/login', + component: 'RouteSchemaRenderer', + title: `登录`, + uiSchemaKey: 'dtf9j0b8p9u', + }, + { + type: 'route', + path: '/register', + component: 'RouteSchemaRenderer', + title: `注册`, + uiSchemaKey: '46qlxqam3xk', + }, + ], + }, +]; + +export default () => { + return ( +
+ + + +
+ ); +}; diff --git a/packages/client/src/components/router-config/index.md b/packages/client/src/components/router-config/index.md new file mode 100644 index 0000000000..7154b54467 --- /dev/null +++ b/packages/client/src/components/router-config/index.md @@ -0,0 +1,3 @@ + + + diff --git a/packages/client/src/components/router-config/index.tsx b/packages/client/src/components/router-config/index.tsx new file mode 100644 index 0000000000..98830b7606 --- /dev/null +++ b/packages/client/src/components/router-config/index.tsx @@ -0,0 +1,105 @@ +import { get } from 'lodash'; +import React from 'react'; +import { useContext } from 'react'; +import { createContext } from 'react'; +import { Switch, Route, Redirect } from 'react-router'; + +export interface RedirectProps { + type: 'redirect'; + to: any; + path?: string; + exact?: boolean; + strict?: boolean; + push?: boolean; + from?: string; + [key: string]: any; +} + +export interface RouteProps { + type: 'route'; + path?: string | string[]; + exact?: boolean; + strict?: boolean; + sensitive?: boolean; + component?: any; + routes?: RouteProps[], + [key: string]: any; +} + +export type RouteRedirectProps = RedirectProps | RouteProps; + +export interface RouteSwitchOptions { + routes?: RouteRedirectProps[]; + components?: any; +} + +export interface RouteSwitchProps { + routes?: RouteRedirectProps[]; + components?: any; +} + +export const RouteComponentsContext = createContext(null); + +export function useComponent(route: RouteProps) { + const components = useContext(RouteComponentsContext); + if (typeof route.component === 'string') { + const component = get(components, route.component); + return component; + } + return route.component || (() => null); +} + +export function createRouteSwitch(options: RouteSwitchOptions) { + function ComponentRenderer(props) { + const Component = useComponent(props.route); + return ( + + + + ); + } + + function RouteSwitch(props: RouteSwitchProps) { + const { routes = [] } = props; + if (!routes.length) { + return null; + } + return ( + + + {routes.map((route) => { + if (route.type == 'redirect') { + return ( + + ); + } + if (!route.path && Array.isArray(route.routes)) { + route.path = route.routes.map((r) => r.path) as any; + } + return ( + { + return ; + }} + /> + ); + })} + + + ); + } + return RouteSwitch; +} diff --git a/packages/client/src/demos/api/routes-getAccessible.ts b/packages/client/src/demos/api/routes-getAccessible.ts index ac4f51b425..9fd1bb7f7f 100644 --- a/packages/client/src/demos/api/routes-getAccessible.ts +++ b/packages/client/src/demos/api/routes-getAccessible.ts @@ -1,12 +1,4 @@ -import Mock from 'mockjs'; - export default [ - { - type: 'redirect', - from: '/admin', - to: '/admin/item2', - exact: true, - }, { type: 'redirect', from: '/', @@ -14,28 +6,30 @@ export default [ exact: true, }, { + type: 'route', path: '/admin/:name(.+)?', component: 'AdminLayout', - title: `后台 - ${Mock.mock('@string')}`, - schemaName: 'menu', + title: `后台`, + uiSchemaKey: 'qqzzjakwkwl', }, { + type: 'route', component: 'AuthLayout', - routes: [ + children: [ { - name: 'login', + type: 'route', path: '/login', - component: 'DefaultPage', - title: `登录 - ${Mock.mock('@string')}`, - schemaName: 'login', + component: 'RouteSchemaRenderer', + title: `登录`, + uiSchemaKey: 'dtf9j0b8p9u', }, { - name: 'register', + type: 'route', path: '/register', - component: 'DefaultPage', - title: `注册 - ${Mock.mock('@string')}`, - schemaName: 'register', + component: 'RouteSchemaRenderer', + title: `注册`, + uiSchemaKey: '46qlxqam3xk', }, ], }, -] +]; diff --git a/packages/client/src/demos/demo1.tsx b/packages/client/src/demos/demo1.tsx index 0439e58573..71abaa3b4c 100644 --- a/packages/client/src/demos/demo1.tsx +++ b/packages/client/src/demos/demo1.tsx @@ -1,43 +1,51 @@ -import React, { useEffect } from 'react'; +import { useRequest } from 'ahooks'; import { Spin } from 'antd'; +import React, { useMemo } from 'react'; import { - RouteSwitch, - useGlobalAction, - AdminLayout, - AuthLayout, - DefaultPage, -} from '@nocobase/client'; -import { - Link, - useHistory, - useLocation, - useRouteMatch, MemoryRouter as Router, } from 'react-router-dom'; -import { UseRequestProvider } from 'ahooks'; -import { request } from './api'; - -const templates = { +import { + createRouteSwitch, + RouteRedirectProps, AdminLayout, AuthLayout, - DefaultPage, -}; + RouteSchemaRenderer, +} from '../'; +import { UseRequestProvider } from 'ahooks'; +import { extend } from 'umi-request'; + +const request = extend({ + prefix: 'http://localhost:23003/api/', + timeout: 1000, +}); + +const RouteSwitch = createRouteSwitch({ + components: { + AdminLayout, + AuthLayout, + RouteSchemaRenderer, + }, +}); + +const App = () => { + const { data, loading } = useRequest('routes:getAccessible', { + formatResult: (result) => result?.data, + }); -function App() { - const { data, loading } = useGlobalAction('routes:getAccessible'); if (loading) { - return ; + return } + return (
- +
); -} +}; -export default function IndexPage() { +export default () => { return ( { @@ -231,6 +234,9 @@ export function useDesignable(path?: any) { if (!property.name) { property.name = uid(); } + if (target['key']) { + property['parentKey'] = target['key']; + } target.addProperty(property.name, property); // BUG: 空 properties 时,addProperty 无反应。 const tmp = { name: uid() }; @@ -251,6 +257,9 @@ export function useDesignable(path?: any) { if (!property.name) { property.name = uid(); } + if (target['parentKey']) { + property['parentKey'] = target['parentKey']; + } addPropertyAfter(target, property); refresh(); return target.parent.properties[property.name]; @@ -267,6 +276,9 @@ export function useDesignable(path?: any) { if (!property.name) { property.name = uid(); } + if (target['parentKey']) { + property['parentKey'] = target['parentKey']; + } addPropertyBefore(target, property); refresh(); return target.parent.properties[property.name]; diff --git a/packages/client/src/schemas/menu/index.md b/packages/client/src/schemas/menu/index.md index 0f60866266..30bc108d86 100644 --- a/packages/client/src/schemas/menu/index.md +++ b/packages/client/src/schemas/menu/index.md @@ -412,6 +412,68 @@ export default () => { } ``` +### 设计器模式 - 菜单项为空时 + +```tsx +import React, { useRef, useState } from 'react'; +import { SchemaRenderer } from '../'; +import { MenuContainerContext } from './'; +import { Layout } from 'antd'; + +export default () => { + const sideMenuRef = useRef(); + + const [activeKey, setActiveKey] = useState('item3'); + + const onSelect = (info) => { + setActiveKey(info.key); + console.log({ info }) + } + + const schema = { + type: 'object', + properties: { + menu1: { + type: 'void', + 'x-component': 'Menu', + 'x-designable-bar': 'Menu.DesignableBar', + 'x-component-props': { + defaultSelectedKeys: [activeKey], + mode: 'mix', + theme: 'dark', + sideMenuRef: '{{ sideMenuRef }}', + onSelect: '{{ onSelect }}', + }, + }, + }, + } + + return ( +
+ + + + + + + + + {activeKey} + + + +
+ ) +} +``` + ### Menu.Action diff --git a/packages/client/src/schemas/menu/index.tsx b/packages/client/src/schemas/menu/index.tsx index 937620fb41..f5b301c86e 100644 --- a/packages/client/src/schemas/menu/index.tsx +++ b/packages/client/src/schemas/menu/index.tsx @@ -94,22 +94,30 @@ function useDesignableBar() { } export const Menu: MenuType = observer((props: any) => { - const { sideMenuRef, onSelect, mode, defaultSelectedKeys, ...others } = props; + const { + children, + sideMenuRef, + onSelect, + mode, + defaultSelectedKeys, + ...others + } = props; let defaultSelectedKey = defaultSelectedKeys ? defaultSelectedKeys[0] : null; - const schema = useFieldSchema(); - const { schema: designableSchema, refresh } = useDesignable(); + // const schema = useFieldSchema(); + const { schema, schema: designableSchema, refresh } = useDesignable(); const designableBar = schema['x-designable-bar']; const history = useHistory(); const renderSideMenu = (selectedKey) => { - if (!selectedKey) { - return; - } if ((mode as any) !== 'mix') { return; } if (!sideMenuRef || !sideMenuRef.current) { return; } + if (!selectedKey || !schema.properties) { + sideMenuRef.current.style.display = 'none'; + return; + } const subSchema = schema.properties[selectedKey]; if (!subSchema) { sideMenuRef.current.style.display = 'none'; @@ -160,6 +168,7 @@ export const Menu: MenuType = observer((props: any) => { ...newProps, [`${uid()}-add-new`]: { type: 'void', + parentKey: subSchema['key'], 'x-component': 'Menu.AddNew', }, }, @@ -173,6 +182,8 @@ export const Menu: MenuType = observer((props: any) => { console.log({ defaultSelectedKey }, schema.properties); renderSideMenu(defaultSelectedKey); }); + const isEmpty = !Object.keys(designableSchema.properties || {}).length; + console.log({ designableSchema }) return ( { onSelect && onSelect(info); }} mode={(mode as any) === 'mix' ? 'horizontal' : mode} - /> + > + {!isEmpty ? ( + + ) : ( + { + Object.keys(subSchema.properties).forEach((name) => { + if (name === 'add-new') { + return; + } + designableSchema.addProperty( + name, + subSchema.properties[name].toJSON(), + ); + }); + refresh(); + }} + schema={{ + type: 'object', + properties: { + 'add-new': { + parentKey: schema['key'], + type: 'void', + 'x-component': 'Menu.AddNew', + }, + }, + }} + /> + )} + ); }); const AddNewAction = () => { - const { insertBefore } = useDesignable(); + const fieldSchema = useFieldSchema(); + const { schema, insertBefore } = useDesignable(); + console.log('AddNewAction', schema, fieldSchema); return ( { { - insertBefore({ + const s = insertBefore({ type: 'void', title: uid(), + key: uid(), 'x-component': 'Menu.Item', }); + console.log('s.s.s.s.s.s', s) }} style={{ minWidth: 150 }} > @@ -375,7 +419,7 @@ Menu.DesignableBar = (props) => { const field = useField(); const fieldSchema = useFieldSchema(); const [visible, setVisible] = useState(false); - const { schema, remove, refresh } = useDesignable(); + const { schema, remove, refresh, insertAfter } = useDesignable(); return (
{ > 修改标题 + { + const s = insertAfter({ + type: 'void', + key: uid(), + title: uid(), + 'x-component': 'Menu.SubMenu', + }); + }}>插入 { Modal.confirm({ diff --git a/packages/plugin-routes/src/models/route.ts b/packages/plugin-routes/src/models/route.ts deleted file mode 100644 index 443913eb71..0000000000 --- a/packages/plugin-routes/src/models/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -import _ from 'lodash'; -import { Model } from '@nocobase/database'; - -export class Route extends Model { - -} diff --git a/packages/plugin-routes/.npmignore b/packages/plugin-ui-router/.npmignore similarity index 100% rename from packages/plugin-routes/.npmignore rename to packages/plugin-ui-router/.npmignore diff --git a/packages/plugin-routes/package.json b/packages/plugin-ui-router/package.json similarity index 78% rename from packages/plugin-routes/package.json rename to packages/plugin-ui-router/package.json index 6fc081878c..0813e6cf55 100644 --- a/packages/plugin-routes/package.json +++ b/packages/plugin-ui-router/package.json @@ -1,5 +1,5 @@ { - "name": "@nocobase/plugin-routes", + "name": "@nocobase/plugin-ui-router", "version": "0.4.0-alpha.7", "main": "lib/index.js", "license": "MIT", @@ -7,7 +7,8 @@ "@nocobase/database": "^0.4.0-alpha.7", "@nocobase/resourcer": "^0.4.0-alpha.7", "@nocobase/server": "^0.4.0-alpha.7", - "deepmerge": "^4.2.2" + "deepmerge": "^4.2.2", + "flat-to-nested": "^1.1.1" }, "devDependencies": { "@nocobase/actions": "^0.4.0-alpha.7" diff --git a/packages/plugin-routes/src/__tests__/index.ts b/packages/plugin-ui-router/src/__tests__/index.ts similarity index 100% rename from packages/plugin-routes/src/__tests__/index.ts rename to packages/plugin-ui-router/src/__tests__/index.ts diff --git a/packages/plugin-routes/src/__tests__/routes.test.ts b/packages/plugin-ui-router/src/__tests__/routes.test.ts similarity index 78% rename from packages/plugin-routes/src/__tests__/routes.test.ts rename to packages/plugin-ui-router/src/__tests__/routes.test.ts index 6b46d3fec0..789f026643 100644 --- a/packages/plugin-routes/src/__tests__/routes.test.ts +++ b/packages/plugin-ui-router/src/__tests__/routes.test.ts @@ -15,20 +15,20 @@ describe('routes', () => { afterEach(() => app.database.close()); - it.only('create route', async () => { - const Route = db.getModel('routes'); - const item = { - path: '/admin/:name(.+)?', - component: 'AdminLayout', - title: `后台`, - uiSchema: { - name: 'menu', - }, - }; - console.log(Route.associations); - const route = await Route.create(item); - await route.updateAssociations(item); - }); + // it.only('create route', async () => { + // const Route = db.getModel('routes'); + // const item = { + // path: '/admin/:name(.+)?', + // component: 'AdminLayout', + // title: `后台`, + // uiSchema: { + // name: 'menu', + // }, + // }; + // console.log(Route.associations); + // const route = await Route.create(item); + // await route.updateAssociations(item); + // }); it('create route', async () => { const Route = db.getModel('routes'); diff --git a/packages/plugin-ui-router/src/actions/getAccessible.ts b/packages/plugin-ui-router/src/actions/getAccessible.ts new file mode 100644 index 0000000000..49d9201ab8 --- /dev/null +++ b/packages/plugin-ui-router/src/actions/getAccessible.ts @@ -0,0 +1,18 @@ +import { Model, ModelCtor } from '@nocobase/database'; +import { actions } from '@nocobase/actions'; +import FlatToNested from 'flat-to-nested'; + +const flatToNested = new FlatToNested({ + id: 'key', + parent: 'parentKey', + children: 'routes', +}); + +export default async (ctx: actions.Context, next: actions.Next) => { + const { resourceKey } = ctx.action.params; + const Route = ctx.db.getModel('routes'); + const routes = await Route.findAll(); + const data = flatToNested.convert(routes.map(route => route.toProps())); + ctx.body = data.routes; + await next(); +} diff --git a/packages/plugin-routes/src/collections/routes.ts b/packages/plugin-ui-router/src/collections/routes.ts similarity index 97% rename from packages/plugin-routes/src/collections/routes.ts rename to packages/plugin-ui-router/src/collections/routes.ts index 66ade6d26c..3d01f95298 100644 --- a/packages/plugin-routes/src/collections/routes.ts +++ b/packages/plugin-ui-router/src/collections/routes.ts @@ -3,6 +3,7 @@ import { TableOptions } from '@nocobase/database'; export default { name: 'routes', title: '路由表', + model: 'Route', fields: [ { type: 'uid', diff --git a/packages/plugin-routes/src/models/index.ts b/packages/plugin-ui-router/src/models/index.ts similarity index 100% rename from packages/plugin-routes/src/models/index.ts rename to packages/plugin-ui-router/src/models/index.ts diff --git a/packages/plugin-ui-router/src/models/route.ts b/packages/plugin-ui-router/src/models/route.ts new file mode 100644 index 0000000000..cd8d72102f --- /dev/null +++ b/packages/plugin-ui-router/src/models/route.ts @@ -0,0 +1,33 @@ +import _ from 'lodash'; +import { Model } from '@nocobase/database'; + +export class Route extends Model { + static async create(value?: any, options?: any): Promise { + // console.log({ value }); + const attributes = this.toAttributes(value); + // @ts-ignore + const model: Model = await super.create(attributes, options); + return model; + } + + static toAttributes(value = {}): any { + const data = _.cloneDeep(value); + const keys = [ + ...Object.keys(this.rawAttributes), + ...Object.keys(this.associations), + ]; + const attrs = _.pick(data, keys); + const options = _.omit(data, keys); + return { ...attrs, options }; + } + + toProps() { + const json = this.toJSON(); + const data: any = _.omit(json, ['options', 'created_at', 'updated_at', 'ui_schema_key']); + const options = json['options'] || {}; + if (json['ui_schema_key']) { + data.uiSchemaKey = json['ui_schema_key']; + } + return { ...data, ...options } + } +} diff --git a/packages/plugin-routes/src/server.ts b/packages/plugin-ui-router/src/server.ts similarity index 73% rename from packages/plugin-routes/src/server.ts rename to packages/plugin-ui-router/src/server.ts index 0a1bdeee64..2c9a3496f2 100644 --- a/packages/plugin-routes/src/server.ts +++ b/packages/plugin-ui-router/src/server.ts @@ -2,6 +2,7 @@ import path from 'path'; import { Application } from '@nocobase/server'; import { registerModels } from '@nocobase/database'; import * as models from './models'; +import getAccessible from './actions/getAccessible'; export default async function (this: Application, options = {}) { const database = this.database; @@ -10,4 +11,6 @@ export default async function (this: Application, options = {}) { database.import({ directory: path.resolve(__dirname, 'collections'), }); + + this.resourcer.registerActionHandler('routes:getAccessible', getAccessible); } diff --git a/packages/plugin-routes/src/utils.ts b/packages/plugin-ui-router/src/utils.ts similarity index 100% rename from packages/plugin-routes/src/utils.ts rename to packages/plugin-ui-router/src/utils.ts diff --git a/packages/plugin-ui-schema/src/actions/getTree.ts b/packages/plugin-ui-schema/src/actions/getTree.ts new file mode 100644 index 0000000000..92683baaf5 --- /dev/null +++ b/packages/plugin-ui-schema/src/actions/getTree.ts @@ -0,0 +1,15 @@ +import { Model, ModelCtor } from '@nocobase/database'; +import { actions } from '@nocobase/actions'; + +export default async (ctx: actions.Context, next: actions.Next) => { + const { resourceKey } = ctx.action.params; + const UISchema = ctx.db.getModel('ui_schemas'); + const schema = await UISchema.findByPk(resourceKey); + const property = schema.toProperty(); + const properties = await schema.getProperties(); + if (Object.keys(properties).length) { + property.properties = properties; + } + ctx.body = property; + await next(); +} diff --git a/packages/plugin-ui-schema/src/server.ts b/packages/plugin-ui-schema/src/server.ts index 0146073ccf..8318f9ff43 100644 --- a/packages/plugin-ui-schema/src/server.ts +++ b/packages/plugin-ui-schema/src/server.ts @@ -2,6 +2,7 @@ import path from 'path'; import { Application } from '@nocobase/server'; import { registerModels, Table } from '@nocobase/database'; import * as models from './models'; +import getTree from './actions/getTree'; export default async function (this: Application, options = {}) { const database = this.database; @@ -11,9 +12,11 @@ export default async function (this: Application, options = {}) { directory: path.resolve(__dirname, 'collections'), }); - database.getModel('ui_schemas').beforeCreate((model) => { - if (!model.get('name')) { - model.set('name', model.get('key')); - } - }); + // database.getModel('ui_schemas').beforeCreate((model) => { + // if (!model.get('name')) { + // model.set('name', model.get('key')); + // } + // }); + + this.resourcer.registerActionHandler('ui_schemas:getTree', getTree); } diff --git a/packages/server/src/middleware.ts b/packages/server/src/middleware.ts index 038722fa4c..d088b81f94 100644 --- a/packages/server/src/middleware.ts +++ b/packages/server/src/middleware.ts @@ -1,5 +1,5 @@ import compose from 'koa-compose'; -import { pathToRegexp } from 'path-to-regexp'; +import pathToRegexp from 'path-to-regexp'; import Resourcer, { getNameByParams, KoaMiddlewareOptions, parseRequest, parseQuery, ResourcerContext, ResourceType } from '@nocobase/resourcer'; import Database, { BELONGSTO, BELONGSTOMANY, HASMANY, HASONE } from '@nocobase/database'; diff --git a/packages/server/src/middlewares/db-resource-router.ts b/packages/server/src/middlewares/db-resource-router.ts index 1256998f4e..7628940762 100644 --- a/packages/server/src/middlewares/db-resource-router.ts +++ b/packages/server/src/middlewares/db-resource-router.ts @@ -1,5 +1,5 @@ import compose from 'koa-compose'; -import { pathToRegexp } from 'path-to-regexp'; +import pathToRegexp from 'path-to-regexp'; import Resourcer, { getNameByParams, KoaMiddlewareOptions, parseRequest, parseQuery, ResourcerContext, ResourceType } from '@nocobase/resourcer'; import Database, { BELONGSTO, BELONGSTOMANY, HASMANY, HASONE } from '@nocobase/database'; diff --git a/yarn.lock b/yarn.lock index a8a79366d8..97ca74b588 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5278,6 +5278,20 @@ boxen@^3.0.0: type-fest "^0.3.0" widest-line "^2.0.0" +boxen@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" + integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^5.3.1" + chalk "^3.0.0" + cli-boxes "^2.2.0" + string-width "^4.1.0" + term-size "^2.1.0" + type-fest "^0.8.1" + widest-line "^3.1.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -5702,6 +5716,14 @@ chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^4.0.0, chalk@^4.1.0: version "4.1.1" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" @@ -5803,7 +5825,7 @@ chokidar@^2.0.4: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.0.2: +chokidar@^3.0.2, chokidar@^3.2.2: version "3.5.2" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== @@ -6187,6 +6209,18 @@ configstore@^4.0.0: write-file-atomic "^2.0.0" xdg-basedir "^3.0.0" +configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + connected-react-router@6.5.2: version "6.5.2" resolved "https://registry.npmjs.org/connected-react-router/-/connected-react-router-6.5.2.tgz#422af70f86cb276681e20ab4295cf27dd9b6c7e3" @@ -6534,6 +6568,11 @@ crypto-random-string@^1.0.0: resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + crypto-random-string@^3.3.0, crypto-random-string@^3.3.1: version "3.3.1" resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.3.1.tgz#13cee94cac8001e4842501608ef779e0ed08f82d" @@ -6878,7 +6917,7 @@ debug@3.1.0, debug@~3.1.0: dependencies: ms "2.0.0" -debug@3.X, debug@^3.1.0, debug@^3.2.7: +debug@3.X, debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -7679,6 +7718,11 @@ escalade@^3.1.1: resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + escape-html@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -8370,6 +8414,11 @@ flat-cache@^3.0.4: flatted "^3.1.0" rimraf "^3.0.2" +flat-to-nested@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/flat-to-nested/-/flat-to-nested-1.1.1.tgz#ec183cd9a72f6bfbf8ca21acb0fc8235e509f581" + integrity sha512-Sym5oik6BO9JnsDEjv9Q9hPTCexG2ttk0UiM2mgLEiCiiUOQr8acBd33r8ixnoSGR0HAxPoP8WtLAL5oV46IhQ== + flatted@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" @@ -8811,6 +8860,13 @@ global-dirs@^0.1.0: dependencies: ini "^1.3.4" +global-dirs@^2.0.1: + version "2.1.0" + resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d" + integrity sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ== + dependencies: + ini "1.3.7" + global@^4.3.2: version "4.4.0" resolved "https://registry.npmjs.org/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" @@ -9480,6 +9536,11 @@ iferr@^0.1.5: resolved "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= + ignore-walk@^3.0.1: version "3.0.4" resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" @@ -9647,6 +9708,11 @@ inherits@2.0.3: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +ini@1.3.7: + version "1.3.7" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== + ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -9998,6 +10064,14 @@ is-installed-globally@^0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" +is-installed-globally@^0.3.1: + version "0.3.2" + resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" + integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== + dependencies: + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + is-module@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" @@ -10026,6 +10100,11 @@ is-npm@^3.0.0: resolved "https://registry.npmjs.org/is-npm/-/is-npm-3.0.0.tgz#ec9147bfb629c43f494cf67936a961edec7e8053" integrity sha512-wsigDr1Kkschp2opC4G3yA6r9EgVA6NjRpWzIi9axXqeIaAATPRJc4uLujXe3Nd9uO8KoDyA4MD6aZSeXTADhA== +is-npm@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" + integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== + is-number-object@^1.0.4: version "1.0.5" resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb" @@ -10060,6 +10139,11 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" +is-path-inside@^3.0.1: + version "3.0.3" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -12889,6 +12973,22 @@ node-schedule@^2.0.0: long-timeout "0.1.1" sorted-array-functions "^1.3.0" +nodemon@^2.0.12: + version "2.0.12" + resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.12.tgz#5dae4e162b617b91f1873b3bfea215dd71e144d5" + integrity sha512-egCTmNZdObdBxUBw6ZNwvZ/xzk24CKRs5K6d+5zbmrMr7rOpPmfPeF6OxM3DDpaRx331CQRFEktn+wrFFfBSOA== + dependencies: + chokidar "^3.2.2" + debug "^3.2.6" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.7" + semver "^5.7.1" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.3" + update-notifier "^4.1.0" + nopt@^4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" @@ -12904,6 +13004,13 @@ nopt@^5.0.0: dependencies: abbrev "1" +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -14812,6 +14919,11 @@ psl@^1.1.28, psl@^1.1.33: resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== +pstree.remy@^1.1.7: + version "1.1.8" + resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + public-encrypt@^4.0.0: version "4.0.3" resolved "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" @@ -14864,6 +14976,13 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +pupa@^2.0.1: + version "2.1.1" + resolved "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" + integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== + dependencies: + escape-goat "^2.0.0" + q@^1.1.2, q@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -16771,6 +16890,13 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== + dependencies: + semver "^6.3.0" + "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -17385,7 +17511,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0: +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: version "4.2.2" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== @@ -17626,7 +17752,7 @@ supports-color@^3.2.3: dependencies: has-flag "^1.0.0" -supports-color@^5.3.0, supports-color@^5.4.0: +supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -17755,6 +17881,11 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" +term-size@^2.1.0: + version "2.2.1" + resolved "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" + integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== + terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" @@ -18024,6 +18155,13 @@ toposort-class@^1.0.1: resolved "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" integrity sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg= +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@^2.5.0, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -18408,6 +18546,13 @@ unc-path-regex@^0.1.2: resolved "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= +undefsafe@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" + integrity sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A== + dependencies: + debug "^2.2.0" + underscore@^1.13.1: version "1.13.1" resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" @@ -18503,6 +18648,13 @@ unique-string@^1.0.0: dependencies: crypto-random-string "^1.0.0" +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + unist-builder@^2.0.0: version "2.0.3" resolved "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436" @@ -18619,6 +18771,25 @@ update-notifier@3.0.0: semver-diff "^2.0.0" xdg-basedir "^3.0.0" +update-notifier@^4.1.0: + version "4.1.3" + resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3" + integrity sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A== + dependencies: + boxen "^4.2.0" + chalk "^3.0.0" + configstore "^5.0.1" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.3.1" + is-npm "^4.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.0.0" + pupa "^2.0.1" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" + upper-case@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz#d89810823faab1df1549b7d97a76f8662bae6f7a" @@ -19084,6 +19255,13 @@ widest-line@^2.0.0: dependencies: string-width "^2.1.1" +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + wildcard@^1.1.0: version "1.1.2" resolved "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz#a7020453084d8cd2efe70ba9d3696263de1710a5" @@ -19237,6 +19415,11 @@ xdg-basedir@^3.0.0: resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"