Просмотр исходного кода

Merge branch 'master' into fix/110106-sandbox-math-toc

Shun Miyazawa 3 лет назад
Родитель
Сommit
a5ca340e2f
87 измененных файлов с 674 добавлено и 921 удалено
  1. 2 6
      .github/workflows/ci-app-prod.yml
  2. 3 5
      .github/workflows/ci-app.yml
  3. 5 1
      .github/workflows/reusable-app-prod.yml
  4. 2 6
      .vscode/launch.json
  5. 0 0
      packages-obsolete/plugin-attachment-refs/.eslintignore
  6. 0 0
      packages-obsolete/plugin-attachment-refs/.gitignore
  7. 0 0
      packages-obsolete/plugin-attachment-refs/README.md
  8. 0 0
      packages-obsolete/plugin-attachment-refs/package.json
  9. 0 0
      packages-obsolete/plugin-attachment-refs/src/client-entry.js
  10. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/css/index.css
  11. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/components/AttachmentList.jsx
  12. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/components/ExtractedAttachments.jsx
  13. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/GalleryContext.js
  14. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPostRenderInterceptor.js
  15. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPreRenderInterceptor.js
  16. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/RefsContext.js
  17. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/TagCacheManagerFactory.js
  18. 0 0
      packages-obsolete/plugin-attachment-refs/src/index.js
  19. 0 0
      packages-obsolete/plugin-attachment-refs/src/server-entry.js
  20. 0 0
      packages-obsolete/plugin-attachment-refs/src/server/routes/index.js
  21. 0 0
      packages-obsolete/plugin-attachment-refs/src/server/routes/refs.js
  22. 0 0
      packages-obsolete/plugin-attachment-refs/src/utils/logger/index.ts
  23. 0 0
      packages-obsolete/plugin-attachment-refs/tsconfig.base.json
  24. 0 0
      packages-obsolete/plugin-attachment-refs/tsconfig.build.cjs.json
  25. 0 0
      packages-obsolete/plugin-attachment-refs/tsconfig.build.esm.json
  26. 0 0
      packages-obsolete/plugin-attachment-refs/tsconfig.json
  27. 1 2
      packages/app/docker/Dockerfile
  28. 1 0
      packages/app/next.config.js
  29. 4 3
      packages/app/package.json
  30. 5 3
      packages/app/src/components/InstallerForm.tsx
  31. 0 2
      packages/app/src/components/Layout/BasicLayout.tsx
  32. 0 10
      packages/app/src/components/Layout/NoLoginLayout.module.scss
  33. 9 4
      packages/app/src/components/LoginForm.tsx
  34. 18 0
      packages/app/src/components/ReactMarkdownComponents/Table.tsx
  35. 6 3
      packages/app/src/components/Sidebar/CustomSidebar.tsx
  36. 8 0
      packages/app/src/interfaces/errors/external-account-login-error.ts
  37. 11 0
      packages/app/src/models/vo/external-account-login-error.ts
  38. 2 0
      packages/app/src/pages/[[...path]].page.tsx
  39. 10 0
      packages/app/src/pages/login.page.tsx
  40. 15 1
      packages/app/src/pages/tags.page.tsx
  41. 1 1
      packages/app/src/server/crowi/index.js
  42. 11 11
      packages/app/src/server/routes/index.js
  43. 48 45
      packages/app/src/server/routes/login-passport.js
  44. 20 0
      packages/app/src/services/renderer/remark-plugins/table.ts
  45. 11 5
      packages/app/src/services/renderer/renderer.tsx
  46. 2 1
      packages/app/src/stores/middlewares/sync-to-storage.ts
  47. 1 1
      packages/app/src/styles/organisms/_wiki-custom-sidebar.scss
  48. 7 3
      packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts
  49. 21 20
      packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts
  50. 1 1
      packages/app/tsconfig.build.client.json
  51. 1 1
      packages/app/tsconfig.build.server.json
  52. 1 1
      packages/app/tsconfig.json
  53. 0 1
      packages/core/src/index.ts
  54. 0 69
      packages/core/src/plugin/service/tag-cache-manager.js
  55. 0 125
      packages/core/src/test/plugin/service/tag-cache-manager.test.js
  56. 0 265
      packages/plugin-lsx/src/components/Lsx.tsx
  57. 0 54
      packages/plugin-lsx/src/components/LsxPageList/LsxListView.jsx
  58. 0 48
      packages/plugin-lsx/src/components/PageNode.js
  59. 0 1
      packages/plugin-lsx/src/components/index.ts
  60. 0 21
      packages/plugin-lsx/src/components/tag-cache-manager.ts
  61. 2 2
      packages/remark-drawio-plugin/package.json
  62. 27 12
      packages/remark-growi-plugin/src/micromark-extension-growi-plugin/lib/factory-attributes.js
  63. 69 2
      packages/remark-growi-plugin/test/micromark-extension-growi-plugin.test.js
  64. 0 0
      packages/remark-lsx/.eslintignore
  65. 0 0
      packages/remark-lsx/.eslintrc.js
  66. 0 0
      packages/remark-lsx/.gitignore
  67. 1 1
      packages/remark-lsx/README.md
  68. 3 2
      packages/remark-lsx/package.json
  69. 0 0
      packages/remark-lsx/src/components/Lsx.module.scss
  70. 98 0
      packages/remark-lsx/src/components/Lsx.tsx
  71. 0 0
      packages/remark-lsx/src/components/LsxPageList/LsxListView.module.scss
  72. 58 0
      packages/remark-lsx/src/components/LsxPageList/LsxListView.tsx
  73. 22 8
      packages/remark-lsx/src/components/LsxPageList/LsxPage.tsx
  74. 1 0
      packages/remark-lsx/src/components/index.ts
  75. 0 0
      packages/remark-lsx/src/components/lsx-context.ts
  76. 0 0
      packages/remark-lsx/src/index.ts
  77. 7 0
      packages/remark-lsx/src/interfaces/page-node.ts
  78. 0 0
      packages/remark-lsx/src/server/routes/index.js
  79. 0 0
      packages/remark-lsx/src/server/routes/lsx.js
  80. 0 0
      packages/remark-lsx/src/services/renderer/index.ts
  81. 9 0
      packages/remark-lsx/src/services/renderer/lsx.ts
  82. 144 0
      packages/remark-lsx/src/stores/lsx.tsx
  83. 0 0
      packages/remark-lsx/tsconfig.base.json
  84. 0 0
      packages/remark-lsx/tsconfig.build.cjs.json
  85. 0 0
      packages/remark-lsx/tsconfig.build.esm.json
  86. 0 0
      packages/remark-lsx/tsconfig.json
  87. 6 174
      yarn.lock

+ 2 - 6
.github/workflows/ci-app-prod.yml

@@ -14,11 +14,9 @@ on:
       - '!packages/app/docker/**'
       - packages/codemirror-textlint/**
       - packages/core/**
-      - packages/remark-drawio-plugin/**
-      - packages/remark-growi-plugin/**
+      - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**
-      - packages/plugin-**
   pull_request:
     branches:
       - master
@@ -33,11 +31,9 @@ on:
       - '!packages/app/docker/**'
       - packages/codemirror-textlint/**
       - packages/core/**
-      - packages/remark-drawio-plugin/**
-      - packages/remark-growi-plugin/**
+      - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**
-      - packages/plugin-**
   workflow_call:
     inputs:
       cypress-config-video:

+ 3 - 5
.github/workflows/ci-app.yml

@@ -16,11 +16,9 @@ on:
       - '!packages/app/docker/**'
       - packages/codemirror-textlint/**
       - packages/core/**
-      - packages/remark-drawio-plugin/**
-      - packages/remark-growi-plugin/**
+      - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**
-      - packages/plugin-*/**
 
 jobs:
   lint:
@@ -56,7 +54,7 @@ jobs:
 
       - name: lerna run lint for plugins
         run: |
-          yarn lerna run lint --scope @growi/remark-* --scope @growi/plugin-*
+          yarn lerna run lint --scope @growi/remark-*
       - name: lerna run lint for app
         run: |
           yarn lerna run lint --scope @growi/app --scope @growi/codemirror-textlint --scope @growi/core --scope @growi/slack --scope @growi/ui
@@ -110,7 +108,7 @@ jobs:
 
       - name: lerna run test for plugins
         run: |
-          yarn lerna run test --scope @growi/remark-* --scope @growi/plugin-*
+          yarn lerna run test --scope @growi/remark-*
 
       - name: Test app
         working-directory: ./packages/app

+ 5 - 1
.github/workflows/reusable-app-prod.yml

@@ -235,7 +235,11 @@ jobs:
 
     - name: lerna bootstrap
       run: |
-        npx lerna bootstrap -- --frozen-lockfile
+        npx lerna bootstrap -- --production
+
+    - name: lerna add packages needed for CI
+      run: |
+        npx lerna add yargs
 
     - name: Download production files artifact
       uses: actions/download-artifact@v3

+ 2 - 6
.vscode/launch.json

@@ -73,12 +73,8 @@
             "path": "${workspaceFolder}/packages/core"
           },
           {
-            "url": "webpack://_n_e/plugin-attachment-refs",
-            "path": "${workspaceFolder}/packages/plugin-attachment-refs"
-          },
-          {
-            "url": "webpack://_n_e/plugin-lsx",
-            "path": "${workspaceFolder}/packages/plugin-lsx"
+            "url": "webpack://_n_e/remark-lsx",
+            "path": "${workspaceFolder}/packages/remark-lsx"
           },
           {
             "url": "webpack://_n_e/slack",

+ 0 - 0
packages/plugin-attachment-refs/.eslintignore → packages-obsolete/plugin-attachment-refs/.eslintignore


+ 0 - 0
packages/plugin-attachment-refs/.gitignore → packages-obsolete/plugin-attachment-refs/.gitignore


+ 0 - 0
packages/plugin-attachment-refs/README.md → packages-obsolete/plugin-attachment-refs/README.md


+ 0 - 0
packages/plugin-attachment-refs/package.json → packages-obsolete/plugin-attachment-refs/package.json


+ 0 - 0
packages/plugin-attachment-refs/src/client-entry.js → packages-obsolete/plugin-attachment-refs/src/client-entry.js


+ 0 - 0
packages/plugin-attachment-refs/src/client/css/index.css → packages-obsolete/plugin-attachment-refs/src/client/css/index.css


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/components/AttachmentList.jsx → packages-obsolete/plugin-attachment-refs/src/client/js/components/AttachmentList.jsx


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/components/ExtractedAttachments.jsx → packages-obsolete/plugin-attachment-refs/src/client/js/components/ExtractedAttachments.jsx


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/util/GalleryContext.js → packages-obsolete/plugin-attachment-refs/src/client/js/util/GalleryContext.js


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPostRenderInterceptor.js → packages-obsolete/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPostRenderInterceptor.js


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPreRenderInterceptor.js → packages-obsolete/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPreRenderInterceptor.js


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/util/RefsContext.js → packages-obsolete/plugin-attachment-refs/src/client/js/util/RefsContext.js


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/util/TagCacheManagerFactory.js → packages-obsolete/plugin-attachment-refs/src/client/js/util/TagCacheManagerFactory.js


+ 0 - 0
packages/plugin-attachment-refs/src/index.js → packages-obsolete/plugin-attachment-refs/src/index.js


+ 0 - 0
packages/plugin-attachment-refs/src/server-entry.js → packages-obsolete/plugin-attachment-refs/src/server-entry.js


+ 0 - 0
packages/plugin-attachment-refs/src/server/routes/index.js → packages-obsolete/plugin-attachment-refs/src/server/routes/index.js


+ 0 - 0
packages/plugin-attachment-refs/src/server/routes/refs.js → packages-obsolete/plugin-attachment-refs/src/server/routes/refs.js


+ 0 - 0
packages/plugin-attachment-refs/src/utils/logger/index.ts → packages-obsolete/plugin-attachment-refs/src/utils/logger/index.ts


+ 0 - 0
packages/plugin-attachment-refs/tsconfig.base.json → packages-obsolete/plugin-attachment-refs/tsconfig.base.json


+ 0 - 0
packages/plugin-attachment-refs/tsconfig.build.cjs.json → packages-obsolete/plugin-attachment-refs/tsconfig.build.cjs.json


+ 0 - 0
packages/plugin-attachment-refs/tsconfig.build.esm.json → packages-obsolete/plugin-attachment-refs/tsconfig.build.esm.json


+ 0 - 0
packages/plugin-attachment-refs/tsconfig.json → packages-obsolete/plugin-attachment-refs/tsconfig.json


+ 1 - 2
packages/app/docker/Dockerfile

@@ -105,12 +105,11 @@ COPY ["package.json", "lerna.json", "tsconfig.base.json", "./"]
 COPY packages/app packages/app
 COPY packages/core packages/core
 COPY packages/codemirror-textlint packages/codemirror-textlint
-COPY packages/plugin-attachment-refs packages/plugin-attachment-refs
-COPY packages/plugin-lsx packages/plugin-lsx
 COPY packages/slack packages/slack
 COPY packages/ui packages/ui
 COPY packages/remark-drawio-plugin packages/remark-drawio-plugin
 COPY packages/remark-growi-plugin packages/remark-growi-plugin
+COPY packages/remark-lsx packages/remark-lsx
 COPY packages/hackmd packages/hackmd
 
 # build

+ 1 - 0
packages/app/next.config.js

@@ -25,6 +25,7 @@ const setupTranspileModules = () => {
     // listing ESM packages until experimental.esmExternals works correctly to avoid ERR_REQUIRE_ESM
     'react-markdown',
     'unified',
+    'markdown-table',
     'character-entities-html4',
     'comma-separated-tokens',
     'decode-named-character-reference',

+ 4 - 3
packages/app/package.json

@@ -68,8 +68,9 @@
     "@growi/codemirror-textlint": "^6.0.0-RC.9",
     "@growi/core": "^6.0.0-RC.9",
     "@growi/hackmd": "^6.0.0-RC.9",
-    "@growi/plugin-attachment-refs": "^6.0.0-RC.9",
-    "@growi/plugin-lsx": "^6.0.0-RC.9",
+    "@growi/remark-drawio-plugin": "^6.0.0-RC.9",
+    "@growi/remark-growi-plugin": "^6.0.0-RC.9",
+    "@growi/remark-lsx": "^6.0.0-RC.9",
     "@growi/slack": "^6.0.0-RC.9",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
@@ -122,6 +123,7 @@
     "is-absolute-url": "^4.0.1",
     "is-iso-date": "^0.0.1",
     "lucene-query-parser": "^1.2.0",
+    "markdown-table": "^1.1.1",
     "md5": "^2.2.1",
     "method-override": "^3.0.0",
     "migrate-mongo": "^8.2.3",
@@ -236,7 +238,6 @@
     "jquery.cookie": "~1.4.1",
     "jshint": "^2.13.0",
     "load-css-file": "^1.0.0",
-    "markdown-table": "^1.1.1",
     "material-icons": "^1.11.3",
     "morgan": "^1.10.0",
     "next-transpile-modules": "^9.0.0",

+ 5 - 3
packages/app/src/components/InstallerForm.tsx

@@ -106,11 +106,13 @@ const InstallerForm = memo((): JSX.Element => {
       <div className="row">
         <form role="form" id="register-form" className="col-md-12" onSubmit={submitHandler}>
           <div className="dropdown mb-3">
-            <div className="d-flex dropdown-with-icon">
-              <i className="icon-bubbles border-0 rounded-0" />
+            <div className="input-group">
+              <div className="input-group-prepend dropdown-with-icon">
+                <i className="input-group-text icon-bubbles border-0 rounded-0" />
+              </div>
               <button
                 type="button"
-                className="btn btn-secondary dropdown-toggle text-right w-100 border-0 shadow-none"
+                className="btn btn-secondary dropdown-toggle form-control text-right rounded-right"
                 id="dropdownLanguage"
                 data-testid="dropdownLanguage"
                 data-toggle="dropdown"

+ 0 - 2
packages/app/src/components/Layout/BasicLayout.tsx

@@ -22,7 +22,6 @@ const PageDeleteModal = dynamic(() => import('../PageDeleteModal'), { ssr: false
 const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
 const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
 const PageAccessoriesModal = dynamic(() => import('../PageAccessoriesModal'), { ssr: false });
-const DrawioModal = dynamic(() => import('../PageEditor/DrawioModal').then(mod => mod.DrawioModal), { ssr: false });
 // Fab
 const Fab = dynamic(() => import('../Fab').then(mod => mod.Fab), { ssr: false });
 
@@ -64,7 +63,6 @@ export const BasicLayout = ({
         <PageDeleteModal />
         <PageRenameModal />
         <PageAccessoriesModal />
-        <DrawioModal />
       </DndProvider>
 
       <PagePresentationModal />

+ 0 - 10
packages/app/src/components/Layout/NoLoginLayout.module.scss

@@ -63,16 +63,6 @@
     }
   }
 
-  .dropdown-with-icon {
-    .dropdown-toggle {
-      @extend .form-control;
-    }
-    i {
-      @extend .input-group-text;
-      margin-right: -1px;
-    }
-  }
-
   .input-group {
     margin-bottom: 10px;
 

+ 9 - 4
packages/app/src/components/LoginForm.tsx

@@ -7,8 +7,9 @@ import { useRouter } from 'next/router';
 import ReactCardFlip from 'react-card-flip';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
+import type { IExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
-import { IErrorV3 } from '~/interfaces/errors/v3-error';
+import type { IErrorV3 } from '~/interfaces/errors/v3-error';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { toArrayIfNot } from '~/utils/array-utils';
 
@@ -26,7 +27,8 @@ type LoginFormProps = {
   isLdapStrategySetup: boolean,
   isLdapSetupFailed: boolean,
   objOfIsExternalAuthEnableds?: any,
-  isMailerSetup?: boolean
+  isMailerSetup?: boolean,
+  externalAccountLoginError?: IExternalAccountLoginError,
 }
 export const LoginForm = (props: LoginFormProps): JSX.Element => {
   const { t } = useTranslation();
@@ -129,7 +131,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   }, [t]);
 
   // wrap error elements which do not use dangerouslySetInnerHtml
-  const generateSafelySetErrors = useCallback((errors: IErrorV3[]): JSX.Element => {
+  const generateSafelySetErrors = useCallback((errors: (IErrorV3 | IExternalAccountLoginError)[]): JSX.Element => {
     if (errors == null || errors.length === 0) return <></>;
     return (
       <ul className="alert alert-danger">
@@ -151,7 +153,10 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     // Generate login error elements using dangerouslySetInnerHTML
     const loginErrorElementWithDangerouslySetInnerHTML = generateDangerouslySetErrors(loginErrorListForDangerouslySetInnerHTML);
     // Generate login error elements using <ul>, <li>
-    const loginErrorElement = generateSafelySetErrors(loginErrorList);
+
+    const loginErrorElement = props.externalAccountLoginError != null
+      ? generateSafelySetErrors([...loginErrorList, props.externalAccountLoginError])
+      : generateSafelySetErrors(loginErrorList);
 
     return (
       <>

+ 18 - 0
packages/app/src/components/ReactMarkdownComponents/Table.tsx

@@ -0,0 +1,18 @@
+import React from 'react';
+
+type TableProps = {
+  children: React.ReactNode,
+  className?: string
+}
+
+export const Table = React.memo((props: TableProps): JSX.Element => {
+
+  const { children, className } = props;
+
+  return (
+    <table className={`${className}`}>
+      {children}
+    </table>
+  );
+});
+Table.displayName = 'Table';

+ 6 - 3
packages/app/src/components/Sidebar/CustomSidebar.tsx

@@ -1,6 +1,7 @@
 import React, { FC } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
 
 import { IRevision } from '~/interfaces/revision';
 import { useSWRxPageByPath } from '~/stores/page';
@@ -19,9 +20,11 @@ const logger = loggerFactory('growi:cli:CustomSidebar');
 const SidebarNotFound = () => {
   return (
     <div className="grw-sidebar-content-header h5 text-center p-3">
-      <a href="/Sidebar#edit">
-        <i className="icon-magic-wand"></i> Create <strong>/Sidebar</strong> page
-      </a>
+      <Link href="/Sidebar#edit">
+        <a href="/Sidebar#edit">
+          <i className="icon-magic-wand"></i> Create <strong>/Sidebar</strong> page
+        </a>
+      </Link>
     </div>
   );
 };

+ 8 - 0
packages/app/src/interfaces/errors/external-account-login-error.ts

@@ -0,0 +1,8 @@
+import { ExternalAccountLoginError } from '~/models/vo/external-account-login-error';
+
+export type IExternalAccountLoginError = ExternalAccountLoginError;
+
+// type guard
+export const isExternalAccountLoginError = (args: any): args is IExternalAccountLoginError => {
+  return (args as IExternalAccountLoginError).message != null;
+};

+ 11 - 0
packages/app/src/models/vo/external-account-login-error.ts

@@ -0,0 +1,11 @@
+export class ExternalAccountLoginError extends Error {
+
+  args?: any;
+
+  constructor(message = '', args = undefined) {
+    super();
+    this.message = message;
+    this.args = args;
+  }
+
+}

+ 2 - 0
packages/app/src/pages/[[...path]].page.tsx

@@ -86,6 +86,7 @@ const UnsavedAlertDialog = dynamic(() => import('../components/UnsavedAlertDialo
 const GrowiSubNavigationSwitcher = dynamic(() => import('../components/Navbar/GrowiSubNavigationSwitcher'), { ssr: false });
 const UsersHomePageFooter = dynamic<UsersHomePageFooterProps>(() => import('../components/UsersHomePageFooter')
   .then(mod => mod.UsersHomePageFooter), { ssr: false });
+const DrawioModal = dynamic(() => import('../components/PageEditor/DrawioModal').then(mod => mod.DrawioModal), { ssr: false });
 const HandsontableModal = dynamic(() => import('../components/PageEditor/HandsontableModal').then(mod => mod.HandsontableModal), { ssr: false });
 
 const logger = loggerFactory('growi:pages:all');
@@ -361,6 +362,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
           <UnsavedAlertDialog />
           <DescendantsPageListModal />
+          <DrawioModal />
           <HandsontableModal />
           {shouldRenderPutbackPageModal && <PutbackPageModal />}
         </div>

+ 10 - 0
packages/app/src/pages/login.page.tsx

@@ -8,6 +8,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import { LoginForm } from '~/components/LoginForm';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
+import { IExternalAccountLoginError, isExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
 import type { RegistrationMode } from '~/interfaces/registration-mode';
 
 import {
@@ -30,6 +31,7 @@ type Props = CommonProps & {
   isLdapSetupFailed: boolean,
   isPasswordResetEnabled: boolean,
   isEmailAuthenticationEnabled: boolean,
+  externalAccountLoginError?: IExternalAccountLoginError,
 };
 
 const LoginPage: NextPage<Props> = (props: Props) => {
@@ -54,6 +56,7 @@ const LoginPage: NextPage<Props> = (props: Props) => {
         isPasswordResetEnabled={props.isPasswordResetEnabled}
         isMailerSetup={props.isMailerSetup}
         registrationMode={props.registrationMode}
+        externalAccountLoginError={props.externalAccountLoginError}
       />
     </NoLoginLayout>
   );
@@ -120,6 +123,13 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
   const props: Props = result.props as Props;
 
+  if (context.query.externalAccountLoginError != null) {
+    const externalAccountLoginError = context.query.externalAccountLoginError;
+    if (isExternalAccountLoginError(externalAccountLoginError)) {
+      props.externalAccountLoginError = { ...externalAccountLoginError as IExternalAccountLoginError };
+    }
+  }
+
   injectServerConfigurations(context, props);
   injectEnabledStrategies(context, props);
   await injectNextI18NextConfigurations(context, props, ['translation']);

+ 15 - 1
packages/app/src/pages/tags.page.tsx

@@ -3,6 +3,7 @@ import React, { useState, useCallback } from 'react';
 import type { IUser, IUserHasId } from '@growi/core';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 
@@ -24,7 +25,7 @@ import {
 } from '../stores/context';
 
 import {
-  CommonProps, getServerSideCommonProps, useCustomTitle,
+  CommonProps, getServerSideCommonProps, getNextI18NextConfig, useCustomTitle,
 } from './utils/commons';
 
 const PAGING_LIMIT = 10;
@@ -139,6 +140,17 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   };
 }
 
+/**
+ * for Server Side Translations
+ * @param context
+ * @param props
+ * @param namespacesRequired
+ */
+async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  props._nextI18Next = nextI18NextConfig._nextI18Next;
+}
+
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const { user } = req;
@@ -152,8 +164,10 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   if (user != null) {
     props.currentUser = user.toObject();
   }
+
   await injectUserUISettings(context, props);
   injectServerConfigurations(context, props);
+  await injectNextI18NextConfigurations(context, props, ['translation']);
 
   return {
     props,

+ 1 - 1
packages/app/src/server/crowi/index.js

@@ -3,7 +3,7 @@ import http from 'http';
 import path from 'path';
 
 import { createTerminus } from '@godaddy/terminus';
-import lsxRoutes from '@growi/plugin-lsx/server/routes';
+import lsxRoutes from '@growi/remark-lsx/server/routes';
 import mongoose from 'mongoose';
 import next from 'next';
 

+ 11 - 11
packages/app/src/server/routes/index.js

@@ -97,17 +97,17 @@ module.exports = function(crowi, app) {
   }
 
   // OAuth
-  app.get('/passport/google'                      , loginPassport.loginWithGoogle, loginPassport.loginFailure);
-  app.get('/passport/github'                      , loginPassport.loginWithGitHub, loginPassport.loginFailure);
-  app.get('/passport/twitter'                     , loginPassport.loginWithTwitter, loginPassport.loginFailure);
-  app.get('/passport/oidc'                        , loginPassport.loginWithOidc, loginPassport.loginFailure);
-  app.get('/passport/saml'                        , loginPassport.loginWithSaml, loginPassport.loginFailure);
-  app.get('/passport/basic'                       , loginPassport.loginWithBasic, loginPassport.loginFailure);
-  app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback   , loginPassport.loginFailure);
-  app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback   , loginPassport.loginFailure);
-  app.get('/passport/twitter/callback'            , loginPassport.loginPassportTwitterCallback  , loginPassport.loginFailure);
-  app.get('/passport/oidc/callback'               , loginPassport.loginPassportOidcCallback     , loginPassport.loginFailure);
-  app.post('/passport/saml/callback'              , addActivity, loginPassport.loginPassportSamlCallback, loginPassport.loginFailure);
+  app.get('/passport/google'                      , loginPassport.loginWithGoogle, loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/github'                      , loginPassport.loginWithGitHub, loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/twitter'                     , loginPassport.loginWithTwitter, loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/oidc'                        , loginPassport.loginWithOidc, loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/saml'                        , loginPassport.loginWithSaml, loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/basic'                       , loginPassport.loginWithBasic, loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback   , loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback   , loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/twitter/callback'            , loginPassport.loginPassportTwitterCallback  , loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/oidc/callback'               , loginPassport.loginPassportOidcCallback     , loginPassport.loginFailureForExternalAccount);
+  app.post('/passport/saml/callback'              , addActivity, loginPassport.loginPassportSamlCallback, loginPassport.loginFailureForExternalAccount);
 
   app.post('/_api/login/testLdap'    , loginRequiredStrictly , loginFormValidator.loginRules() , loginFormValidator.loginValidation , loginPassport.testLdapCredentials);
 

+ 48 - 45
packages/app/src/server/routes/login-passport.js

@@ -1,8 +1,10 @@
 
 import { ErrorV3 } from '@growi/core';
+import next from 'next';
 
 import { SupportedAction } from '~/interfaces/activity';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
+import { ExternalAccountLoginError } from '~/models/vo/external-account-login-error';
 import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
 import loggerFactory from '~/utils/logger';
 
@@ -131,20 +133,6 @@ module.exports = function(crowi, app) {
     return res.apiv3({ redirectTo });
   };
 
-  /**
-   * failure handler
-   * @param {*} req
-   * @param {*} res
-   */
-  const loginFailureHandler = async(req, res, message) => {
-    req.flash('errorMessage', message || req.t('message.sign_in_failure'));
-
-    const parameters = { action: SupportedAction.ACTION_USER_LOGIN_FAILURE };
-    activityEvent.emit('update', res.locals.activity._id, parameters);
-
-    return res.redirect('/login');
-  };
-
   const cannotLoginErrorHadnler = (req, res, next) => {
     // this is called when all login method is somehow failed without invoking 'return next(<any Error>)'
     const err = new ErrorV3('message.sign_in_failure');
@@ -166,6 +154,20 @@ module.exports = function(crowi, app) {
     return res.apiv3Err(error);
   };
 
+  const loginFailureForExternalAccount = async(error, req, res, next) => {
+    const parameters = {
+      ip:  req.ip,
+      endpoint: req.originalUrl,
+      action: SupportedAction.ACTION_USER_LOGIN_FAILURE,
+    };
+    await crowi.activityService.createActivity(parameters);
+
+    const { nextApp } = crowi;
+    req.crowi = crowi;
+    nextApp.render(req, res, '/login', { externalAccountLoginError: error });
+    return;
+  };
+
   /**
    * return true(valid) or false(invalid)
    *
@@ -359,8 +361,8 @@ module.exports = function(crowi, app) {
   const loginWithGoogle = function(req, res, next) {
     if (!passportService.isGoogleStrategySetup) {
       debug('GoogleStrategy has not been set up');
-      req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'GoogleStrategy' }));
-      return next();
+      const error = new ExternalAccountLoginError('message.strategy_has_not_been_set_up', { strategy: 'GoogleStrategy' });
+      return next(error);
     }
 
     passport.authenticate('google', {
@@ -379,7 +381,7 @@ module.exports = function(crowi, app) {
       response = await promisifiedPassportAuthentication(strategyName, req, res);
     }
     catch (err) {
-      return loginFailureHandler(req, res);
+      return next(new ExternalAccountLoginError(err.message));
     }
 
     let name;
@@ -413,14 +415,14 @@ module.exports = function(crowi, app) {
 
     const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
-      return loginFailureHandler(req, res);
+      return next(new ExternalAccountLoginError('message.sign_in_failure'));
     }
 
     const user = await externalAccount.getPopulatedUser();
 
     // login
     req.logIn(user, async(err) => {
-      if (err) { debug(err.message); return next() }
+      if (err) { debug(err.message); return next(new ExternalAccountLoginError(err.message)) }
 
       return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_GOOGLE, true);
     });
@@ -429,8 +431,8 @@ module.exports = function(crowi, app) {
   const loginWithGitHub = function(req, res, next) {
     if (!passportService.isGitHubStrategySetup) {
       debug('GitHubStrategy has not been set up');
-      req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'GitHubStrategy' }));
-      return next();
+      const error = new ExternalAccountLoginError('message.strategy_has_not_been_set_up', { strategy: 'GitHubStrategy' });
+      return next(error);
     }
 
     passport.authenticate('github')(req, res);
@@ -445,7 +447,7 @@ module.exports = function(crowi, app) {
       response = await promisifiedPassportAuthentication(strategyName, req, res);
     }
     catch (err) {
-      return loginFailureHandler(req, res);
+      return next(new ExternalAccountLoginError(err.message));
     }
 
     const userInfo = {
@@ -456,14 +458,14 @@ module.exports = function(crowi, app) {
 
     const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
-      return loginFailureHandler(req, res);
+      return next(new ExternalAccountLoginError('message.sign_in_failure'));
     }
 
     const user = await externalAccount.getPopulatedUser();
 
     // login
     req.logIn(user, async(err) => {
-      if (err) { debug(err.message); return next() }
+      if (err) { debug(err.message); return next(new ExternalAccountLoginError(err.message)) }
 
       return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_GITHUB, true);
     });
@@ -472,8 +474,8 @@ module.exports = function(crowi, app) {
   const loginWithTwitter = function(req, res, next) {
     if (!passportService.isTwitterStrategySetup) {
       debug('TwitterStrategy has not been set up');
-      req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'TwitterStrategy' }));
-      return next();
+      const error = new ExternalAccountLoginError('message.strategy_has_not_been_set_up', { strategy: 'TwitterStrategy' });
+      return next(error);
     }
 
     passport.authenticate('twitter')(req, res);
@@ -488,7 +490,7 @@ module.exports = function(crowi, app) {
       response = await promisifiedPassportAuthentication(strategyName, req, res);
     }
     catch (err) {
-      return loginFailureHandler(req, res);
+      return next(new ExternalAccountLoginError(err.message));
     }
 
     const userInfo = {
@@ -499,14 +501,14 @@ module.exports = function(crowi, app) {
 
     const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
-      return loginFailureHandler(req, res);
+      return next(new ExternalAccountLoginError('message.sign_in_failure'));
     }
 
     const user = await externalAccount.getPopulatedUser();
 
     // login
     req.logIn(user, async(err) => {
-      if (err) { debug(err.message); return next() }
+      if (err) { debug(err.message); return next(new ExternalAccountLoginError(err.message)) }
 
       return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_TWITTER, true);
     });
@@ -515,8 +517,8 @@ module.exports = function(crowi, app) {
   const loginWithOidc = function(req, res, next) {
     if (!passportService.isOidcStrategySetup) {
       debug('OidcStrategy has not been set up');
-      req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'OidcStrategy' }));
-      return next();
+      const error = new ExternalAccountLoginError('message.strategy_has_not_been_set_up', { strategy: 'OidcStrategy' });
+      return next(error);
     }
 
     passport.authenticate('oidc')(req, res);
@@ -536,7 +538,7 @@ module.exports = function(crowi, app) {
     }
     catch (err) {
       debug(err);
-      return loginFailureHandler(req, res);
+      return next(new ExternalAccountLoginError(err.message));
     }
 
     const userInfo = {
@@ -549,13 +551,13 @@ module.exports = function(crowi, app) {
 
     const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
-      return loginFailureHandler(req, res);
+      return new ExternalAccountLoginError('message.sign_in_failure');
     }
 
     // login
     const user = await externalAccount.getPopulatedUser();
     req.logIn(user, async(err) => {
-      if (err) { debug(err.message); return next() }
+      if (err) { debug(err.message); return next(new ExternalAccountLoginError(err.message)) }
 
       return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_OIDC, true);
     });
@@ -564,8 +566,8 @@ module.exports = function(crowi, app) {
   const loginWithSaml = function(req, res, next) {
     if (!passportService.isSamlStrategySetup) {
       debug('SamlStrategy has not been set up');
-      req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'SamlStrategy' }));
-      return next();
+      const error = new ExternalAccountLoginError('message.strategy_has_not_been_set_up', { strategy: 'SamlStrategy' });
+      return next(error);
     }
 
     passport.authenticate('saml')(req, res);
@@ -585,7 +587,7 @@ module.exports = function(crowi, app) {
       response = await promisifiedPassportAuthentication(strategyName, req, res);
     }
     catch (err) {
-      return loginFailureHandler(req, res);
+      return next(new ExternalAccountLoginError(err.message));
     }
 
     const userInfo = {
@@ -603,12 +605,12 @@ module.exports = function(crowi, app) {
 
     // Attribute-based Login Control
     if (!crowi.passportService.verifySAMLResponseByABLCRule(response)) {
-      return loginFailureHandler(req, res, 'Sign in failure due to insufficient privileges.');
+      return next(new ExternalAccountLoginError('Sign in failure due to insufficient privileges.'));
     }
 
     const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
-      return loginFailureHandler(req, res);
+      return next(new ExternalAccountLoginError('message.sign_in_failure'));
     }
 
     const user = await externalAccount.getPopulatedUser();
@@ -617,7 +619,7 @@ module.exports = function(crowi, app) {
     req.logIn(user, (err) => {
       if (err != null) {
         logger.error(err);
-        return loginFailureHandler(req, res);
+        return next(new ExternalAccountLoginError(err.message));
       }
 
       return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_SAML, true);
@@ -633,8 +635,8 @@ module.exports = function(crowi, app) {
   const loginWithBasic = async(req, res, next) => {
     if (!passportService.isBasicStrategySetup) {
       debug('BasicStrategy has not been set up');
-      req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'Basic' }));
-      return next();
+      const error = new ExternalAccountLoginError('message.strategy_has_not_been_set_up', { strategy: 'Basic' });
+      return next(error);
     }
 
     const providerId = 'basic';
@@ -645,7 +647,7 @@ module.exports = function(crowi, app) {
       userId = await promisifiedPassportAuthentication(strategyName, req, res);
     }
     catch (err) {
-      return loginFailureHandler(req, res);
+      return next(new ExternalAccountLoginError(err.message));
     }
 
     const userInfo = {
@@ -656,12 +658,12 @@ module.exports = function(crowi, app) {
 
     const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
-      return loginFailureHandler(req, res);
+      return next(new ExternalAccountLoginError('message.sign_in_failure'));
     }
 
     const user = await externalAccount.getPopulatedUser();
     await req.logIn(user, (err) => {
-      if (err) { debug(err.message); return next() }
+      if (err) { debug(err.message); return next(new ExternalAccountLoginError(err.message)) }
 
       return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_BASIC, true);
     });
@@ -670,6 +672,7 @@ module.exports = function(crowi, app) {
   return {
     cannotLoginErrorHadnler,
     loginFailure,
+    loginFailureForExternalAccount,
     loginWithLdap,
     testLdapCredentials,
     loginWithLocal,

+ 20 - 0
packages/app/src/services/renderer/remark-plugins/table.ts

@@ -0,0 +1,20 @@
+import { Plugin } from 'unified';
+import { visit } from 'unist-util-visit';
+
+export const remarkPlugin: Plugin = function() {
+  return (tree) => {
+    visit(tree, (node) => {
+      if (node.type === 'table' || node.type === 'tableCell' || node.type === 'tableRow') {
+
+        // omit position to fix the key regardless of its position
+        // see:
+        //   https://github.com/remarkjs/react-markdown/issues/703
+        //   https://github.com/remarkjs/react-markdown/issues/466
+        //
+        //   https://github.com/remarkjs/react-markdown/blob/a80dfdee2703d84ac2120d28b0e4998a5b417c85/lib/ast-to-react.js#L201-L204
+        //   https://github.com/remarkjs/react-markdown/blob/a80dfdee2703d84ac2120d28b0e4998a5b417c85/lib/ast-to-react.js#L217-L222
+        delete node.position;
+      }
+    });
+  };
+};

+ 11 - 5
packages/app/src/services/renderer/renderer.tsx

@@ -1,10 +1,10 @@
 // allow only types to import from react
 import { ComponentType } from 'react';
 
-import { Lsx } from '@growi/plugin-lsx/components';
-import * as lsxGrowiPlugin from '@growi/plugin-lsx/services/renderer';
 import * as drawioPlugin from '@growi/remark-drawio-plugin';
 import growiPlugin from '@growi/remark-growi-plugin';
+import { Lsx, LsxImmutable } from '@growi/remark-lsx/components';
+import * as lsxGrowiPlugin from '@growi/remark-lsx/services/renderer';
 import { Schema as SanitizeOption } from 'hast-util-sanitize';
 import { SpecialComponents } from 'react-markdown/lib/ast-to-react';
 import { NormalComponents } from 'react-markdown/lib/complex-types';
@@ -26,6 +26,7 @@ import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
 import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents/DrawioViewerWithEditButton';
 import { Header } from '~/components/ReactMarkdownComponents/Header';
 import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
+import { Table } from '~/components/ReactMarkdownComponents/Table';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import loggerFactory from '~/utils/logger';
@@ -38,6 +39,7 @@ import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-lin
 import * as toc from './rehype-plugins/relocate-toc';
 import * as plantuml from './remark-plugins/plantuml';
 import { pukiwikiLikeLinker } from './remark-plugins/pukiwiki-like-linker';
+import * as table from './remark-plugins/table';
 import * as xsvToTable from './remark-plugins/xsv-to-table';
 
 // import CsvToTable from './PreProcessor/CsvToTable';
@@ -352,7 +354,7 @@ export const generateViewOptions = (
     components.h1 = Header;
     components.h2 = Header;
     components.h3 = Header;
-    components.lsx = props => <Lsx {...props} forceToFetchData />;
+    components.lsx = Lsx;
     components.drawio = DrawioViewerWithEditButton;
     components.table = TableWithEditButton;
   }
@@ -407,6 +409,7 @@ export const generateSimpleViewOptions = (config: RendererConfig, pagePath: stri
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
+    table.remarkPlugin,
   );
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);
@@ -426,8 +429,9 @@ export const generateSimpleViewOptions = (config: RendererConfig, pagePath: stri
 
   // add components
   if (components != null) {
-    components.lsx = props => <Lsx {...props} />;
+    components.lsx = LsxImmutable;
     components.drawio = drawioPlugin.DrawioViewer;
+    components.table = Table;
   }
 
   verifySanitizePlugin(options, false);
@@ -446,6 +450,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
+    table.remarkPlugin,
   );
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);
@@ -466,8 +471,9 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
 
   // add components
   if (components != null) {
-    components.lsx = props => <Lsx {...props} />;
+    components.lsx = LsxImmutable;
     components.drawio = drawioPlugin.DrawioViewer;
+    components.table = Table;
   }
 
   // verifySanitizePlugin(options, false);

+ 2 - 1
packages/app/src/stores/middlewares/sync-to-storage.ts

@@ -35,10 +35,11 @@ export const createSyncToStorageMiddlware = (
 
       config.fallbackData = initData;
       const swrNext = useSWRNext(key, fetcher, config);
+      const swrMutate = swrNext.mutate;
 
       return Object.assign(swrNext, {
         mutate: (data, shouldRevalidate) => {
-          return swrNext.mutate(data, shouldRevalidate)
+          return swrMutate(data, shouldRevalidate)
             .then((value) => {
               storage.setItem(keyInStorage, storageSerializer.serialize(value));
               return value;

+ 1 - 1
packages/app/src/styles/organisms/_wiki-custom-sidebar.scss

@@ -39,7 +39,7 @@
     margin: 10px 0;
   }
 
-  .page-list .page-list-ul {
+  .lsx.page-list > .page-list-ul {
     padding-left: 0;
     margin: 0;
   }

+ 7 - 3
packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts

@@ -15,7 +15,7 @@ context('Access to page', () => {
     cy.waitUntilSkeletonDisappear();
 
     // for check download toc data
-    cy.get('.toc-link').should('be.visible');
+    cy.get('.toc-link').eq(0).contains('Table of Contents');
 
     cy.screenshot(`${ssPrefix}-sandbox`);
   });
@@ -59,8 +59,11 @@ context('Access to page', () => {
       cy.wait(2000);
       cy.getByTestid('editor-button').should('be.visible').click();
     })
+
     cy.getByTestid('navbar-editor').should('be.visible');
     cy.get('.grw-editor-navbar-bottom').should('be.visible');
+    cy.getByTestid('save-page-btn').should('be.visible');
+    cy.get('.grw-grant-selector').should('be.visible')
 
     cy.screenshot(`${ssPrefix}-Sandbox-edit-page`);
   })
@@ -95,8 +98,9 @@ context('Access to /me page', () => {
 
   it('/me is successfully loaded', () => {
     cy.visit('/me');
-    // eslint-disable-next-line cypress/no-unnecessary-waiting
-    cy.wait(500); // wait loading image
+
+    cy.getByTestid('grw-user-settings').should('be.visible');
+
     cy.screenshot(`${ssPrefix}-me`);
   });
 

+ 21 - 20
packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts

@@ -38,10 +38,12 @@ context('Modal for page operation', () => {
 
     cy.getByTestid('newPageBtn').click();
 
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(500) // Wait for animation to finish when the Create Page button is pressed
+
     cy.getByTestid('page-create-modal').should('be.visible').within(() => {
       cy.screenshot(`${ssPrefix}new-page-modal-opened`);
       cy.get('button.close').click();
-
     });
     cy.screenshot(`${ssPrefix}page-create-modal-closed`);
   });
@@ -82,6 +84,7 @@ context('Modal for page operation', () => {
     cy.getByTestid('newPageBtn').click();
 
     cy.getByTestid('page-create-modal').should('be.visible').within(() => {
+      cy.get('.rbt-input-main').should('have.value', '/Sandbox/');
       cy.get('.rbt-input-main').type(pageName);
       cy.screenshot(`${ssPrefix}under-path-add-page-name`);
       cy.getByTestid('btn-create-page-under-below').click();
@@ -230,16 +233,13 @@ context('Page Accessories Modal', () => {
      cy.waitUntilSkeletonDisappear();
 
      cy.get('#grw-subnav-container').within(() => {
-      cy.getByTestid('open-page-item-control-btn').should('be.visible');
       cy.getByTestid('open-page-item-control-btn').within(() => {
         cy.get('button.btn-page-item-control').click({force: true});
-        cy.getByTestid('page-item-control-menu').should('be.visible');
-        cy.getByTestid('open-page-accessories-modal-btn-with-attachment-data-tab').click();
       });
+      cy.getByTestid('open-page-accessories-modal-btn-with-attachment-data-tab').click({force: true});
     });
 
-     cy.getByTestid('page-accessories-modal').should('be.visible')
-     cy.getByTestid('page-attachment').should('be.visible')
+     cy.getByTestid('page-attachment').should('be.visible').contains('No attachments yet.');
      cy.screenshot(`${ssPrefix}-open-page-attachment-data-bootstrap4`);
   });
 
@@ -317,19 +317,13 @@ context('Tag Oprations', { scrollBehavior: false }, () =>{
     cy.visit('/Sandbox');
     cy.waitUntilSkeletonDisappear();
 
-    cy.get('.grw-taglabels-container').within(()=>{
-      cy.get('.grw-tag-labels').within(()=>{
-        cy.get('a').then(($el)=>{
-          cy.wrap($el).contains(tag).click();
-        });
-      });
-    });
+    cy.get('.grw-tag-label').should('be.visible').contains(tag).click();
 
     // Search result page
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
-    cy.getByTestid('search-result-content').should('be.visible');
-    cy.get('#revision-loader').should('be.visible');
+    cy.getByTestid('search-result-content', { timeout: 60000 }).should('be.visible');
+    cy.get('#revision-loader', { timeout: 60000 }).contains('Table of Contents', { timeout: 60000 });
 
     // force to add 'active' to pass VRT: https://github.com/weseek/growi/pull/6603
     cy.getByTestid('page-list-item-L').first().invoke('addClass', 'active');
@@ -355,7 +349,10 @@ context('Tag Oprations', { scrollBehavior: false }, () =>{
     }).screenshot(`${ssPrefix}3-duplicate-page`);
 
     cy.getByTestid('page-duplicate-modal').within(() => {
+      cy.intercept('POST', '/_api/v3/pages/duplicate').as('duplicate');
       cy.get('.modal-footer > button.btn').click();
+      // Wait for completion of request to '/_api/v3/pages/duplicate'
+      cy.wait('@duplicate')
     });
 
     cy.visit(`Sandbox-${newPageName}`);
@@ -369,15 +366,16 @@ context('Tag Oprations', { scrollBehavior: false }, () =>{
     const oldPageName = '/Sandbox-our';
     const newPageName = '/Sandbox-us';
 
-    cy.visit('/Sandbox-our');
+    cy.visit(oldPageName);
     cy.waitUntilSkeletonDisappear();
 
-    // Search result page
     cy.get('.grw-tag-label').should('be.visible').contains(tag).click();
+
+    // Search result page
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
-    cy.getByTestid('search-result-content').should('be.visible');
-    cy.get('#revision-loader').should('be.visible');
+    cy.getByTestid('search-result-content', { timeout: 60000 }).should('be.visible');
+    cy.get('#revision-loader', { timeout: 60000 }).contains('Table of Contents', { timeout: 60000 });
 
     // eslint-disable-next-line cypress/no-unnecessary-waiting
     cy.wait(300);
@@ -417,7 +415,10 @@ context('Tag Oprations', { scrollBehavior: false }, () =>{
     }).screenshot(`${ssPrefix}3-insert-new-page-name`);
 
     cy.getByTestid('page-rename-modal').should('be.visible').within(() => {
-      cy.get('.modal-footer > button').click();
+      cy.intercept('PUT', '/_api/v3/pages/rename').as('rename');
+      cy.getByTestid('grw-page-rename-button').should('not.be.disabled').click();
+      // Wait for completion of request to '/_api/v3/pages/rename'
+      cy.wait('@rename')
     });
 
     cy.visit(newPageName);

+ 1 - 1
packages/app/tsconfig.build.client.json

@@ -8,7 +8,7 @@
     "paths": {
       "~/*": ["./src/*"],
       "^/*": ["./*"],
-      "@growi/plugin-lsx/*": ["../plugin-lsx/src/*"],
+      "@growi/remark-lsx/*": ["../remark-lsx/src/*"],
       "@growi/*": ["../*/src"],
       "debug": ["./src/server/utils/logger/alias-for-debug"]
     }

+ 1 - 1
packages/app/tsconfig.build.server.json

@@ -11,7 +11,7 @@
     "paths": {
       "~/*": ["./src/*"],
       "^/*": ["./*"],
-      "@growi/plugin-lsx/*": ["../plugin-lsx/dist/cjs/*"],
+      "@growi/remark-lsx/*": ["../remark-lsx/dist/cjs/*"],
       "debug": ["./src/utils/logger/alias-for-debug"]
     }
   },

+ 1 - 1
packages/app/tsconfig.json

@@ -5,7 +5,7 @@
     "paths": {
       "~/*": ["./src/*"],
       "^/*": ["./*"],
-      "@growi/plugin-lsx/*": ["../plugin-lsx/src/*"],
+      "@growi/remark-lsx/*": ["../remark-lsx/src/*"],
       "@growi/*": ["../*/src"],
       "debug": ["./src/server/utils/logger/alias-for-debug"]
     }

+ 0 - 1
packages/core/src/index.ts

@@ -22,7 +22,6 @@ export * from './interfaces/revision';
 export * from './interfaces/subscription';
 export * from './interfaces/tag';
 export * from './interfaces/user';
-export * from './plugin/service/tag-cache-manager';
 export * from './models/devided-page-path';
 export * from './models/vo/error-apiv3';
 export * from './service/localstorage-manager';

+ 0 - 69
packages/core/src/plugin/service/tag-cache-manager.js

@@ -1,69 +0,0 @@
-import { LocalStorageManager } from '../../service/localstorage-manager';
-
-/**
- * Service Class for caching React state and TagContext
- */
-export class TagCacheManager {
-
-  /**
-   * @callback generateCacheKey
-   * @param {TagContext} tagContext - TagContext instance
-   * @returns {string} Cache key from TagContext
-   *
-   */
-
-  /**
-   * Constructor
-   * @param {string} cacheNs Used as LocalStorageManager namespace
-   * @param {generateCacheKey} generateCacheKey
-   */
-  constructor(cacheNs, generateCacheKey) {
-    if (cacheNs == null) {
-      throw new Error('args \'cacheNs\' is required.');
-    }
-    if (generateCacheKey == null) {
-      throw new Error('args \'generateCacheKey\' is required.');
-    }
-    if (typeof generateCacheKey !== 'function') {
-      throw new Error('args \'generateCacheKey\' should be function.');
-    }
-
-    this.cacheNs = cacheNs;
-    this.generateCacheKey = generateCacheKey;
-  }
-
-  /**
-   * Retrieve state cache object from local storage
-   * @param {TagContext} tagContext
-   * @returns {object} a cache object that correspont to the specified `tagContext`
-   */
-  getStateCache(tagContext) {
-    const localStorageManager = LocalStorageManager.getInstance();
-
-    const key = this.generateCacheKey(tagContext);
-    const stateCache = localStorageManager.retrieveFromSessionStorage(this.cacheNs, key);
-
-    return stateCache;
-  }
-
-  /**
-   * store state object of React Component with specified key
-   *
-   * @param {TagContext} tagContext
-   * @param {object} state state object of React Component
-   */
-  cacheState(tagContext, state) {
-    const localStorageManager = LocalStorageManager.getInstance();
-    const key = this.generateCacheKey(tagContext);
-    localStorageManager.saveToSessionStorage(this.cacheNs, key, state);
-  }
-
-  /**
-   * clear all state caches
-   */
-  clearAllStateCaches() {
-    const localStorageManager = LocalStorageManager.getInstance();
-    localStorageManager.clearAllStateCaches(this.cacheNs);
-  }
-
-}

+ 0 - 125
packages/core/src/test/plugin/service/tag-cache-manager.test.js

@@ -1,125 +0,0 @@
-/* eslint-disable import/first */
-
-// import each from 'jest-each';
-jest.mock('~/service/localstorage-manager');
-
-import { TagCacheManager } from '~/plugin/service/tag-cache-manager';
-import { LocalStorageManager } from '~/service/localstorage-manager';
-/* eslint-enable import/first */
-
-describe('TagCacheManager.constructor', () => {
-
-  test('throws Exception when \'cacheNs\' is null', () => {
-    const generateCacheKeyMock = jest.fn();
-
-    expect(() => {
-      // eslint-disable-next-line no-new
-      new TagCacheManager(null, generateCacheKeyMock);
-    }).toThrowError(/cacheNs/);
-  });
-
-  test('throws Exception when \'generateCacheKey\' is null', () => {
-    expect(() => {
-      // eslint-disable-next-line no-new
-      new TagCacheManager('dummy ns', null);
-    }).toThrowError(/generateCacheKey/);
-  });
-
-  test('throws Exception when \'generateCacheKey\' is not function', () => {
-    expect(() => {
-      // eslint-disable-next-line no-new
-      new TagCacheManager('dummy ns', {});
-    }).toThrowError(/generateCacheKey/);
-  });
-
-  test('set params', () => {
-    const generateCacheKeyMock = jest.fn();
-
-    const instance = new TagCacheManager('dummy ns', generateCacheKeyMock);
-    expect(instance).not.toBeNull();
-    expect(instance.cacheNs).toBe('dummy ns');
-    expect(instance.generateCacheKey).toBe(generateCacheKeyMock);
-  });
-
-});
-
-describe('TagCacheManager', () => {
-
-  let generateCacheKeyMock = null;
-  let localStorageManagerMock = null;
-
-  let tagCacheManager = null;
-
-
-  beforeEach(() => {
-    generateCacheKeyMock = jest.fn();
-    localStorageManagerMock = jest.fn();
-
-    // mock for LocalStorageManager.getInstance
-    LocalStorageManager.getInstance = jest.fn();
-    LocalStorageManager.getInstance.mockReturnValue(localStorageManagerMock);
-
-    tagCacheManager = new TagCacheManager('dummy ns', generateCacheKeyMock);
-  });
-
-  test('.getStateCache', () => {
-    // partial mock
-    tagCacheManager.generateCacheKey = jest.fn().mockReturnValue('dummy key');
-
-    // mock for LocalStorageManager
-    const stateCacheMock = jest.fn();
-    localStorageManagerMock.retrieveFromSessionStorage = jest.fn();
-    localStorageManagerMock.retrieveFromSessionStorage
-      .mockReturnValue(stateCacheMock);
-
-    const tagContextMock = jest.fn();
-
-    // when
-    const result = tagCacheManager.getStateCache(tagContextMock);
-    // then
-    expect(result).not.toBeNull();
-    expect(result).toBe(stateCacheMock);
-    const generateCacheKeyMockCalls = tagCacheManager.generateCacheKey.mock.calls;
-    expect(generateCacheKeyMockCalls.length).toBe(1);
-    expect(generateCacheKeyMockCalls[0][0]).toBe(tagContextMock);
-    const retrieveFromSessionStorageMockCalls = localStorageManagerMock.retrieveFromSessionStorage.mock.calls;
-    expect(retrieveFromSessionStorageMockCalls.length).toBe(1);
-    expect(retrieveFromSessionStorageMockCalls[0][0]).toBe('dummy ns');
-    expect(retrieveFromSessionStorageMockCalls[0][1]).toBe('dummy key');
-  });
-
-  test('.getStateCache with state object', () => {
-    // partial mock
-    tagCacheManager.generateCacheKey = jest.fn().mockReturnValue('dummy key');
-
-    // mock for LocalStorageManager
-    localStorageManagerMock.saveToSessionStorage = jest.fn();
-
-    const tagContextMock = jest.fn();
-    const stateMock = jest.fn();
-
-    // when
-    tagCacheManager.cacheState(tagContextMock, stateMock);
-    // then
-    const generateCacheKeyMockCalls = tagCacheManager.generateCacheKey.mock.calls;
-    expect(generateCacheKeyMockCalls.length).toBe(1);
-    expect(generateCacheKeyMockCalls[0][0]).toBe(tagContextMock);
-    const saveToSessionStorageMockCalls = localStorageManagerMock.saveToSessionStorage.mock.calls;
-    expect(saveToSessionStorageMockCalls.length).toBe(1);
-    expect(saveToSessionStorageMockCalls[0][0]).toBe('dummy ns');
-    expect(saveToSessionStorageMockCalls[0][1]).toBe('dummy key');
-    expect(saveToSessionStorageMockCalls[0][2]).toBe(stateMock);
-  });
-
-  test('.clearAllStateCaches', () => {
-    // mock for LocalStorageManager
-    localStorageManagerMock.clearAllStateCaches = jest.fn();
-
-    // when
-    tagCacheManager.clearAllStateCaches();
-    // then
-    const clearAllStateCachesMockCalls = localStorageManagerMock.clearAllStateCaches.mock.calls;
-    expect(clearAllStateCachesMockCalls.length).toBe(1);
-    expect(clearAllStateCachesMockCalls[0][0]).toBe('dummy ns');
-  });
-});

+ 0 - 265
packages/plugin-lsx/src/components/Lsx.tsx

@@ -1,265 +0,0 @@
-import React, {
-  useCallback, useEffect, useMemo, useState,
-} from 'react';
-
-import * as url from 'url';
-
-import { IPage, pathUtils } from '@growi/core';
-import axios from 'axios';
-
-import { LsxListView } from './LsxPageList/LsxListView';
-import { PageNode } from './PageNode';
-import { LsxContext } from './lsx-context';
-import { getInstance as getTagCacheManager } from './tag-cache-manager';
-
-import styles from './Lsx.module.scss';
-
-
-const tagCacheManager = getTagCacheManager();
-
-
-/**
- * compare whether path1 and path2 is the same
- *
- * @param {string} path1
- * @param {string} path2
- * @returns
- *
- * @memberOf Lsx
- */
-function isEquals(path1: string, path2: string) {
-  return pathUtils.removeTrailingSlash(path1) === pathUtils.removeTrailingSlash(path2);
-}
-
-function getParentPath(path: string) {
-  return pathUtils.addTrailingSlash(decodeURIComponent(url.resolve(path, '../')));
-}
-
-/**
- * generate PageNode instances for target page and the ancestors
- *
- * @param {any} pathToNodeMap
- * @param {any} rootPagePath
- * @param {any} pagePath
- * @returns
- * @memberof Lsx
- */
-function generatePageNode(pathToNodeMap: Record<string, PageNode>, rootPagePath: string, pagePath: string): PageNode | null {
-  // exclude rootPagePath itself
-  if (isEquals(pagePath, rootPagePath)) {
-    return null;
-  }
-
-  // return when already registered
-  if (pathToNodeMap[pagePath] != null) {
-    return pathToNodeMap[pagePath];
-  }
-
-  // generate node
-  const node = new PageNode(pagePath);
-  pathToNodeMap[pagePath] = node;
-
-  /*
-    * process recursively for ancestors
-    */
-  // get or create parent node
-  const parentPath = getParentPath(pagePath);
-  const parentNode = generatePageNode(pathToNodeMap, rootPagePath, parentPath);
-  // associate to patent
-  if (parentNode != null) {
-    parentNode.children.push(node);
-  }
-
-  return node;
-}
-
-function generatePageNodeTree(rootPagePath: string, pages: IPage[]) {
-  const pathToNodeMap: Record<string, PageNode> = {};
-
-  pages.forEach((page) => {
-    // add slash ensure not to forward match to another page
-    // e.g. '/Java/' not to match to '/JavaScript'
-    const pagePath = pathUtils.addTrailingSlash(page.path);
-
-    const node = generatePageNode(pathToNodeMap, rootPagePath, pagePath); // this will not be null
-
-    // exclude rootPagePath itself
-    if (node == null) {
-      return;
-    }
-
-    // set the Page substance
-    node.page = page;
-  });
-
-  // return root objects
-  const rootNodes: PageNode[] = [];
-  Object.keys(pathToNodeMap).forEach((pagePath) => {
-    // exclude '/'
-    if (pagePath === '/') {
-      return;
-    }
-
-    const parentPath = getParentPath(pagePath);
-
-    // pick up what parent doesn't exist
-    if ((parentPath === '/') || !(parentPath in pathToNodeMap)) {
-      rootNodes.push(pathToNodeMap[pagePath]);
-    }
-  });
-  return rootNodes;
-}
-
-
-type Props = {
-  children: React.ReactNode,
-  className?: string,
-
-  prefix: string,
-  num?: string,
-  depth?: string,
-  sort?: string,
-  reverse?: string,
-  filter?: string,
-
-  forceToFetchData?: boolean,
-};
-
-type StateCache = {
-  isError: boolean,
-  errorMessage: string,
-  basisViewersCount?: number,
-  nodeTree?: PageNode[],
-}
-
-export const Lsx = ({
-  prefix,
-  num, depth, sort, reverse, filter,
-  ...props
-}: Props): JSX.Element => {
-
-  const [isLoading, setLoading] = useState(false);
-  const [isError, setError] = useState(false);
-  const [isCacheExists, setCacheExists] = useState(false);
-  const [nodeTree, setNodeTree] = useState<PageNode[]|undefined>();
-  const [basisViewersCount, setBasisViewersCount] = useState<number|undefined>();
-  const [errorMessage, setErrorMessage] = useState('');
-
-  const { forceToFetchData } = props;
-
-  const lsxContext = useMemo(() => {
-    const options = {
-      num, depth, sort, reverse, filter,
-    };
-    return new LsxContext(prefix, options);
-  }, [depth, filter, num, prefix, reverse, sort]);
-
-  const retrieveDataFromCache = useCallback(() => {
-    // get state object cache
-    const stateCache = tagCacheManager.getStateCache(lsxContext) as StateCache | null;
-
-    // instanciate PageNode
-    if (stateCache != null && stateCache.nodeTree != null) {
-      stateCache.nodeTree = stateCache.nodeTree.map((obj) => {
-        return PageNode.instanciateFrom(obj);
-      });
-    }
-
-    return stateCache;
-  }, [lsxContext]);
-
-  const loadData = useCallback(async() => {
-    setLoading(true);
-
-    // add slash ensure not to forward match to another page
-    // ex: '/Java/' not to match to '/JavaScript'
-    const pagePath = pathUtils.addTrailingSlash(lsxContext.pagePath);
-
-    let newNodeTree: PageNode[] = [];
-    try {
-      const result = await axios.get('/_api/plugins/lsx', {
-        params: {
-          pagePath,
-          options: lsxContext.options,
-        },
-      });
-
-      newNodeTree = generatePageNodeTree(pagePath, result.data.pages);
-      setNodeTree(newNodeTree);
-      setBasisViewersCount(result.data.toppageViewersCount);
-      setError(false);
-
-      // store to sessionStorage
-      tagCacheManager.cacheState(lsxContext, {
-        isError: false,
-        errorMessage: '',
-        basisViewersCount,
-        nodeTree: newNodeTree,
-      });
-    }
-    catch (error) {
-      setError(true);
-      setErrorMessage(error.message);
-
-      // store to sessionStorage
-      tagCacheManager.cacheState(lsxContext, {
-        isError: true,
-        errorMessage: error.message,
-      });
-    }
-    finally {
-      setLoading(false);
-    }
-  }, [basisViewersCount, lsxContext]);
-
-  useEffect(() => {
-    // get state object cache
-    const stateCache = retrieveDataFromCache();
-
-    if (stateCache != null) {
-      setCacheExists(true);
-      setNodeTree(stateCache.nodeTree);
-      setError(stateCache.isError);
-      setErrorMessage(stateCache.errorMessage);
-
-      // switch behavior by forceToFetchData
-      if (!forceToFetchData) {
-        return; // go to render()
-      }
-    }
-
-    loadData();
-  }, [forceToFetchData, loadData, retrieveDataFromCache]);
-
-  const renderContents = () => {
-    if (isError) {
-      return (
-        <div className="text-warning">
-          <i className="fa fa-exclamation-triangle fa-fw"></i>
-          {lsxContext.toString()} (-&gt; <small>{errorMessage}</small>)
-        </div>
-      );
-    }
-
-    const showListView = nodeTree != null && (!isLoading || nodeTree.length > 0);
-
-    return (
-      <>
-        { isLoading && (
-          <div className={`text-muted ${isLoading ? 'lsx-blink' : ''}`}>
-            <small>
-              <i className="fa fa-spinner fa-pulse mr-1"></i>
-              {lsxContext.toString()}
-              { isCacheExists && <>&nbsp;(Showing cache..)</> }
-            </small>
-          </div>
-        ) }
-        { showListView && (
-          <LsxListView nodeTree={nodeTree} lsxContext={lsxContext} basisViewersCount={basisViewersCount} />
-        ) }
-      </>
-    );
-  };
-
-  return <div className={`lsx ${styles.lsx}`}>{renderContents()}</div>;
-};

+ 0 - 54
packages/plugin-lsx/src/components/LsxPageList/LsxListView.jsx

@@ -1,54 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-
-import { PageNode } from '../PageNode';
-import { LsxContext } from '../lsx-context';
-
-import { LsxPage } from './LsxPage';
-
-import styles from './LsxListView.module.scss';
-
-export class LsxListView extends React.Component {
-
-  render() {
-    const listView = this.props.nodeTree.map((pageNode) => {
-      return (
-        <LsxPage
-          key={pageNode.pagePath}
-          depth={1}
-          pageNode={pageNode}
-          lsxContext={this.props.lsxContext}
-          basisViewersCount={this.props.basisViewersCount}
-        />
-      );
-    });
-
-    // no contents
-    if (this.props.nodeTree.length === 0) {
-      return (
-        <div className="text-muted">
-          <small>
-            <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
-            $lsx(<a href={this.props.lsxContext.pagePath}>{this.props.lsxContext.pagePath}</a>) has no contents
-          </small>
-        </div>
-      );
-    }
-
-    return (
-      <div className={`page-list ${styles['page-list']} lsx`}>
-        <ul className="page-list-ul">
-          {listView}
-        </ul>
-      </div>
-    );
-  }
-
-}
-
-LsxListView.propTypes = {
-  nodeTree: PropTypes.arrayOf(PropTypes.instanceOf(PageNode)).isRequired,
-  lsxContext: PropTypes.instanceOf(LsxContext).isRequired,
-  basisViewersCount: PropTypes.number,
-};

+ 0 - 48
packages/plugin-lsx/src/components/PageNode.js

@@ -1,48 +0,0 @@
-export class PageNode {
-
-  constructor(pagePath) {
-    this.pagePath = pagePath;
-    this.children = [];
-
-    this.page = undefined;
-  }
-
-  /**
-   * calculate generations number of decendants
-   *
-   * ex:
-   *  /foo          -2
-   *  /foo/bar      -1
-   *  /foo/bar/buz   0
-   *
-   * @returns generations num of decendants
-   *
-   * @memberOf PageNode
-   */
-  /*
-   * commented out because it became unnecessary -- 2017.05.18 Yuki Takei
-   *
-  getDecendantsGenerationsNum() {
-    if (this.children.length == 0) {
-      return -1;
-    }
-
-    return -1 + Math.min.apply(null, this.children.map((child) => {
-      return child.getDecendantsGenerationsNum();
-    }))
-  }
-  */
-
-  static instanciateFrom(obj) {
-    const pageNode = new PageNode(obj.pagePath);
-    pageNode.page = obj.page;
-
-    // instanciate recursively
-    pageNode.children = obj.children.map((childObj) => {
-      return PageNode.instanciateFrom(childObj);
-    });
-
-    return pageNode;
-  }
-
-}

+ 0 - 1
packages/plugin-lsx/src/components/index.ts

@@ -1 +0,0 @@
-export { Lsx } from './Lsx';

+ 0 - 21
packages/plugin-lsx/src/components/tag-cache-manager.ts

@@ -1,21 +0,0 @@
-import { TagCacheManager } from '@growi/core';
-
-import { LsxContext } from './lsx-context';
-
-const LSX_STATE_CACHE_NS = 'lsx-state-cache';
-
-
-let _instance;
-
-export function getInstance(): TagCacheManager {
-  if (_instance == null) {
-    // create generateCacheKey implementation
-    const generateCacheKey = (lsxContext: LsxContext) => {
-      return `${lsxContext.pagePath}__${lsxContext.getStringifiedAttributes('_')}`;
-    };
-
-    _instance = new TagCacheManager(LSX_STATE_CACHE_NS, generateCacheKey);
-  }
-
-  return _instance;
-}

+ 2 - 2
packages/remark-drawio-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-drawio-plugin",
-  "version": "6.0.0-RC.8",
+  "version": "6.0.0-RC.9",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "keywords": [
@@ -24,11 +24,11 @@
     "test": ""
   },
   "dependencies": {
+    "pako": "^2.1.0",
     "xmldoc": "^1.2.0"
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
-    "pako": "^2.1.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
   }

+ 27 - 12
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/lib/factory-attributes.js

@@ -82,13 +82,6 @@ export function factoryAttributes(
       return shortcutStart(code);
     }
 
-    if (code === codes.colon || code === codes.underscore || code === codes.slash || asciiAlpha(code)) {
-      effects.enter(attributeType);
-      effects.enter(attributeNameType);
-      effects.consume(code);
-      return name;
-    }
-
     if (disallowEol && markdownSpace(code)) {
       return factorySpace(effects, between, types.whitespace)(code);
     }
@@ -97,6 +90,19 @@ export function factoryAttributes(
       return factoryAttributesDevider(effects, between)(code);
     }
 
+    if (code !== codes.rightParenthesis
+      && code !== codes.eof
+      && code !== codes.carriageReturn
+      && code !== codes.lineFeed
+      && code !== codes.carriageReturnLineFeed
+      && code !== codes.ampersand
+    ) {
+      effects.enter(attributeType);
+      effects.enter(attributeNameType);
+      effects.consume(code);
+      return name;
+    }
+
     return end(code);
   }
 
@@ -166,11 +172,20 @@ export function factoryAttributes(
   /** @type {State} */
   function name(code) {
     if (
-      code === codes.dash
-      || code === codes.dot
-      || code === codes.colon
-      || code === codes.underscore
-      || asciiAlphanumeric(code)
+      code !== codes.eof
+        && code !== codes.carriageReturn
+        && code !== codes.lineFeed
+        && code !== codes.carriageReturnLineFeed
+        && code !== codes.quotationMark
+        && code !== codes.numberSign
+        && code !== codes.apostrophe
+        && code !== codes.dot
+        && code !== codes.lessThan
+        && code !== codes.equalsTo
+        && code !== codes.greaterThan
+        && code !== codes.graveAccent
+        && code !== codes.rightParenthesis
+        && code !== codes.space
     ) {
       effects.consume(code);
       return name;

+ 69 - 2
packages/remark-growi-plugin/test/micromark-extension-growi-plugin.test.js

@@ -113,7 +113,7 @@ test('micromark-extension-directive (syntax)', (t) => {
     t.equal(
       micromark('$a(', options()),
       '<p>(</p>',
-      'should support a name followed by an unclosed `{`',
+      'should support a name followed by an unclosed `(`',
     );
 
     t.equal(
@@ -125,7 +125,7 @@ test('micromark-extension-directive (syntax)', (t) => {
     t.equal(
       micromark('$a(b', options()),
       '<p>(b</p>',
-      'should support a name followed by an unclosed `{` w/ content',
+      'should support a name followed by an unclosed `(` w/ content',
     );
 
     t.equal(
@@ -326,6 +326,12 @@ test('micromark-extension-directive (syntax)', (t) => {
       'should not support a grave accent in an unquoted attribute value',
     );
 
+    t.equal(
+      micromark('a $a(b💚=a💚b)', options()),
+      '<p>a </p>',
+      'should support most other characters in attribute keys',
+    );
+
     t.equal(
       micromark('a $a(b=a💚b)', options()),
       '<p>a </p>',
@@ -547,6 +553,12 @@ test('micromark-extension-directive (syntax)', (t) => {
     //   'should not support `=` to start an unquoted attribute value',
     // );
 
+    t.equal(
+      micromark('$a(b💚=a💚b)', options()),
+      '',
+      'should support most other characters in attribute keys',
+    );
+
     t.equal(
       micromark('$a(b=a💚b)', options()),
       '',
@@ -772,6 +784,41 @@ test('micromark-extension-directive (compile)', (t) => {
     'should support directives (youtube)',
   );
 
+  t.equal(
+    micromark(
+      [
+        'Text:',
+        'a $lsx',
+        'a $lsx()',
+        'a $lsx(num=1)',
+        'a $lsx(/)',
+        'a $lsx(💚)',
+        'Leaf:',
+        '$lsx',
+        '$lsx()',
+        '$lsx(num=1)',
+        '$lsx(/)',
+        '$lsx(💚)',
+      ].join('\n\n'),
+      options({ lsx }),
+    ),
+    [
+      '<p>Text:</p>',
+      '<p>a <lsx ></lsx></p>',
+      '<p>a <lsx ></lsx></p>',
+      '<p>a <lsx num="1"></lsx></p>',
+      '<p>a <lsx prefix="/"></lsx></p>',
+      '<p>a <lsx prefix="💚"></lsx></p>',
+      '<p>Leaf:</p>',
+      '<lsx ></lsx>',
+      '<lsx ></lsx>',
+      '<lsx num="1"></lsx>',
+      '<lsx prefix="/"></lsx>',
+      '<lsx prefix="💚"></lsx>',
+    ].join('\n'),
+    'should support directives (lsx)',
+  );
+
   t.equal(
     micromark('a $youtube[Cat in a box]\n$br a', options({ youtube, '*': h })),
     '<p>a <youtube>Cat in a box</youtube>\n<br> a</p>',
@@ -1051,6 +1098,26 @@ function youtube(d) {
   this.tag('</iframe>');
 }
 
+/** @type {Handle} */
+function lsx(d) {
+  const attrs = d.attributes || {};
+
+  const props = [];
+
+  // eslint-disable-next-line no-restricted-syntax
+  for (const key in attrs) {
+    if (attrs[key].length === 0) {
+      props.push(`prefix="${key}"`);
+    }
+    else {
+      props.push(`${key}="${attrs[key]}"`);
+    }
+  }
+
+  this.tag(`<lsx ${props.join(' ')}>`);
+  this.tag('</lsx>');
+}
+
 /** @type {Handle} */
 function h(d) {
   const content = d.content || d.label;

+ 0 - 0
packages/plugin-lsx/.eslintignore → packages/remark-lsx/.eslintignore


+ 0 - 0
packages/plugin-lsx/.eslintrc.js → packages/remark-lsx/.eslintrc.js


+ 0 - 0
packages/plugin-lsx/.gitignore → packages/remark-lsx/.gitignore


+ 1 - 1
packages/plugin-lsx/README.md → packages/remark-lsx/README.md

@@ -1,4 +1,4 @@
-# growi-plugin-lsx
+# remark-lsx
 
 [GROWI][growi] Plugin to add lsx tag like [Pukiwiki lsx plugin](http://ukiya.sakura.ne.jp/index.php?PukiWiki%2F1.4%2F%E3%83%9E%E3%83%8B%E3%83%A5%E3%82%A2%E3%83%AB%2F%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%2F%E7%8B%AC%E8%87%AA%E3%81%AB%E8%BF%BD%E5%8A%A0%E3%81%97%E3%81%9F%E3%82%82%E3%81%AE%2Flsx)
 

+ 3 - 2
packages/plugin-lsx/package.json → packages/remark-lsx/package.json

@@ -1,5 +1,5 @@
 {
-  "name": "@growi/plugin-lsx",
+  "name": "@growi/remark-lsx",
   "version": "6.0.0-RC.9",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
@@ -25,7 +25,8 @@
   "dependencies": {
     "@growi/core": "^6.0.0-RC.9",
     "@growi/remark-growi-plugin": "^6.0.0-RC.9",
-    "@growi/ui": "^6.0.0-RC.9"
+    "@growi/ui": "^6.0.0-RC.9",
+    "swr": "^1.3.0"
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",

+ 0 - 0
packages/plugin-lsx/src/components/Lsx.module.scss → packages/remark-lsx/src/components/Lsx.module.scss


+ 98 - 0
packages/remark-lsx/src/components/Lsx.tsx

@@ -0,0 +1,98 @@
+import React, { useCallback, useMemo } from 'react';
+
+
+import { useSWRxNodeTree } from '../stores/lsx';
+
+import { LsxListView } from './LsxPageList/LsxListView';
+import { LsxContext } from './lsx-context';
+
+import styles from './Lsx.module.scss';
+
+
+type Props = {
+  children: React.ReactNode,
+  className?: string,
+
+  prefix: string,
+  num?: string,
+  depth?: string,
+  sort?: string,
+  reverse?: string,
+  filter?: string,
+
+  isImmutable?: boolean,
+};
+
+export const Lsx = React.memo(({
+  prefix,
+  num, depth, sort, reverse, filter,
+  isImmutable,
+  ...props
+}: Props): JSX.Element => {
+
+  const lsxContext = useMemo(() => {
+    const options = {
+      num, depth, sort, reverse, filter,
+    };
+    return new LsxContext(prefix, options);
+  }, [depth, filter, num, prefix, reverse, sort]);
+
+  const { data, error } = useSWRxNodeTree(lsxContext, isImmutable);
+
+  const isLoading = data === undefined;
+  const hasError = error != null;
+  const errorMessage = error?.message;
+
+  const Error = useCallback((): JSX.Element => {
+    if (!hasError) {
+      return <></>;
+    }
+
+    return (
+      <div className="text-warning">
+        <i className="fa fa-exclamation-triangle fa-fw"></i>
+        {lsxContext.toString()} (-&gt; <small>{errorMessage}</small>)
+      </div>
+    );
+  }, [errorMessage, hasError, lsxContext]);
+
+  const Loading = useCallback((): JSX.Element => {
+    if (hasError) {
+      return <></>;
+    }
+    if (!isLoading) {
+      return <></>;
+    }
+
+    return (
+      <div className={`text-muted ${isLoading ? 'lsx-blink' : ''}`}>
+        <small>
+          <i className="fa fa-spinner fa-pulse mr-1"></i>
+          {lsxContext.toString()}
+        </small>
+      </div>
+    );
+  }, [hasError, isLoading, lsxContext]);
+
+  const contents = useMemo(() => {
+    if (isLoading) {
+      return <></>;
+    }
+
+    return <LsxListView nodeTree={data.nodeTree} lsxContext={lsxContext} basisViewersCount={data.toppageViewersCount} />;
+  }, [data?.nodeTree, data?.toppageViewersCount, isLoading, lsxContext]);
+
+  return (
+    <div className={`lsx ${styles.lsx}`}>
+      <Error />
+      <Loading />
+      {contents}
+    </div>
+  );
+});
+Lsx.displayName = 'Lsx';
+
+export const LsxImmutable = React.memo((props: Omit<Props, 'isImmutable'>): JSX.Element => {
+  return <Lsx {...props} isImmutable />;
+});
+LsxImmutable.displayName = 'LsxImmutable';

+ 0 - 0
packages/plugin-lsx/src/components/LsxPageList/LsxListView.module.scss → packages/remark-lsx/src/components/LsxPageList/LsxListView.module.scss


+ 58 - 0
packages/remark-lsx/src/components/LsxPageList/LsxListView.tsx

@@ -0,0 +1,58 @@
+import React, { useMemo } from 'react';
+
+import type { PageNode } from '../../interfaces/page-node';
+import { LsxContext } from '../lsx-context';
+
+import { LsxPage } from './LsxPage';
+
+import styles from './LsxListView.module.scss';
+
+
+type Props = {
+  nodeTree?: PageNode[],
+  lsxContext: LsxContext,
+  basisViewersCount?: number,
+};
+
+
+export const LsxListView = React.memo((props: Props): JSX.Element => {
+
+  const { nodeTree, lsxContext, basisViewersCount } = props;
+
+  const isEmpty = nodeTree == null || nodeTree.length === 0;
+
+  const contents = useMemo(() => {
+    if (isEmpty) {
+      return (
+        <div className="text-muted">
+          <small>
+            <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
+            $lsx(<a href={lsxContext.pagePath}>{lsxContext.pagePath}</a>) has no contents
+          </small>
+        </div>
+      );
+    }
+
+    return nodeTree.map((pageNode) => {
+      return (
+        <LsxPage
+          key={pageNode.pagePath}
+          depth={1}
+          pageNode={pageNode}
+          lsxContext={lsxContext}
+          basisViewersCount={basisViewersCount}
+        />
+      );
+    });
+  }, [basisViewersCount, isEmpty, lsxContext, nodeTree]);
+
+  return (
+    <div className={`page-list ${styles['page-list']} lsx`}>
+      <ul className="page-list-ul">
+        {contents}
+      </ul>
+    </div>
+  );
+
+});
+LsxListView.displayName = 'LsxListView';

+ 22 - 8
packages/plugin-lsx/src/components/LsxPageList/LsxPage.tsx → packages/remark-lsx/src/components/LsxPageList/LsxPage.tsx

@@ -2,8 +2,9 @@ import React, { useMemo } from 'react';
 
 import { pathUtils } from '@growi/core';
 import { PagePathLabel, PageListMeta } from '@growi/ui';
+import Link from 'next/link';
 
-import { PageNode } from '../PageNode';
+import type { PageNode } from '../../interfaces/page-node';
 import { LsxContext } from '../lsx-context';
 
 
@@ -19,7 +20,8 @@ export const LsxPage = React.memo((props: Props): JSX.Element => {
     pageNode, lsxContext, depth, basisViewersCount,
   } = props;
 
-  const isExists = pageNode.page !== undefined;
+  const pageId = pageNode.page?._id;
+  const pagePath = pageNode.pagePath;
   const isLinkable = (() => {
     // process depth option
     const optDepth = lsxContext.getOptDepth();
@@ -58,31 +60,42 @@ export const LsxPage = React.memo((props: Props): JSX.Element => {
   }, [basisViewersCount, depth, hasChildren, lsxContext, pageNode.children]);
 
   const iconElement: JSX.Element = useMemo(() => {
+    const isExists = pageId != null;
     return (isExists)
       ? <i className="ti ti-agenda" aria-hidden="true"></i>
       : <i className="ti ti-file lsx-page-not-exist" aria-hidden="true"></i>;
-  }, [isExists]);
+  }, [pageId]);
 
   const pagePathElement: JSX.Element = useMemo(() => {
+    const isExists = pageId != null;
+
     const classNames: string[] = [];
     if (!isExists) {
       classNames.push('lsx-page-not-exist');
     }
 
     // create PagePath element
-    let pagePathNode = <PagePathLabel path={pageNode.pagePath} isLatterOnly additionalClassNames={classNames} />;
+    let pagePathNode = <PagePathLabel path={pagePath} isLatterOnly additionalClassNames={classNames} />;
     if (isLinkable) {
-      pagePathNode = <a className="page-list-link" href={encodeURI(pathUtils.removeTrailingSlash(pageNode.pagePath))}>{pagePathNode}</a>;
+      const href = isExists
+        ? `/${pageId}`
+        : encodeURI(pathUtils.removeTrailingSlash(pagePath));
+
+      pagePathNode = (
+        <Link href={href} prefetch={false}>
+          <a className="page-list-link" href={href}>{pagePathNode}</a>
+        </Link>
+      );
     }
     return pagePathNode;
-  }, [isExists, isLinkable, pageNode.pagePath]);
+  }, [isLinkable, pageId, pagePath]);
 
   const pageListMetaElement: JSX.Element = useMemo(() => {
-    if (!isExists) {
+    if (pageNode.page == null) {
       return <></>;
     }
     return <PageListMeta page={pageNode.page} basisViewersCount={basisViewersCount} />;
-  }, [basisViewersCount, isExists, pageNode.page]);
+  }, [basisViewersCount, pageNode.page]);
 
   return (
     <li className="page-list-li">
@@ -93,3 +106,4 @@ export const LsxPage = React.memo((props: Props): JSX.Element => {
   );
 
 });
+LsxPage.displayName = 'LsxPage';

+ 1 - 0
packages/remark-lsx/src/components/index.ts

@@ -0,0 +1 @@
+export { Lsx, LsxImmutable } from './Lsx';

+ 0 - 0
packages/plugin-lsx/src/components/lsx-context.ts → packages/remark-lsx/src/components/lsx-context.ts


+ 0 - 0
packages/plugin-lsx/src/index.ts → packages/remark-lsx/src/index.ts


+ 7 - 0
packages/remark-lsx/src/interfaces/page-node.ts

@@ -0,0 +1,7 @@
+import { IPageHasId } from '@growi/core';
+
+export type PageNode = {
+  pagePath: string,
+  children: PageNode[],
+  page?: IPageHasId,
+}

+ 0 - 0
packages/plugin-lsx/src/server/routes/index.js → packages/remark-lsx/src/server/routes/index.js


+ 0 - 0
packages/plugin-lsx/src/server/routes/lsx.js → packages/remark-lsx/src/server/routes/lsx.js


+ 0 - 0
packages/plugin-lsx/src/services/renderer/index.ts → packages/remark-lsx/src/services/renderer/index.ts


+ 9 - 0
packages/plugin-lsx/src/services/renderer/lsx.ts → packages/remark-lsx/src/services/renderer/lsx.ts

@@ -48,6 +48,15 @@ export const remarkPlugin: Plugin = function() {
 
         data.hName = 'lsx';
         data.hProperties = attributes;
+
+        // omit position to fix the key regardless of its position
+        // see:
+        //   https://github.com/remarkjs/react-markdown/issues/703
+        //   https://github.com/remarkjs/react-markdown/issues/466
+        //
+        //   https://github.com/remarkjs/react-markdown/blob/a80dfdee2703d84ac2120d28b0e4998a5b417c85/lib/ast-to-react.js#L201-L204
+        //   https://github.com/remarkjs/react-markdown/blob/a80dfdee2703d84ac2120d28b0e4998a5b417c85/lib/ast-to-react.js#L217-L222
+        delete node.position;
       }
     });
   };

+ 144 - 0
packages/remark-lsx/src/stores/lsx.tsx

@@ -0,0 +1,144 @@
+import * as url from 'url';
+
+import { IPageHasId, pathUtils } from '@growi/core';
+import axios from 'axios';
+import useSWR, { SWRResponse } from 'swr';
+
+import { LsxContext } from '../components/lsx-context';
+import type { PageNode } from '../interfaces/page-node';
+
+function isEquals(path1: string, path2: string) {
+  return pathUtils.removeTrailingSlash(path1) === pathUtils.removeTrailingSlash(path2);
+}
+
+function getParentPath(path: string) {
+  return pathUtils.addTrailingSlash(decodeURIComponent(url.resolve(path, '../')));
+}
+
+/**
+ * generate PageNode instances for target page and the ancestors
+ *
+ * @param {any} pathToNodeMap
+ * @param {any} rootPagePath
+ * @param {any} pagePath
+ * @returns
+ * @memberof Lsx
+ */
+function generatePageNode(pathToNodeMap: Record<string, PageNode>, rootPagePath: string, pagePath: string): PageNode | null {
+  // exclude rootPagePath itself
+  if (isEquals(pagePath, rootPagePath)) {
+    return null;
+  }
+
+  // return when already registered
+  if (pathToNodeMap[pagePath] != null) {
+    return pathToNodeMap[pagePath];
+  }
+
+  // generate node
+  const node = { pagePath, children: [] };
+  pathToNodeMap[pagePath] = node;
+
+  /*
+    * process recursively for ancestors
+    */
+  // get or create parent node
+  const parentPath = getParentPath(pagePath);
+  const parentNode = generatePageNode(pathToNodeMap, rootPagePath, parentPath);
+  // associate to patent
+  if (parentNode != null) {
+    parentNode.children.push(node);
+  }
+
+  return node;
+}
+
+function generatePageNodeTree(rootPagePath: string, pages: IPageHasId[]) {
+  const pathToNodeMap: Record<string, PageNode> = {};
+
+  pages.forEach((page) => {
+    // add slash ensure not to forward match to another page
+    // e.g. '/Java/' not to match to '/JavaScript'
+    const pagePath = pathUtils.addTrailingSlash(page.path);
+
+    const node = generatePageNode(pathToNodeMap, rootPagePath, pagePath); // this will not be null
+
+    // exclude rootPagePath itself
+    if (node == null) {
+      return;
+    }
+
+    // set the Page substance
+    node.page = page;
+  });
+
+  // return root objects
+  const rootNodes: PageNode[] = [];
+  Object.keys(pathToNodeMap).forEach((pagePath) => {
+    // exclude '/'
+    if (pagePath === '/') {
+      return;
+    }
+
+    const parentPath = getParentPath(pagePath);
+
+    // pick up what parent doesn't exist
+    if ((parentPath === '/') || !(parentPath in pathToNodeMap)) {
+      rootNodes.push(pathToNodeMap[pagePath]);
+    }
+  });
+  return rootNodes;
+}
+
+type LsxResponse = {
+  pages: IPageHasId[],
+  toppageViewersCount: number,
+}
+
+const useSWRxLsxResponse = (
+    pagePath: string, options?: Record<string, string | undefined>, isImmutable?: boolean,
+): SWRResponse<LsxResponse, Error> => {
+  return useSWR(
+    ['/_api/plugins/lsx', pagePath, options, isImmutable],
+    (endpoint, pagePath, options) => {
+      return axios.get(endpoint, {
+        params: {
+          pagePath,
+          options,
+        },
+      }).then(result => result.data as LsxResponse);
+    },
+    {
+      revalidateIfStale: !isImmutable,
+      revalidateOnFocus: !isImmutable,
+      revalidateOnReconnect: !isImmutable,
+    },
+  );
+};
+
+type LsxNodeTree = {
+  nodeTree: PageNode[],
+  toppageViewersCount: number,
+}
+
+export const useSWRxNodeTree = (lsxContext: LsxContext, isImmutable?: boolean): SWRResponse<LsxNodeTree, Error> => {
+  const { data, error } = useSWRxLsxResponse(lsxContext.pagePath, lsxContext.options, isImmutable);
+
+  return useSWR(
+    data === undefined ? null : ['lsxNodeTree', lsxContext.pagePath, lsxContext.options, isImmutable, data],
+    (key, pagePath, options, isImmutable, data) => {
+      if (error != null) {
+        throw error;
+      }
+      return {
+        nodeTree: generatePageNodeTree(pagePath, data.pages),
+        toppageViewersCount: data.toppageViewersCount,
+      };
+    },
+    {
+      revalidateIfStale: !isImmutable,
+      revalidateOnFocus: !isImmutable,
+      revalidateOnReconnect: !isImmutable,
+    },
+  );
+};

+ 0 - 0
packages/plugin-lsx/tsconfig.base.json → packages/remark-lsx/tsconfig.base.json


+ 0 - 0
packages/plugin-lsx/tsconfig.build.cjs.json → packages/remark-lsx/tsconfig.build.cjs.json


+ 0 - 0
packages/plugin-lsx/tsconfig.build.esm.json → packages/remark-lsx/tsconfig.build.esm.json


+ 0 - 0
packages/plugin-lsx/tsconfig.json → packages/remark-lsx/tsconfig.json


+ 6 - 174
yarn.lock

@@ -4876,11 +4876,6 @@ JSONStream@^1.0.4, JSONStream@^1.3.5:
     jsonparse "^1.2.0"
     through ">=2.2.7 <3"
 
-a11y-focus-store@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/a11y-focus-store/-/a11y-focus-store-1.0.0.tgz#ae52561cb86ae6c2904c1a4abf2e5820bf5305b0"
-  integrity sha1-rlJWHLhq5sKQTBpKvy5YIL9TBbA=
-
 abbrev@1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@@ -5020,11 +5015,6 @@ align-text@^0.1.1, align-text@^0.1.3:
     longest "^1.0.1"
     repeat-string "^1.5.2"
 
-animation-bus@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/animation-bus/-/animation-bus-0.2.0.tgz#43854c2c9463fb82c664eff0e19b9733081150fa"
-  integrity sha1-Q4VMLJRj+4LGZO/w4ZuXMwgRUPo=
-
 ansi-colors@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
@@ -6011,11 +6001,6 @@ boundary@^1.0.1:
   resolved "https://registry.yarnpkg.com/boundary/-/boundary-1.0.1.tgz#4d67dc2602c0cc16dd9bce7ebf87e948290f5812"
   integrity sha1-TWfcJgLAzBbdm85+v4fpSCkPWBI=
 
-bowser@^1.7.3:
-  version "1.9.4"
-  resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a"
-  integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==
-
 bowser@^2.11.0:
   version "2.11.0"
   resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
@@ -7673,14 +7658,6 @@ crc32-stream@^4.0.2:
     crc-32 "^1.2.0"
     readable-stream "^3.4.0"
 
-create-react-class@^15.5.2:
-  version "15.7.0"
-  resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.7.0.tgz#7499d7ca2e69bb51d13faf59bd04f0c65a1d6c1e"
-  integrity sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==
-  dependencies:
-    loose-envify "^1.3.1"
-    object-assign "^4.1.1"
-
 create-react-context@<=0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.2.tgz#9836542f9aaa22868cd7d4a6f82667df38019dca"
@@ -7758,14 +7735,6 @@ csrf@3.1.0:
     tsscmp "1.0.6"
     uid-safe "2.1.5"
 
-css-in-js-utils@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz#3b472b398787291b47cfe3e44fecfdd9e914ba99"
-  integrity sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==
-  dependencies:
-    hyphenate-style-name "^1.0.2"
-    isobject "^3.0.1"
-
 css-selector-parser@^1.0.0:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.4.1.tgz#03f9cb8a81c3e5ab2c51684557d5aaf6d2569759"
@@ -10663,11 +10632,6 @@ fs.realpath@^1.0.0:
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
-fscreen@^1.0.1:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/fscreen/-/fscreen-1.2.0.tgz#1a8c88e06bc16a07b473ad96196fb06d6657f59e"
-  integrity sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==
-
 fsevents@^1.0.0:
   version "1.2.13"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
@@ -10863,11 +10827,6 @@ get-port@^5.1.1:
   resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
   integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
 
-get-prefix@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/get-prefix/-/get-prefix-1.0.0.tgz#0d305448a4e3176f9c277175b14e16dbe6fba0b5"
-  integrity sha1-DTBUSKTjF2+cJ3F1sU4W2+b7oLU=
-
 get-set-props@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/get-set-props/-/get-set-props-0.1.0.tgz#998475c178445686d0b32246da5df8dbcfbe8ea3"
@@ -10996,14 +10955,6 @@ github-slugger@^1.0.0, github-slugger@^1.1.1:
   resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.4.0.tgz#206eb96cdb22ee56fdc53a28d5a302338463444e"
   integrity sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ==
 
-glam@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/glam/-/glam-5.0.1.tgz#b965a46f1a7f9ba3a23d16430b2e706f63c80ee8"
-  integrity sha512-NCnYcPpefXJMH30LaUfKKP3BkpipI9jkeOvzMZAd76cuDxfKmQRBvgQ1LxXRj9IRZVAwl0K3WQvbw+tiyK2pcw==
-  dependencies:
-    fbjs "^0.8.16"
-    inline-style-prefixer "^3.0.8"
-
 glob-base@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -11912,11 +11863,6 @@ humanize-ms@^1.2.1:
   dependencies:
     ms "^2.0.0"
 
-hyphenate-style-name@^1.0.2:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
-  integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
-
 i18next-chained-backend@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/i18next-chained-backend/-/i18next-chained-backend-4.0.0.tgz#97679ee4b6e04e1ad96e49b3c4ab755ff62238eb"
@@ -12171,14 +12117,6 @@ inline-style-parser@0.1.1:
   resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1"
   integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==
 
-inline-style-prefixer@^3.0.8:
-  version "3.0.8"
-  resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-3.0.8.tgz#8551b8e5b4d573244e66a34b04f7d32076a2b534"
-  integrity sha1-hVG45bTVcyROZqNLBPfTIHaitTQ=
-  dependencies:
-    bowser "^1.7.3"
-    css-in-js-utils "^2.0.0"
-
 inquirer@7.1.0:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29"
@@ -13522,9 +13460,9 @@ jose@^4.1.4:
   integrity sha512-f8E/z+T3Q0kA9txzH2DKvH/ds2uggcw0m3vVPSB9HrSkrQ7mojjifvS7aR8cw+lQl2Fcmx9npwaHpM/M3GD8UQ==
 
 jpeg-js@^0.4.0, jpeg-js@^0.4.2:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b"
-  integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
+  integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==
 
 jquery-slimscroll@^1.3.8:
   version "1.3.8"
@@ -15841,7 +15779,7 @@ misspellings@^1.0.1:
   resolved "https://registry.yarnpkg.com/misspellings/-/misspellings-1.1.0.tgz#53d500266cbd09cda9d94c4cf392e60589b5b324"
   integrity sha1-U9UAJmy9Cc2p2UxM85LmBYm1syQ=
 
-mitt@1.1.3, mitt@^1.1.3:
+mitt@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.1.3.tgz#528c506238a05dce11cd914a741ea2cc332da9b8"
 
@@ -17780,11 +17718,6 @@ penpal@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/penpal/-/penpal-4.0.0.tgz#1cba7a64600c1e601f91dac393c21843c977bdec"
 
-performance-now@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
-  integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=
-
 performance-now@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -18180,7 +18113,7 @@ prop-types@^15.0.0, prop-types@^15.6.2:
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
-prop-types@^15.5.10, prop-types@^15.5.8:
+prop-types@^15.5.8:
   version "15.6.0"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
   dependencies:
@@ -18377,18 +18310,6 @@ quote@0.4.0:
   resolved "https://registry.yarnpkg.com/quote/-/quote-0.4.0.tgz#10839217f6c1362b89194044d29b233fd7f32f01"
   integrity sha1-EIOSF/bBNiuJGUBE0psjP9fzLwE=
 
-raf-schd@^2.1.0:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-2.1.2.tgz#ec622b5167f2912089f054dc03ebd5bcf33c8f62"
-  integrity sha512-Orl0IEvMtUCgPddgSxtxreK77UiQz4nPYJy9RggVzu4mKsZkQWiAaG1y9HlYWdvm9xtN348xRaT37qkvL/+A+g==
-
-raf@^3.1.0:
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
-  integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
-  dependencies:
-    performance-now "^2.1.0"
-
 ramda@0.27.1:
   version "0.27.1"
   resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9"
@@ -18556,16 +18477,6 @@ react-dnd@^14.0.5:
     fast-deep-equal "^3.1.3"
     hoist-non-react-statics "^3.3.2"
 
-react-dom@^16.2.0:
-  version "16.14.0"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
-  integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==
-  dependencies:
-    loose-envify "^1.1.0"
-    object-assign "^4.1.1"
-    prop-types "^15.6.2"
-    scheduler "^0.19.1"
-
 react-dom@^18.2.0:
   version "18.2.0"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@@ -18592,14 +18503,6 @@ react-frame-component@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-4.0.0.tgz#57d51cdb2da3b204cc34577349f9f5bb84a76aac"
 
-react-full-screen@^0.2.2:
-  version "0.2.5"
-  resolved "https://registry.yarnpkg.com/react-full-screen/-/react-full-screen-0.2.5.tgz#bc79a5cdb9640d8b9b09e11a17fa54f6e6fa5789"
-  integrity sha512-LNkxjLWmiR+AwemSVdn/miUcBy8tHA6mDVS1qz1AM/DHNEtQbzkh5ok9A6g99502OqutQq1zBvCBGLV8rsB2tw==
-  dependencies:
-    "@types/react" "*"
-    fscreen "^1.0.1"
-
 react-hotkeys@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-2.0.0.tgz#a7719c7340cbba888b0e9184f806a9ec0ac2c53f"
@@ -18624,21 +18527,6 @@ react-image-crop@^8.3.0:
     core-js "^3.2.1"
     prop-types "^15.7.2"
 
-react-images@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/react-images/-/react-images-1.0.0.tgz#9dcca9e91137c9ad175ba832a965ca1141e6aea8"
-  integrity sha512-IC9cXdPRSUqqFcr9kzHKv4yQSqvJGoiiHYdzc1xQmVMomtktow+WPG7s6UDWZQLt3q8EJ59Qmg6yJYnkqWs0Jg==
-  dependencies:
-    a11y-focus-store "^1.0.0"
-    glam "^5.0.1"
-    raf-schd "^2.1.0"
-    react "^16.2.0"
-    react-dom "^16.2.0"
-    react-full-screen "^0.2.2"
-    react-scrolllock "^1.0.9"
-    react-transition-group "^2.2.1"
-    react-view-pager "^0.6.0"
-
 react-is@^16.13.1, react-is@^16.8.1:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -18685,15 +18573,6 @@ react-markdown@^8.0.3:
     unist-util-visit "^4.0.0"
     vfile "^5.0.0"
 
-react-motion@^0.5.0, react-motion@^0.5.2:
-  version "0.5.2"
-  resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
-  integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==
-  dependencies:
-    performance-now "^0.2.0"
-    prop-types "^15.5.8"
-    raf "^3.1.0"
-
 react-multiline-clamp@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/react-multiline-clamp/-/react-multiline-clamp-2.0.0.tgz#913a2092368ef1b52c1c79364d506ba4af27e019"
@@ -18758,14 +18637,6 @@ react-scroll@^1.8.7:
     lodash.throttle "^4.1.1"
     prop-types "^15.7.2"
 
-react-scrolllock@^1.0.9:
-  version "1.0.9"
-  resolved "https://registry.yarnpkg.com/react-scrolllock/-/react-scrolllock-1.0.9.tgz#7c9c3c0cce2ed55042af2808b6483b85b121cdcb"
-  integrity sha1-fJw8DM4u1VBCrygItkg7hbEhzcs=
-  dependencies:
-    create-react-class "^15.5.2"
-    prop-types "^15.5.10"
-
 react-syntax-highlighter@^15.5.0:
   version "15.5.0"
   resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20"
@@ -18777,7 +18648,7 @@ react-syntax-highlighter@^15.5.0:
     prismjs "^1.27.0"
     refractor "^3.6.0"
 
-react-transition-group@^2.2.1, react-transition-group@^2.3.1:
+react-transition-group@^2.3.1:
   version "2.9.0"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
   integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
@@ -18792,18 +18663,6 @@ react-use-ripple@^1.5.2:
   resolved "https://registry.yarnpkg.com/react-use-ripple/-/react-use-ripple-1.5.2.tgz#f42600a0c7729510c3dbba74e0c86ed6c55fd88e"
   integrity sha512-pK7PLEaEGJ4xCM5acxW+ua7ba0lqxbhNzBHzEw+MoD0yVFT3r8SkfkG6aSpiEm4iLZO9HOeSnUz+1k7YVuYX5w==
 
-react-view-pager@^0.6.0:
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/react-view-pager/-/react-view-pager-0.6.0.tgz#6c6be04b0cc3b907b5ceafec7b2ab6e7228df650"
-  integrity sha512-nV6VTLyHmv4T9QszZVD3sRn3EcUKgb2NhSdz9kjTIpzE+SwOl4mfcQtqUwc6St3EnMtus805zVJ8OcSjFEqhpg==
-  dependencies:
-    animation-bus "^0.2.0"
-    get-prefix "^1.0.0"
-    mitt "1.1.3"
-    react-motion "^0.5.0"
-    resize-observer-polyfill "1.5.0"
-    tabbable "1.1.2"
-
 react-waypoint@^10.1.0:
   version "10.1.0"
   resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-10.1.0.tgz#6ab522a61bd52946260e4a78b3182759a97b40ec"
@@ -18814,15 +18673,6 @@ react-waypoint@^10.1.0:
     prop-types "^15.0.0"
     react-is "^17.0.1"
 
-react@^16.2.0:
-  version "16.14.0"
-  resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
-  integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
-  dependencies:
-    loose-envify "^1.1.0"
-    object-assign "^4.1.1"
-    prop-types "^15.6.2"
-
 react@^18.2.0:
   version "18.2.0"
   resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@@ -20566,11 +20416,6 @@ requires-port@^1.0.0:
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
   integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
 
-resize-observer-polyfill@1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.0.tgz#660ff1d9712a2382baa2cad450a4716209f9ca69"
-  integrity sha512-M2AelyJDVR/oLnToJLtuDJRBBWUGUvvGigj1411hXhAdyFWqMaqHp7TixW3FpiLuVaikIcR1QL+zqoJoZlOgpg==
-
 resolve-cwd@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
@@ -21004,14 +20849,6 @@ sax@>=0.6.0, sax@^1.2.4:
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
 
-scheduler@^0.19.1:
-  version "0.19.1"
-  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
-  integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
-  dependencies:
-    loose-envify "^1.1.0"
-    object-assign "^4.1.1"
-
 scheduler@^0.23.0:
   version "0.23.0"
   resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
@@ -22564,11 +22401,6 @@ synckit@^0.7.2:
     "@pkgr/utils" "^2.2.0"
     tslib "^2.4.0"
 
-tabbable@1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.2.tgz#b171680aea6e0a3e9281ff23532e2e5de11c0d94"
-  integrity sha512-77oqsKEPrxIwgRcXUwipkj9W5ItO97L6eUT1Ar7vh+El16Zm4M6V+YU1cbipHEa6q0Yjw8O3Hoh8oRgatV5s7A==
-
 table@^5.2.3:
   version "5.4.6"
   resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"