Sfoglia il codice sorgente

Merge branch 'imprv/115672-115674-presentation-preview' into feat/115673-marp

reiji-h 2 anni fa
parent
commit
2df13584d5
93 ha cambiato i file con 1904 aggiunte e 725 eliminazioni
  1. 2 2
      .devcontainer/Dockerfile
  2. 1 1
      .github/workflows/reusable-app-prod.yml
  3. 44 1
      CHANGELOG.md
  4. 1 1
      apps/app/cypress.config.ts
  5. 1 0
      apps/app/docker/Dockerfile
  6. 0 1
      apps/app/docker/Dockerfile.dockerignore
  7. 1 0
      apps/app/next.config.js
  8. 8 5
      apps/app/package.json
  9. 8 0
      apps/app/src/client/services/renderer/renderer.tsx
  10. 4 1
      apps/app/src/components/Admin/UserGroup/UserGroupForm.tsx
  11. 1 1
      apps/app/src/components/Page/PageView.tsx
  12. 3 66
      apps/app/src/components/Page/RevisionRenderer.tsx
  13. 15 2
      apps/app/src/components/PageEditor.tsx
  14. 1 1
      apps/app/src/components/PageEditor/Preview.tsx
  15. 33 0
      apps/app/src/components/ReactMarkdownComponents/SlideViewer.tsx
  16. 7 0
      apps/app/src/components/TemplateModal/TemplateModal.module.scss
  17. 174 39
      apps/app/src/components/TemplateModal/TemplateModal.tsx
  18. 1 1
      apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginInstallerForm.tsx
  19. 13 2
      apps/app/src/features/growi-plugin/interfaces/growi-plugin.ts
  20. 5 0
      apps/app/src/features/growi-plugin/server/consts/index.ts
  21. 157 0
      apps/app/src/features/growi-plugin/server/models/growi-plugin.integ.ts
  22. 15 25
      apps/app/src/features/growi-plugin/server/models/growi-plugin.ts
  23. 4 2
      apps/app/src/features/growi-plugin/server/models/vo/github-url.ts
  24. 11 0
      apps/app/src/features/growi-plugin/server/services/growi-plugin/generate-template-plugin-meta.ts
  25. 12 0
      apps/app/src/features/growi-plugin/server/services/growi-plugin/generate-theme-plugin-meta.ts
  26. 94 0
      apps/app/src/features/growi-plugin/server/services/growi-plugin/growi-plugin.integ.ts
  27. 87 68
      apps/app/src/features/growi-plugin/server/services/growi-plugin/growi-plugin.ts
  28. 1 0
      apps/app/src/features/growi-plugin/server/services/growi-plugin/index.ts
  29. 62 11
      apps/app/src/features/templates/server/routes/apiv3/index.ts
  30. 4 3
      apps/app/src/features/templates/stores/template.tsx
  31. 5 3
      apps/app/src/pages/[[...path]].page.tsx
  32. 2 1
      apps/app/src/server/crowi/express-init.js
  33. 1 1
      apps/app/src/server/routes/apiv3/bookmark-folder.ts
  34. 1 1
      apps/app/src/server/routes/apiv3/customize-setting.js
  35. 1 1
      apps/app/src/server/routes/apiv3/users.js
  36. 19 5
      apps/app/src/server/service/search-delegator/elasticsearch-client.ts
  37. 6 6
      apps/app/src/server/service/search-delegator/elasticsearch.ts
  38. 1 1
      apps/app/src/server/util/mongoose-utils.ts
  39. 2 2
      apps/app/src/services/renderer/remark-plugins/attachment.ts
  40. 70 0
      apps/app/src/services/renderer/remark-plugins/slides.ts
  41. 1 1
      apps/app/src/stores/modal.tsx
  42. 10 10
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts
  43. 1 0
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--sticky-features.cy.ts
  44. 90 0
      apps/app/test/cypress/e2e/23-editor/23-editor--template-modal.cy.ts
  45. 6 0
      apps/app/test/cypress/e2e/23-editor/23-editor--with-navigation.cy.ts
  46. 1 1
      apps/app/tsconfig.json
  47. 2 2
      apps/slackbot-proxy/package.json
  48. 2 3
      package.json
  49. 2 2
      packages/core/package.json
  50. 0 5
      packages/core/src/interfaces/growi-facade.ts
  51. 0 1
      packages/core/src/interfaces/index.ts
  52. 0 9
      packages/core/src/interfaces/template.ts
  53. 2 2
      packages/hackmd/package.json
  54. 1 1
      packages/pluginkit/package.json
  55. 12 0
      packages/pluginkit/src/model/growi-plugin-package-data.ts
  56. 4 1
      packages/pluginkit/src/model/growi-plugin-validation-data.ts
  57. 1 1
      packages/pluginkit/src/model/growi-plugin-validation-error.ts
  58. 1 0
      packages/pluginkit/src/model/index.ts
  59. 11 0
      packages/pluginkit/src/v4/server/utils/common/import-package-json.spec.ts
  60. 9 0
      packages/pluginkit/src/v4/server/utils/common/import-package-json.ts
  61. 2 0
      packages/pluginkit/src/v4/server/utils/common/index.ts
  62. 117 0
      packages/pluginkit/src/v4/server/utils/common/validate-growi-plugin-directive.spec.ts
  63. 6 6
      packages/pluginkit/src/v4/server/utils/common/validate-growi-plugin-directive.ts
  64. 1 1
      packages/pluginkit/src/v4/server/utils/index.ts
  65. 0 11
      packages/pluginkit/src/v4/server/utils/package-json/import.spec.ts
  66. 0 6
      packages/pluginkit/src/v4/server/utils/package-json/import.ts
  67. 0 2
      packages/pluginkit/src/v4/server/utils/package-json/index.ts
  68. 0 117
      packages/pluginkit/src/v4/server/utils/package-json/validate.spec.ts
  69. 0 205
      packages/pluginkit/src/v4/server/utils/template.ts
  70. 16 0
      packages/pluginkit/src/v4/server/utils/template/get-markdown.ts
  71. 29 0
      packages/pluginkit/src/v4/server/utils/template/get-status.ts
  72. 4 0
      packages/pluginkit/src/v4/server/utils/template/index.ts
  73. 103 0
      packages/pluginkit/src/v4/server/utils/template/scan.ts
  74. 36 0
      packages/pluginkit/src/v4/server/utils/template/validate-all-locales.ts
  75. 33 0
      packages/pluginkit/src/v4/server/utils/template/validate-growi-plugin-directive.ts
  76. 9 1
      packages/pluginkit/src/v4/utils/template.ts
  77. 1 7
      packages/pluginkit/tsconfig.json
  78. 0 2
      packages/pluginkit/vite.config.ts
  79. 2 2
      packages/presentation/package.json
  80. 1 1
      packages/preset-templates/package.json
  81. 24 7
      packages/preset-templates/test/index.test.ts
  82. 1 1
      packages/preset-themes/package.json
  83. 2 2
      packages/remark-attachment-refs/package.json
  84. 2 2
      packages/remark-drawio/package.json
  85. 2 2
      packages/remark-growi-directive/package.json
  86. 2 2
      packages/remark-lsx/package.json
  87. 1 1
      packages/remark-lsx/src/server/routes/list-pages/index.ts
  88. 0 1
      packages/remark-lsx/tsconfig.json
  89. 2 2
      packages/slack/package.json
  90. 2 2
      packages/ui/package.json
  91. 1 1
      packages/ui/tsconfig.json
  92. 0 3
      tsconfig.base.json
  93. 471 45
      yarn.lock

+ 2 - 2
.devcontainer/Dockerfile

@@ -39,9 +39,9 @@ RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-ke
 RUN apt-get update \
     && apt-get -y install --no-install-recommends git-lfs \
 
-    # Uncomment below lines to install Chrome and libs for Cypress
+    # Uncomment below lines to install Chromium
     # --- works only on AMD64 ---
-    # && apt-get -y install --no-install-recommends google-chrome-stable \
+    # && apt-get -y install --no-install-recommends chromium \
     #    libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb fonts-noto-cjk \
 
     # Clean up

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

@@ -312,7 +312,7 @@ jobs:
     - name: Cypress Run
       uses: cypress-io/github-action@v5
       with:
-        browser: chrome
+        browser: chromium
         working-directory: ./apps/app
         spec: '${{ steps.determine-spec-exp.outputs.value }}'
         install: false

+ 44 - 1
CHANGELOG.md

@@ -1,9 +1,52 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.1.4...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.1.6...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.1.6](https://github.com/weseek/growi/compare/v6.1.5...v6.1.6) - 2023-07-12
+
+### 🐛 Bug Fixes
+
+- fix: Revert current page mutation and add workaround for saving page (#7877) @yuki-takei
+- fix: The official docker image missed preset-templates (#7865) @yuki-takei
+- fix: SSL connection error to Elasticsearch8 using self certificate (#7818) @miya
+
+## [v6.1.5](https://github.com/weseek/growi/compare/v6.1.4...v6.1.5) - 2023-07-10
+
+### 💎 Features
+
+- feat: Rich Attachment (#7534) @jam411
+- feat: Plugin kit (#7830) @yuki-takei
+- feat: Deciding whether to use SSR based on the volume of latestRevisionBodyLength (#7772) @miya
+
+### 🚀 Improvement
+
+- imprv: Load templates from the server 2 (#7850) @yuki-takei
+- imprv: Improve release parent group button (#7838) @WNomunomu
+- imprv: Load templates from the server (#7842) @yuki-takei
+- imprv: Able to send new passsword by email (#7758) @soumaeda
+- imprv: Convert jsx into tsx (#7832) @WNomunomu
+- imprv: After reset password footer modal design (#7790) @soumaeda
+- imprv: Update email alert (#7771) @WNomunomu
+- imprv: Can use normal browser transition in searching page (#7826) @yuki-takei
+- imprv: Show tooltip when copying password (#7800) @soumaeda
+
+### 🐛 Bug Fixes
+
+- fix(lsx): Except option (#7855) @yuki-takei
+- fix: Page body is not displayed when skipSSR (#7849) @miya
+- fix: When uploading an attachment file to a new page and pressing the update button, an error occurs (#7844) @miya
+- fix: Editing user group settings (#7827) @WNomunomu
+- fix: Handsontable not display full screen (#7784) @mudana-grune
+- fix: Brand logo fill color transition (#7828) @yuki-takei
+- fix: Email body of global notification is not displayed (#7824) @jam411
+- fix(lsx): Prefix is not uniquely determined by usage (#7815) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: Dependencies specification for local packages (#7809) @yuki-takei
+
 ## [v6.1.4](https://github.com/weseek/growi/compare/v6.1.3...v6.1.4) - 2023-06-12
 
 ### 💎 Features

+ 1 - 1
apps/app/cypress.config.ts

@@ -9,7 +9,7 @@ export default defineConfig({
       // change screen size
       // see: https://docs.cypress.io/api/plugins/browser-launch-api#Set-screen-size-when-running-headless
       on('before:browser:launch', (browser, launchOptions) => {
-        if (browser.name === 'chrome' && browser.isHeadless) {
+        if (browser.name === 'chromium' && browser.isHeadless) {
           launchOptions.args.push('--window-size=1400,1024');
           launchOptions.args.push('--force-device-scale-factor=1');
         }

+ 1 - 0
apps/app/docker/Dockerfile

@@ -83,6 +83,7 @@ RUN tar -xf node_modules.tar
 RUN rm node_modules.tar
 
 # build
+RUN turbo run clean
 RUN turbo run build
 
 # make artifacts

+ 0 - 1
apps/app/docker/Dockerfile.dockerignore

@@ -1,5 +1,4 @@
 **/node_modules
-**/dist
 **/coverage
 **/Dockerfile
 **/*.dockerignore

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

@@ -26,6 +26,7 @@ const getTranspilePackages = () => {
     'character-entities-legacy',
     'comma-separated-tokens',
     'decode-named-character-reference',
+    'devlop',
     'escape-string-regexp',
     'hastscript',
     'html-void-elements',

+ 8 - 5
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.1.5-RC.0",
+  "version": "6.1.7-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -8,8 +8,8 @@
     "start": "yarn next start",
     "build:client": "yarn next build",
     "build:server": "yarn cross-env NODE_ENV=production tsc -p tsconfig.build.server.json && tsc-alias -p tsconfig.build.server-tsc-alias.json",
-    "postbuild:server": "npx -y shx echo \"Listing files under transpiled\" && npx -y shx ls transpiled && npx -y shx rm -rf dist && npx -y shx mv transpiled/src dist && npx -y shx rm -rf transpiled",
-    "clean": "npx -y shx rm -rf dist transpiled",
+    "postbuild:server": "shx echo \"Listing files under transpiled\" && shx ls transpiled && shx rm -rf dist && shx mv transpiled/src dist && shx rm -rf transpiled",
+    "clean": "shx rm -rf dist transpiled",
     "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
     "server:ci": "yarn server --ci",
     "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
@@ -24,7 +24,7 @@
     "dev:migrate:status": "yarn dev:migrate-mongo status -f config/migrate-mongo-config.js",
     "dev:migrate:up": "yarn dev:migrate-mongo up -f config/migrate-mongo-config.js",
     "dev:migrate:down": "yarn dev:migrate-mongo down -f config/migrate-mongo-config.js",
-    "cy:run": "cypress run --browser chrome",
+    "cy:run": "cypress run --browser chromium",
     "//// for CI": "",
     "dev:ci": "yarn cross-env NODE_ENV=development yarn ts-node src/server/app.ts --ci",
     "lint:typecheck": "npx -y tsc",
@@ -127,6 +127,9 @@
     "lucene-query-parser": "^1.2.0",
     "markdown-table": "^1.1.1",
     "md5": "^2.2.1",
+    "mdast-util-frontmatter": "1.0.1",
+    "mdast-util-gfm": "^3.0.0",
+    "mdast-util-to-markdown": "^1.5.0",
     "mermaid": "^10.1.0",
     "method-override": "^3.0.0",
     "migrate-mongo": "^8.2.3",
@@ -189,7 +192,7 @@
     "remark-math": "^5.1.1",
     "remark-toc": "^8.0.1",
     "remark-wiki-link": "^1.0.4",
-    "remark-frontmatter": "^4.0.1",
+    "sanitize-filename": "^1.6.3",
     "socket.io": "^4.2.0",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",

+ 8 - 0
apps/app/src/client/services/renderer/renderer.tsx

@@ -17,6 +17,7 @@ import type { Pluggable } from 'unified';
 import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents/DrawioViewerWithEditButton';
 import { Header } from '~/components/ReactMarkdownComponents/Header';
 import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
+import { SlideViewer } from '~/components/ReactMarkdownComponents/SlideViewer';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import * as mermaid from '~/features/mermaid';
 import { RehypeSanitizeOption } from '~/interfaces/rehype';
@@ -27,6 +28,7 @@ import * as keywordHighlighter from '~/services/renderer/rehype-plugins/keyword-
 import * as relocateToc from '~/services/renderer/rehype-plugins/relocate-toc';
 import * as attachment from '~/services/renderer/remark-plugins/attachment';
 import * as plantuml from '~/services/renderer/remark-plugins/plantuml';
+import * as slides from '~/services/renderer/remark-plugins/slides';
 import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
 import {
   commonSanitizeOption, generateCommonOptions, injectCustomSanitizeOption, verifySanitizePlugin,
@@ -63,6 +65,7 @@ export const generateViewOptions = (
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
     attachment.remarkPlugin,
+    slides.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
   );
@@ -80,6 +83,7 @@ export const generateViewOptions = (
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
       attachment.sanitizeOption,
+      slides.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
     )]
@@ -113,6 +117,7 @@ export const generateViewOptions = (
     components.table = TableWithEditButton;
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
+    components.slide = SlideViewer;
   }
 
   if (config.isEnabledXssPrevention) {
@@ -254,6 +259,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     attachment.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
+    slides.remarkPlugin,
   );
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);
@@ -272,6 +278,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
       addLineNumberAttribute.sanitizeOption,
+      slides.sanitizeOption,
     )]
     : () => {};
 
@@ -295,6 +302,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     components.drawio = drawio.DrawioViewer;
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
+    components.slide = SlideViewer;
   }
 
   if (config.isEnabledXssPrevention) {

+ 4 - 1
apps/app/src/components/Admin/UserGroup/UserGroupForm.tsx

@@ -45,6 +45,9 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
 
   const isSelectableParentUserGroups = selectableParentUserGroups != null && selectableParentUserGroups.length > 0;
 
+  const isChildUserGroup = parentUserGroup !== undefined;
+  const messageAtReleaseParentGroup = isChildUserGroup ? t('user_group_management.release_parent_group') : t('user_group_management.select_parent_group');
+
   return (
     <form onSubmit={(e) => {
       e.preventDefault();
@@ -107,7 +110,7 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
                 btn btn-outline-secondary dropdown-toggle mb-3 ${isSelectableParentUserGroups ? '' : 'disabled'}
               `}
             >
-              {selectedParent?.name ?? t('user_group_management.select_parent_group')}
+              {selectedParent?.name ?? messageAtReleaseParentGroup}
             </button>
             <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
               {

+ 1 - 1
apps/app/src/components/Page/PageView.tsx

@@ -132,7 +132,7 @@ export const PageView = (props: Props): JSX.Element => {
     return (
       <>
         <PageContentsUtilities />
-        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} isSlidesOverviewEnabled/>
+        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
       </>
     );
   };

+ 3 - 66
apps/app/src/components/Page/RevisionRenderer.tsx

@@ -1,15 +1,7 @@
-import React, { useEffect, useState } from 'react';
+import React from 'react';
 
-import dynamic from 'next/dynamic';
 import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
 import ReactMarkdown from 'react-markdown';
-import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
-import remarkFrontmatter from 'remark-frontmatter';
-import remarkParse from 'remark-parse';
-import remarkStringify from 'remark-stringify';
-import { unified } from 'unified';
-import { visit } from 'unist-util-visit';
-
 
 import type { RendererOptions } from '~/interfaces/renderer-options';
 import loggerFactory from '~/utils/logger';
@@ -19,12 +11,10 @@ import 'katex/dist/katex.min.css';
 
 const logger = loggerFactory('components:Page:RevisionRenderer');
 
-
 type Props = {
   rendererOptions: RendererOptions,
   markdown: string,
   additionalClassName?: string,
-  isSlidesOverviewEnabled?: boolean,
 }
 
 const ErrorFallback: React.FC<FallbackProps> = React.memo(({ error, resetErrorBoundary }) => {
@@ -38,65 +28,12 @@ const ErrorFallback: React.FC<FallbackProps> = React.memo(({ error, resetErrorBo
 });
 ErrorFallback.displayName = 'ErrorFallback';
 
-const Slides = dynamic(() => import('@growi/presentation').then(mod => mod.Slides), { ssr: false });
-
 const RevisionRenderer = React.memo((props: Props): JSX.Element => {
 
   const {
-    rendererOptions, markdown, additionalClassName, isSlidesOverviewEnabled,
+    rendererOptions, markdown, additionalClassName,
   } = props;
 
-  const [hasSlideFlag, setHasSlideFlag] = useState<boolean>();
-  const [hasMarpFlag, setHasMarpFlag] = useState<boolean>();
-
-  // use useEffect to avoid ssr
-  useEffect(() => {
-    if (isSlidesOverviewEnabled) {
-      const processMarkdown = () => (tree) => {
-        setHasSlideFlag(false);
-        setHasMarpFlag(false);
-        visit(tree, 'yaml', (node) => {
-          if (node.value != null) {
-            const lines = node.value.split('\n');
-
-            lines.forEach((line) => {
-              const [key, value] = line.split(':').map(part => part.trim());
-
-              if (key === 'slide' && value === 'true') {
-                setHasSlideFlag(true);
-              }
-              else if (key === 'marp' && value === 'true') {
-                setHasMarpFlag(true);
-              }
-            });
-          }
-        });
-      };
-
-      unified()
-        .use(remarkParse)
-        .use(remarkStringify)
-        .use(remarkFrontmatter, ['yaml'])
-        .use(processMarkdown)
-        .process(markdown);
-    }
-  }, [markdown, setHasSlideFlag, setHasMarpFlag, isSlidesOverviewEnabled]);
-
-  if (isSlidesOverviewEnabled && (hasSlideFlag || hasMarpFlag)) {
-    const options = {
-      rendererOptions: rendererOptions as ReactMarkdownOptions,
-      isDarkMode: false,
-      disableSeparationsByHeader: false,
-      hasMarpFlag,
-    };
-    return (
-      <Slides
-        options={options}
-        hasMarpFlag={hasMarpFlag}
-      >{markdown}</Slides>
-    );
-  }
-
   return (
     <ErrorBoundary FallbackComponent={ErrorFallback}>
       <ReactMarkdown
@@ -107,8 +44,8 @@ const RevisionRenderer = React.memo((props: Props): JSX.Element => {
       </ReactMarkdown>
     </ErrorBoundary>
   );
-});
 
+});
 RevisionRenderer.displayName = 'RevisionRenderer';
 
 export default RevisionRenderer;

+ 15 - 2
apps/app/src/components/PageEditor.tsx

@@ -83,7 +83,7 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPage } = useSWRxCurrentPage();
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
-  const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
+  const { data: grantData } = useSelectedGrant();
   const { data: pageTags, sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
   const { data: editingMarkdown, mutate: mutateEditingMarkdown } = useEditingMarkdown();
@@ -110,7 +110,13 @@ const PageEditor = React.memo((): JSX.Element => {
 
   const updateStateAfterSave = useUpdateStateAfterSave(pageId, { supressEditingMarkdownMutation: true });
 
-  const currentRevisionId = currentPage?.revision?._id;
+  // TODO: remove workaround
+  // for https://redmine.weseek.co.jp/issues/125923
+  const [createdPageRevisionIdWithAttachment, setCreatedPageRevisionIdWithAttachment] = useState('');
+
+  // TODO: remove workaround
+  // for https://redmine.weseek.co.jp/issues/125923
+  const currentRevisionId = currentPage?.revision?._id ?? createdPageRevisionIdWithAttachment;
 
   const initialValue = useMemo(() => {
     if (!isNotFound) {
@@ -149,6 +155,12 @@ const PageEditor = React.memo((): JSX.Element => {
 
   }, [markdownToPreview, mutateIsConflict]);
 
+  // TODO: remove workaround
+  // for https://redmine.weseek.co.jp/issues/125923
+  useEffect(() => {
+    setCreatedPageRevisionIdWithAttachment('');
+  }, [router]);
+
   useEffect(() => {
     markdownToSave.current = initialValue;
     setMarkdownToPreview(initialValue);
@@ -327,6 +339,7 @@ const PageEditor = React.memo((): JSX.Element => {
         logger.info('Page is created', res.page._id);
         globalEmitter.emit('resetInitializedHackMdStatus');
         mutateIsLatestRevision(true);
+        setCreatedPageRevisionIdWithAttachment(res.page.revision);
         await mutateCurrentPageId(res.page._id);
         await mutateCurrentPage();
       }

+ 1 - 1
apps/app/src/components/PageEditor/Preview.tsx

@@ -32,7 +32,7 @@ const Preview = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>):
       }}
     >
       { markdown != null && (
-        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} isSlidesOverviewEnabled></RevisionRenderer>
+        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown}></RevisionRenderer>
       ) }
     </div>
   );

+ 33 - 0
apps/app/src/components/ReactMarkdownComponents/SlideViewer.tsx

@@ -0,0 +1,33 @@
+import React from 'react';
+
+import dynamic from 'next/dynamic';
+import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+
+import { usePresentationViewOptions } from '~/stores/renderer';
+
+
+const Slides = dynamic(() => import('@growi/presentation').then(mod => mod.Slides), { ssr: false });
+
+type SlideViewerProps = {
+  marp: string,
+  children: string,
+}
+
+export const SlideViewer: React.FC<SlideViewerProps> = React.memo((props: SlideViewerProps) => {
+  const {
+    marp, children,
+  } = props;
+
+  const { data: rendererOptions } = usePresentationViewOptions();
+
+  return (
+    <Slides
+      hasMarpFlag={marp === 'marp'}
+      options={{ rendererOptions: rendererOptions as ReactMarkdownOptions }}
+    >
+      {children}
+    </Slides>
+  );
+});
+
+SlideViewer.displayName = 'SlideViewer';

+ 7 - 0
apps/app/src/components/TemplateModal/TemplateModal.module.scss

@@ -0,0 +1,7 @@
+@use '~/styles/bootstrap/init' as bs;
+
+.dm-templates :global {
+  .dropdown-item:not(:first-child) {
+    border-top: 1px solid bs.$border-color;
+  }
+}

+ 174 - 39
apps/app/src/components/TemplateModal/TemplateModal.tsx

@@ -5,17 +5,23 @@ import React, {
 import assert from 'assert';
 
 import { Lang } from '@growi/core';
-import type { TemplateSummary } from '@growi/pluginkit/dist/v4';
+import {
+  extractSupportedLocales, getLocalizedTemplate, type TemplateSummary,
+} from '@growi/pluginkit/dist/v4';
 import { useTranslation } from 'next-i18next';
 import {
   Modal,
   ModalHeader,
   ModalBody,
   ModalFooter,
+  UncontrolledDropdown,
+  DropdownToggle,
+  DropdownMenu,
+  DropdownItem,
 } from 'reactstrap';
 
 import { useSWRxTemplate, useSWRxTemplates } from '~/features/templates/stores';
-import { useTemplateModal } from '~/stores/modal';
+import { useTemplateModal, type TemplateModalStatus } from '~/stores/modal';
 import { usePersonalSettings } from '~/stores/personal-settings';
 import { usePreviewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
@@ -24,6 +30,10 @@ import Preview from '../PageEditor/Preview';
 
 import { useFormatter } from './use-formatter';
 
+
+import styles from './TemplateModal.module.scss';
+
+
 const logger = loggerFactory('growi:components:TemplateModal');
 
 
@@ -33,51 +43,79 @@ function constructTemplateId(templateSummary: TemplateSummary): string {
   return `${defaultTemplate.pluginId ?? ''}_${defaultTemplate.id}`;
 }
 
-
-type TemplateRadioButtonProps = {
+type TemplateSummaryItemProps = {
   templateSummary: TemplateSummary,
+  selectedLocale?: string,
   onClick?: () => void,
-  usersDefaultLang?: Lang,
   isSelected?: boolean,
+  usersDefaultLang?: Lang,
 }
 
-const TemplateListGroupItem = ({
-  templateSummary, onClick, usersDefaultLang, isSelected,
-}: TemplateRadioButtonProps): JSX.Element => {
-  const templateId = constructTemplateId(templateSummary);
-  const locales = new Set(Object.values(templateSummary).map(s => s.locale));
-
-  const template = usersDefaultLang != null && usersDefaultLang in templateSummary
-    ? templateSummary[usersDefaultLang]
-    : templateSummary.default;
+const TemplateListGroupItem: React.FC<TemplateSummaryItemProps> = ({
+  templateSummary,
+  onClick,
+  isSelected,
+  usersDefaultLang,
+}) => {
+  const localizedTemplate = getLocalizedTemplate(templateSummary, usersDefaultLang);
+  const templateLocales = extractSupportedLocales(templateSummary);
 
-  assert(template.isValid);
+  assert(localizedTemplate?.isValid);
 
   return (
     <a
-      key={templateId}
       className={`list-group-item list-group-item-action ${isSelected ? 'active' : ''}`}
       onClick={onClick}
       aria-current="true"
     >
-      <h4 className="mb-1">{template.title}</h4>
-      <p className="mb-2">{template.desc}</p>
-      { Array.from(locales).map(locale => (
+      <h4 className="mb-1">{localizedTemplate.title}</h4>
+      <p className="mb-2">{localizedTemplate.desc}</p>
+      { templateLocales != null && Array.from(templateLocales).map(locale => (
         <span key={locale} className="badge border rounded-pill text-muted mr-1">{locale}</span>
       ))}
     </a>
   );
 };
 
-export const TemplateModal = (): JSX.Element => {
-  const { t } = useTranslation(['translation', 'commons']);
 
+const TemplateDropdownItem: React.FC<TemplateSummaryItemProps> = ({
+  templateSummary,
+  onClick,
+  usersDefaultLang,
+}) => {
 
-  const { data: templateModalStatus, close } = useTemplateModal();
+  const localizedTemplate = getLocalizedTemplate(templateSummary, usersDefaultLang);
+  const templateLocales = extractSupportedLocales(templateSummary);
+
+  assert(localizedTemplate?.isValid);
+
+  return (
+    <DropdownItem
+      onClick={onClick}
+      className="px-4 py-3"
+    >
+      <h4 className="mb-1 text-wrap">{localizedTemplate.title}</h4>
+      <p className="mb-1 text-wrap">{localizedTemplate.desc}</p>
+      { templateLocales != null && Array.from(templateLocales).map(locale => (
+        <span key={locale} className="badge border rounded-pill text-muted mr-1">{locale}</span>
+      ))}
+    </DropdownItem>
+  );
+};
+
+type TemplateModalSubstanceProps = {
+  templateModalStatus: TemplateModalStatus,
+  close: () => void,
+}
+
+const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element => {
+  const { templateModalStatus, close } = props;
+
+  const { t } = useTranslation(['translation', 'commons']);
 
   const { data: personalSettingsInfo } = usePersonalSettings();
   const { data: rendererOptions } = usePreviewOptions();
-  const { data: templateSummaries } = useSWRxTemplates();
+  const { data: templateSummaries, isLoading } = useSWRxTemplates();
 
   const [selectedTemplateSummary, setSelectedTemplateSummary] = useState<TemplateSummary>();
   const [selectedTemplateLocale, setSelectedTemplateLocale] = useState<string>();
@@ -86,8 +124,12 @@ export const TemplateModal = (): JSX.Element => {
 
   const { format } = useFormatter();
 
+  const usersDefaultLang = personalSettingsInfo?.lang;
+  const selectedLocalizedTemplate = getLocalizedTemplate(selectedTemplateSummary, usersDefaultLang);
+  const selectedTemplateLocales = extractSupportedLocales(selectedTemplateSummary);
+
   const submitHandler = useCallback((markdown?: string) => {
-    if (templateModalStatus == null || markdown == null) {
+    if (markdown == null) {
       return;
     }
 
@@ -100,44 +142,121 @@ export const TemplateModal = (): JSX.Element => {
     close();
   }, [close, format, selectedTemplateMarkdown, templateModalStatus]);
 
+  const onClickHandler = useCallback((
+      templateSummary: TemplateSummary,
+  ) => {
+    let localeToSet: string | Lang | undefined;
+
+    if (selectedTemplateLocale != null && selectedTemplateLocale in templateSummary) {
+      localeToSet = selectedTemplateLocale;
+    }
+    else if (usersDefaultLang != null && usersDefaultLang in templateSummary) {
+      localeToSet = usersDefaultLang;
+    }
+    else {
+      localeToSet = undefined;
+    }
+
+    setSelectedTemplateLocale(localeToSet);
+    setSelectedTemplateSummary(templateSummary);
+  }, [selectedTemplateLocale, usersDefaultLang]);
+
   useEffect(() => {
-    if (!templateModalStatus?.isOpened) {
+    if (!templateModalStatus.isOpened) {
       setSelectedTemplateSummary(undefined);
+      setSelectedTemplateLocale(undefined);
     }
-  }, [templateModalStatus?.isOpened]);
-
-  if (templateSummaries == null || templateModalStatus == null) {
-    return <></>;
-  }
+  }, [templateModalStatus.isOpened]);
 
   return (
-    <Modal className="link-edit-modal" isOpen={templateModalStatus.isOpened} toggle={close} size="xl" autoFocus={false}>
+    <div data-testid='template-modal'>
       <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
         {t('template.modal_label.Select template')}
       </ModalHeader>
-
       <ModalBody className="container">
         <div className="row">
+          {/* List Group */}
           <div className="d-none d-lg-block col-lg-4">
+
+            { isLoading && (
+              <div className='h-100 d-flex justify-content-center align-items-center'>
+                <i className="fa fa-2x fa-spinner fa-pulse text-muted mx-auto"></i>
+              </div>
+            ) }
+
             <div className="list-group">
-              { templateSummaries.map((templateSummary) => {
+              { templateSummaries != null && templateSummaries.map((templateSummary) => {
                 const templateId = constructTemplateId(templateSummary);
+                const isSelected = selectedTemplateSummary != null && constructTemplateId(selectedTemplateSummary) === templateId;
 
                 return (
                   <TemplateListGroupItem
                     key={templateId}
                     templateSummary={templateSummary}
-                    usersDefaultLang={personalSettingsInfo?.lang}
-                    onClick={() => setSelectedTemplateSummary(templateSummary)}
-                    isSelected={selectedTemplateSummary != null && constructTemplateId(selectedTemplateSummary) === templateId}
+                    onClick={() => onClickHandler(templateSummary)}
+                    isSelected={isSelected}
+                    usersDefaultLang={usersDefaultLang}
                   />
                 );
               }) }
             </div>
           </div>
-
+          {/* Dropdown */}
+          <div className='d-lg-none col mb-3'>
+            <UncontrolledDropdown>
+              <DropdownToggle caret type="button" outline className='w-100 text-right' disabled={isLoading}>
+                <span className="float-left">
+                  { (() => {
+                    if (isLoading) {
+                      return 'Loading..';
+                    }
+
+                    return selectedLocalizedTemplate != null && selectedLocalizedTemplate.isValid
+                      ? selectedLocalizedTemplate.title
+                      : t('Select template');
+                  })() }
+                </span>
+              </DropdownToggle>
+              <DropdownMenu role="menu" className={`p-0 ${styles['dm-templates']}`}>
+                { templateSummaries != null && templateSummaries.map((templateSummary) => {
+                  const templateId = constructTemplateId(templateSummary);
+
+                  return (
+                    <TemplateDropdownItem
+                      key={templateId}
+                      templateSummary={templateSummary}
+                      onClick={() => onClickHandler(templateSummary)}
+                      usersDefaultLang={usersDefaultLang}
+                    />
+                  );
+                }) }
+              </DropdownMenu>
+            </UncontrolledDropdown>
+          </div>
           <div className="col-12 col-lg-8">
-            <h3>{t('Preview')}</h3>
+            <div className='row mb-2 mb-lg-0'>
+              <div className="col-6">
+                <h3>{t('preview')}</h3>
+              </div>
+              <div className="col-6 d-flex justify-content-end">
+                <UncontrolledDropdown>
+                  <DropdownToggle caret type="button" outline className='float-right' disabled={selectedTemplateSummary == null}>
+                    <span className="float-left">{selectedTemplateLocale != null ? selectedTemplateLocale : t('Language')}</span>
+                  </DropdownToggle>
+                  <DropdownMenu className="dropdown-menu" role="menu">
+                    { selectedTemplateLocales != null && Array.from(selectedTemplateLocales).map((locale) => {
+                      return (
+                        <DropdownItem
+                          key={locale}
+                          onClick={() => setSelectedTemplateLocale(locale)}>
+                          <span>{locale}</span>
+                        </DropdownItem>
+                      );
+                    }) }
+                  </DropdownMenu>
+                </UncontrolledDropdown>
+              </div>
+            </div>
             <div className='card'>
               <div className="card-body" style={{ height: '400px', overflowY: 'auto' }}>
                 { rendererOptions != null && selectedTemplateSummary != null && (
@@ -147,7 +266,6 @@ export const TemplateModal = (): JSX.Element => {
             </div>
           </div>
         </div>
-
       </ModalBody>
       <ModalFooter>
         <button type="button" className="btn btn-outline-secondary mx-1" onClick={close}>
@@ -161,6 +279,23 @@ export const TemplateModal = (): JSX.Element => {
           {t('commons:Insert')}
         </button>
       </ModalFooter>
+    </div>
+  );
+};
+
+
+export const TemplateModal = (): JSX.Element => {
+  const { data: templateModalStatus, close } = useTemplateModal();
+
+  if (templateModalStatus == null) {
+    return <></>;
+  }
+
+  return (
+    <Modal className="link-edit-modal" isOpen={templateModalStatus.isOpened} toggle={close} size="xl" autoFocus={false}>
+      { templateModalStatus.isOpened && (
+        <TemplateModalSubstance templateModalStatus={templateModalStatus} close={close} />
+      ) }
     </Modal>
   );
 };

+ 1 - 1
apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginInstallerForm.tsx

@@ -25,7 +25,7 @@ export const PluginInstallerForm = (): JSX.Element => {
 
     const pluginInstallerForm: IGrowiPluginOrigin = {
       url,
-      ghBranch,
+      ghBranch: ghBranch || 'main',
       // ghTag,
     };
 

+ 13 - 2
apps/app/src/features/growi-plugin/interfaces/growi-plugin.ts

@@ -1,4 +1,5 @@
-import { GrowiPluginType, GrowiThemeMetadata, HasObjectId } from '@growi/core';
+import type { GrowiPluginType, GrowiThemeMetadata, HasObjectId } from '@growi/core';
+import type { TemplateSummary } from '@growi/pluginkit/dist/v4';
 
 export type IGrowiPluginOrigin = {
   url: string,
@@ -22,7 +23,17 @@ export type IGrowiPluginMeta = {
 }
 
 export type IGrowiThemePluginMeta = IGrowiPluginMeta & {
-  themes: GrowiThemeMetadata[]
+  themes: GrowiThemeMetadata[],
 }
 
+export type IGrowiTemplatePluginMeta = IGrowiPluginMeta & {
+  templateSummaries: TemplateSummary[],
+}
+
+export type IGrowiPluginMetaByType<T extends GrowiPluginType = any> = T extends 'theme'
+  ? IGrowiThemePluginMeta
+  : T extends 'template'
+    ? IGrowiTemplatePluginMeta
+    : IGrowiPluginMeta;
+
 export type IGrowiPluginHasId = IGrowiPlugin & HasObjectId;

+ 5 - 0
apps/app/src/features/growi-plugin/server/consts/index.ts

@@ -0,0 +1,5 @@
+import { resolveFromRoot } from '~/utils/project-dir-utils';
+
+export const PLUGIN_STORING_PATH = resolveFromRoot('tmp/plugins');
+
+export const PLUGIN_EXPRESS_STATIC_DIR = '/static/plugins';

+ 157 - 0
apps/app/src/features/growi-plugin/server/models/growi-plugin.integ.ts

@@ -0,0 +1,157 @@
+import { GrowiPluginType } from '@growi/core';
+
+import { GrowiPlugin } from './growi-plugin';
+
+describe('GrowiPlugin find methods', () => {
+
+  beforeAll(async() => {
+    await GrowiPlugin.insertMany([
+      {
+        isEnabled: false,
+        installedPath: 'weseek/growi-plugin-unenabled1',
+        organizationName: 'weseek',
+        origin: {
+          url: 'https://github.com/weseek/growi-plugin-unenabled1',
+        },
+        meta: {
+          name: '@growi/growi-plugin-unenabled1',
+          types: [GrowiPluginType.Script],
+        },
+      },
+      {
+        isEnabled: false,
+        installedPath: 'weseek/growi-plugin-unenabled2',
+        organizationName: 'weseek',
+        origin: {
+          url: 'https://github.com/weseek/growi-plugin-unenabled2',
+        },
+        meta: {
+          name: '@growi/growi-plugin-unenabled2',
+          types: [GrowiPluginType.Template],
+        },
+      },
+      {
+        isEnabled: true,
+        installedPath: 'weseek/growi-plugin-example1',
+        organizationName: 'weseek',
+        origin: {
+          url: 'https://github.com/weseek/growi-plugin-example1',
+        },
+        meta: {
+          name: '@growi/growi-plugin-example1',
+          types: [GrowiPluginType.Script],
+        },
+      },
+      {
+        isEnabled: true,
+        installedPath: 'weseek/growi-plugin-example2',
+        organizationName: 'weseek',
+        origin: {
+          url: 'https://github.com/weseek/growi-plugin-example2',
+        },
+        meta: {
+          name: '@growi/growi-plugin-example2',
+          types: [GrowiPluginType.Template],
+        },
+      },
+    ]);
+  });
+
+  afterAll(async() => {
+    await GrowiPlugin.deleteMany({});
+  });
+
+  describe.concurrent('.findEnabledPlugins', () => {
+    it('shoud returns documents which isEnabled is true', async() => {
+      // when
+      const results = await GrowiPlugin.findEnabledPlugins();
+
+      const pluginNames = results.map(p => p.meta.name);
+
+      // then
+      expect(results.length === 2).toBeTruthy();
+      expect(pluginNames.includes('@growi/growi-plugin-example1')).toBeTruthy();
+      expect(pluginNames.includes('@growi/growi-plugin-example2')).toBeTruthy();
+    });
+  });
+
+  describe.concurrent('.findEnabledPluginsByType', () => {
+    it("shoud returns documents which type is 'template'", async() => {
+      // when
+      const results = await GrowiPlugin.findEnabledPluginsByType(GrowiPluginType.Template);
+
+      const pluginNames = results.map(p => p.meta.name);
+
+      // then
+      expect(results.length === 1).toBeTruthy();
+      expect(pluginNames.includes('@growi/growi-plugin-example2')).toBeTruthy();
+    });
+  });
+
+});
+
+
+describe('GrowiPlugin activate/deactivate', () => {
+
+  beforeAll(async() => {
+    await GrowiPlugin.insertMany([
+      {
+        isEnabled: false,
+        installedPath: 'weseek/growi-plugin-example1',
+        organizationName: 'weseek',
+        origin: {
+          url: 'https://github.com/weseek/growi-plugin-example1',
+        },
+        meta: {
+          name: '@growi/growi-plugin-example1',
+          types: [GrowiPluginType.Script],
+        },
+      },
+    ]);
+  });
+
+  afterAll(async() => {
+    await GrowiPlugin.deleteMany({});
+  });
+
+  describe('.activatePlugin', () => {
+    it('shoud update the property "isEnabled" to true', async() => {
+      // setup
+      const plugin = await GrowiPlugin.findOne({});
+      assert(plugin != null);
+
+      expect(plugin.isEnabled).toBeFalsy(); // isEnabled: false
+
+      // when
+      const result = await GrowiPlugin.activatePlugin(plugin._id);
+      const pluginAfterActivated = await GrowiPlugin.findOne({ _id: plugin._id });
+
+      // then
+      expect(result).toEqual('@growi/growi-plugin-example1'); // equals to meta.name
+      expect(pluginAfterActivated).not.toBeNull();
+      assert(pluginAfterActivated != null);
+      expect(pluginAfterActivated.isEnabled).toBeTruthy(); // isEnabled: true
+    });
+  });
+
+  describe('.deactivatePlugin', () => {
+    it('shoud update the property "isEnabled" to true', async() => {
+      // setup
+      const plugin = await GrowiPlugin.findOne({});
+      assert(plugin != null);
+
+      expect(plugin.isEnabled).toBeTruthy(); // isEnabled: true
+
+      // when
+      const result = await GrowiPlugin.deactivatePlugin(plugin._id);
+      const pluginAfterActivated = await GrowiPlugin.findOne({ _id: plugin._id });
+
+      // then
+      expect(result).toEqual('@growi/growi-plugin-example1'); // equals to meta.name
+      expect(pluginAfterActivated).not.toBeNull();
+      assert(pluginAfterActivated != null);
+      expect(pluginAfterActivated.isEnabled).toBeFalsy(); // isEnabled: false
+    });
+  });
+
+});

+ 15 - 25
apps/app/src/features/growi-plugin/server/models/growi-plugin.ts

@@ -1,4 +1,4 @@
-import { GrowiPluginType, GrowiThemeMetadata, GrowiThemeSchemeType } from '@growi/core';
+import { GrowiPluginType } from '@growi/core';
 import {
   Schema, type Model, type Document, type Types,
 } from 'mongoose';
@@ -6,33 +6,20 @@ import {
 import { getOrCreateModel } from '~/server/util/mongoose-utils';
 
 import type {
-  IGrowiPlugin, IGrowiPluginMeta, IGrowiPluginOrigin, IGrowiThemePluginMeta,
+  IGrowiPlugin, IGrowiPluginMeta, IGrowiPluginMetaByType, IGrowiPluginOrigin, IGrowiTemplatePluginMeta, IGrowiThemePluginMeta,
 } from '../../interfaces';
 
-export interface IGrowiPluginDocument extends IGrowiPlugin, Document {
+export interface IGrowiPluginDocument<M extends IGrowiPluginMeta = IGrowiPluginMeta> extends IGrowiPlugin<M>, Document {
+  metaJson: IGrowiPluginMeta & IGrowiThemePluginMeta & IGrowiTemplatePluginMeta,
 }
 export interface IGrowiPluginModel extends Model<IGrowiPluginDocument> {
-  findEnabledPlugins(): Promise<IGrowiPlugin[]>
-  findEnabledPluginsIncludingAnyTypes(includingTypes: GrowiPluginType[]): Promise<IGrowiPlugin[]>
+  findEnabledPlugins(): Promise<IGrowiPluginDocument[]>
+  findEnabledPluginsByType<T extends GrowiPluginType>(type: T): Promise<IGrowiPluginDocument<IGrowiPluginMetaByType<T>>[]>
   activatePlugin(id: Types.ObjectId): Promise<string>
   deactivatePlugin(id: Types.ObjectId): Promise<string>
 }
 
-const growiThemeMetadataSchema = new Schema<GrowiThemeMetadata>({
-  name: { type: String, required: true },
-  manifestKey: { type: String, required: true },
-  schemeType: {
-    type: String,
-    enum: GrowiThemeSchemeType,
-    require: true,
-  },
-  bg: { type: String, required: true },
-  topbar: { type: String, required: true },
-  sidebar: { type: String, required: true },
-  accent: { type: String, required: true },
-});
-
-const growiPluginMetaSchema = new Schema<IGrowiPluginMeta|IGrowiThemePluginMeta>({
+const growiPluginMetaSchema = new Schema<IGrowiPluginMeta & IGrowiThemePluginMeta & IGrowiTemplatePluginMeta>({
   name: { type: String, required: true },
   types: {
     type: [String],
@@ -41,7 +28,8 @@ const growiPluginMetaSchema = new Schema<IGrowiPluginMeta|IGrowiThemePluginMeta>
   },
   desc: { type: String },
   author: { type: String },
-  themes: [growiThemeMetadataSchema],
+  themes: [Map],
+  templateSummaries: [Map],
 });
 
 const growiPluginOriginSchema = new Schema<IGrowiPluginOrigin>({
@@ -59,14 +47,16 @@ const growiPluginSchema = new Schema<IGrowiPluginDocument, IGrowiPluginModel>({
 });
 
 growiPluginSchema.statics.findEnabledPlugins = async function(): Promise<IGrowiPlugin[]> {
-  return this.find({ isEnabled: true });
+  return this.find({ isEnabled: true }).lean();
 };
 
-growiPluginSchema.statics.findEnabledPluginsIncludingAnyTypes = async function(types: GrowiPluginType[]): Promise<IGrowiPlugin[]> {
+growiPluginSchema.statics.findEnabledPluginsByType = async function<T extends GrowiPluginType>(
+    type: T,
+): Promise<IGrowiPlugin<IGrowiPluginMetaByType<T>>[]> {
   return this.find({
     isEnabled: true,
-    'meta.types': { $in: types },
-  });
+    'meta.types': { $in: type },
+  }).lean();
 };
 
 growiPluginSchema.statics.activatePlugin = async function(id: Types.ObjectId): Promise<string> {

+ 4 - 2
apps/app/src/features/growi-plugin/server/models/vo/github-url.ts

@@ -1,3 +1,5 @@
+import sanitize from 'sanitize-filename';
+
 // https://regex101.com/r/fK2rV3/1
 const githubReposIdPattern = new RegExp(/^\/([^/]+)\/([^/]+)$/);
 
@@ -44,8 +46,8 @@ export class GitHubUrl {
 
     this._branchName = branchName;
 
-    this._organizationName = matched[1];
-    this._reposName = matched[2];
+    this._organizationName = sanitize(matched[1]);
+    this._reposName = sanitize(matched[2]);
   }
 
 }

+ 11 - 0
apps/app/src/features/growi-plugin/server/services/growi-plugin/generate-template-plugin-meta.ts

@@ -0,0 +1,11 @@
+import type { GrowiPluginValidationData } from '@growi/pluginkit';
+import { scanAllTemplates } from '@growi/pluginkit/dist/v4/server';
+
+import type { IGrowiPlugin, IGrowiTemplatePluginMeta } from '../../../interfaces';
+
+export const generateTemplatePluginMeta = async(plugin: IGrowiPlugin, validationData: GrowiPluginValidationData): Promise<IGrowiTemplatePluginMeta> => {
+  return {
+    ...plugin.meta,
+    templateSummaries: await scanAllTemplates(validationData.projectDirRoot, { pluginId: plugin.installedPath }),
+  };
+};

+ 12 - 0
apps/app/src/features/growi-plugin/server/services/growi-plugin/generate-theme-plugin-meta.ts

@@ -0,0 +1,12 @@
+import type { GrowiPluginValidationData } from '@growi/pluginkit';
+
+import type { IGrowiPlugin, IGrowiThemePluginMeta } from '../../../interfaces';
+
+export const generateThemePluginMeta = async(plugin: IGrowiPlugin, validationData: GrowiPluginValidationData): Promise<IGrowiThemePluginMeta> => {
+  // TODO: validate as a theme plugin
+
+  return {
+    ...plugin.meta,
+    themes: validationData.growiPlugin.themes,
+  };
+};

+ 94 - 0
apps/app/src/features/growi-plugin/server/services/growi-plugin/growi-plugin.integ.ts

@@ -0,0 +1,94 @@
+import fs from 'fs';
+import path from 'path';
+
+import { PLUGIN_STORING_PATH } from '../../consts';
+import { GrowiPlugin } from '../../models';
+
+import { growiPluginService } from './growi-plugin';
+
+describe('Installing a GROWI template plugin', () => {
+
+  it('install() should success', async() => {
+    // when
+    const result = await growiPluginService.install({
+      url: 'https://github.com/weseek/growi-plugin-templates-for-office',
+    });
+    const count = await GrowiPlugin.count({ 'meta.name': 'growi-plugin-templates-for-office' });
+
+    // expect
+    expect(result).toEqual('growi-plugin-templates-for-office');
+    expect(count).toBe(1);
+    expect(fs.existsSync(path.join(
+      PLUGIN_STORING_PATH,
+      'weseek',
+      'growi-plugin-templates-for-office',
+    ))).toBeTruthy();
+  });
+
+  it('install() should success (re-install)', async() => {
+    // confirm
+    const count1 = await GrowiPlugin.count({ 'meta.name': 'growi-plugin-templates-for-office' });
+    expect(count1).toBe(1);
+
+    // setup
+    const dummyFilePath = path.join(
+      PLUGIN_STORING_PATH,
+      'weseek',
+      'growi-plugin-templates-for-office',
+      'dummy.txt',
+    );
+    fs.appendFileSync(dummyFilePath, '');
+    expect(fs.existsSync(dummyFilePath)).toBeTruthy();
+
+    // when
+    const result = await growiPluginService.install({
+      url: 'https://github.com/weseek/growi-plugin-templates-for-office',
+    });
+    const count2 = await GrowiPlugin.count({ 'meta.name': 'growi-plugin-templates-for-office' });
+
+    // expect
+    expect(result).toEqual('growi-plugin-templates-for-office');
+    expect(count2).toBe(1);
+    expect(fs.existsSync(dummyFilePath)).toBeFalsy(); // the dummy file should be removed
+  });
+
+});
+
+describe('Installing a GROWI theme plugin', () => {
+
+  it('install() should success', async() => {
+    // when
+    const result = await growiPluginService.install({
+      url: 'https://github.com/weseek/growi-plugin-theme-welcome-to-fumiya-room',
+    });
+    const count = await GrowiPlugin.count({ 'meta.name': 'growi-plugin-theme-welcome-to-fumiya-room' });
+
+    // expect
+    expect(result).toEqual('growi-plugin-theme-welcome-to-fumiya-room');
+    expect(count).toBe(1);
+    expect(fs.existsSync(path.join(
+      PLUGIN_STORING_PATH,
+      'weseek',
+      'growi-plugin-theme-welcome-to-fumiya-room',
+    ))).toBeTruthy();
+  });
+
+  it('findThemePlugin() should return data with metadata and manifest', async() => {
+    // confirm
+    const count = await GrowiPlugin.count({ 'meta.name': 'growi-plugin-theme-welcome-to-fumiya-room' });
+    expect(count).toBe(1);
+
+    // when
+    const results = await growiPluginService.findThemePlugin('welcome-to-fumiya-room');
+
+    // expect
+    expect(results).not.toBeNull();
+    assert(results != null);
+    expect(results.growiPlugin).not.toBeNull();
+    expect(results.themeMetadata).not.toBeNull();
+    expect(results.themeHref).not.toBeNull();
+    expect(results.themeHref
+      .startsWith('/static/plugins/weseek/growi-plugin-theme-welcome-to-fumiya-room/dist/assets/style.')).toBeTruthy();
+  });
+
+});

+ 87 - 68
apps/app/src/features/growi-plugin/server/services/growi-plugin.ts → apps/app/src/features/growi-plugin/server/services/growi-plugin/growi-plugin.ts

@@ -1,32 +1,39 @@
 import fs, { readFileSync } from 'fs';
 import path from 'path';
 
-import { GrowiPluginType, GrowiThemeMetadata, ViteManifest } from '@growi/core';
+import { GrowiPluginType, type GrowiThemeMetadata, type ViteManifest } from '@growi/core';
+import type { GrowiPluginPackageData } from '@growi/pluginkit';
+import { importPackageJson, validateGrowiDirective } from '@growi/pluginkit/dist/v4/server';
 // eslint-disable-next-line no-restricted-imports
 import axios from 'axios';
 import mongoose from 'mongoose';
+import sanitize from 'sanitize-filename';
 import streamToPromise from 'stream-to-promise';
 import unzipper from 'unzipper';
 
 import loggerFactory from '~/utils/logger';
-import { resolveFromRoot } from '~/utils/project-dir-utils';
 
 import type {
-  IGrowiPlugin, IGrowiPluginOrigin, IGrowiThemePluginMeta, IGrowiPluginMeta,
-} from '../../interfaces';
-import { GrowiPlugin } from '../models';
-import { GitHubUrl } from '../models/vo/github-url';
+  IGrowiPlugin, IGrowiPluginOrigin, IGrowiPluginMeta,
+} from '../../../interfaces';
+import { PLUGIN_EXPRESS_STATIC_DIR, PLUGIN_STORING_PATH } from '../../consts';
+import { GrowiPlugin } from '../../models';
+import { GitHubUrl } from '../../models/vo/github-url';
+
+import { generateTemplatePluginMeta } from './generate-template-plugin-meta';
+import { generateThemePluginMeta } from './generate-theme-plugin-meta';
 
 const logger = loggerFactory('growi:plugins:plugin-utils');
 
-const pluginStoringPath = resolveFromRoot('tmp/plugins');
+export type GrowiPluginResourceEntries = [installedPath: string, href: string][];
 
-const PLUGINS_STATIC_DIR = '/static/plugins'; // configured by express.static
+function retrievePluginManifest(growiPlugin: IGrowiPlugin): ViteManifest | undefined {
+  const manifestPath = path.join(PLUGIN_STORING_PATH, growiPlugin.installedPath, 'dist/manifest.json');
 
-export type GrowiPluginResourceEntries = [installedPath: string, href: string][];
+  if (!fs.existsSync(manifestPath)) {
+    return;
+  }
 
-function retrievePluginManifest(growiPlugin: IGrowiPlugin): ViteManifest {
-  const manifestPath = resolveFromRoot(path.join('tmp/plugins', growiPlugin.installedPath, 'dist/manifest.json'));
   const manifestStr: string = readFileSync(manifestPath, 'utf-8');
   return JSON.parse(manifestStr);
 }
@@ -57,8 +64,8 @@ export class GrowiPluginService implements IGrowiPluginService {
 
       // if not exists repository in file system, download latest plugin repository
       for await (const growiPlugin of growiPlugins) {
-        const pluginPath = path.join(pluginStoringPath, growiPlugin.installedPath);
-        const organizationName = path.join(pluginStoringPath, growiPlugin.organizationName);
+        const pluginPath = path.join(PLUGIN_STORING_PATH, growiPlugin.installedPath);
+        const organizationName = path.join(PLUGIN_STORING_PATH, growiPlugin.organizationName);
         if (fs.existsSync(pluginPath)) {
           continue;
         }
@@ -68,12 +75,12 @@ export class GrowiPluginService implements IGrowiPluginService {
           }
 
           // TODO: imprv Document version and repository version possibly different.
-          const ghUrl = new GitHubUrl(growiPlugin.origin.url, growiPlugin.origin.branchName);
+          const ghUrl = new GitHubUrl(growiPlugin.origin.url, growiPlugin.origin.ghBranch);
           const { reposName, branchName, archiveUrl } = ghUrl;
 
-          const zipFilePath = path.join(pluginStoringPath, `${branchName}.zip`);
-          const unzippedPath = pluginStoringPath;
-          const unzippedReposPath = path.join(pluginStoringPath, `${reposName}-${branchName}`);
+          const zipFilePath = path.join(PLUGIN_STORING_PATH, `${branchName}.zip`);
+          const unzippedPath = PLUGIN_STORING_PATH;
+          const unzippedReposPath = path.join(PLUGIN_STORING_PATH, `${reposName}-${branchName}`);
 
           try {
             // download github repository to local file system
@@ -105,42 +112,42 @@ export class GrowiPluginService implements IGrowiPluginService {
     const {
       organizationName, reposName, branchName, archiveUrl,
     } = ghUrl;
+
+    const sanitizedBranchName = sanitize(branchName);
+
     const installedPath = `${organizationName}/${reposName}`;
 
-    const zipFilePath = path.join(pluginStoringPath, `${branchName}.zip`);
-    const unzippedPath = pluginStoringPath;
-    const unzippedReposPath = path.join(pluginStoringPath, `${reposName}-${branchName}`);
-    const temporaryReposPath = path.join(pluginStoringPath, reposName);
-    const reposStoringPath = path.join(pluginStoringPath, `${installedPath}`);
-    const organizationPath = path.join(pluginStoringPath, organizationName);
+    const organizationPath = path.join(PLUGIN_STORING_PATH, organizationName);
+    const zipFilePath = path.join(organizationPath, `${reposName}-${sanitizedBranchName}.zip`);
+    const temporaryReposPath = path.join(organizationPath, `${reposName}-${sanitizedBranchName}`);
+    const reposPath = path.join(organizationPath, reposName);
 
+    if (!fs.existsSync(organizationPath)) fs.mkdirSync(organizationPath);
 
     let plugins: IGrowiPlugin<IGrowiPluginMeta>[];
 
     try {
       // download github repository to file system's temporary path
       await this.download(archiveUrl, zipFilePath);
-      await this.unzip(zipFilePath, unzippedPath);
-      fs.renameSync(unzippedReposPath, temporaryReposPath);
+      await this.unzip(zipFilePath, organizationPath);
 
       // detect plugins
-      plugins = await GrowiPluginService.detectPlugins(origin, organizationName, reposName);
-
-      if (!fs.existsSync(organizationPath)) fs.mkdirSync(organizationPath);
+      plugins = await GrowiPluginService.detectPlugins(origin, organizationName, reposName, { packageRootPath: temporaryReposPath });
 
       // remove the old repository from the storing path
-      if (fs.existsSync(reposStoringPath)) await fs.promises.rm(reposStoringPath, { recursive: true });
+      if (fs.existsSync(reposPath)) await fs.promises.rm(reposPath, { recursive: true });
 
       // move new repository from temporary path to storing path.
-      fs.renameSync(temporaryReposPath, reposStoringPath);
+      fs.renameSync(temporaryReposPath, reposPath);
     }
     catch (err) {
+      logger.error(err);
+      throw err;
+    }
+    finally {
       // clean up
       if (fs.existsSync(zipFilePath)) await fs.promises.rm(zipFilePath);
-      if (fs.existsSync(unzippedReposPath)) await fs.promises.rm(unzippedReposPath, { recursive: true });
       if (fs.existsSync(temporaryReposPath)) await fs.promises.rm(temporaryReposPath, { recursive: true });
-      logger.error(err);
-      throw err;
     }
 
     try {
@@ -153,9 +160,10 @@ export class GrowiPluginService implements IGrowiPluginService {
       return plugins[0].meta.name;
     }
     catch (err) {
-      // clean up
-      if (fs.existsSync(reposStoringPath)) await fs.promises.rm(reposStoringPath, { recursive: true });
+      // uninstall
+      if (fs.existsSync(reposPath)) await fs.promises.rm(reposPath, { recursive: true });
       await this.deleteOldPluginDocument(installedPath);
+
       logger.error(err);
       throw err;
     }
@@ -188,22 +196,21 @@ export class GrowiPluginService implements IGrowiPluginService {
         }).catch((err) => {
           logger.error(err);
           // eslint-disable-next-line prefer-promise-reject-errors
-          rejects('Filed to download file.');
+          rejects('Failed to download file.');
         });
     });
   }
 
-  private async unzip(zipFilePath: fs.PathLike, unzippedPath: fs.PathLike): Promise<void> {
+  private async unzip(zipFilePath: fs.PathLike, destPath: fs.PathLike): Promise<void> {
     try {
       const stream = fs.createReadStream(zipFilePath);
-      const unzipStream = stream.pipe(unzipper.Extract({ path: unzippedPath }));
+      const unzipStream = stream.pipe(unzipper.Extract({ path: destPath }));
 
       await streamToPromise(unzipStream);
-      await fs.promises.rm(zipFilePath);
     }
     catch (err) {
       logger.error(err);
-      throw new Error('Filed to unzip.');
+      throw new Error('Failed to unzip.');
     }
   }
 
@@ -212,35 +219,39 @@ export class GrowiPluginService implements IGrowiPluginService {
   }
 
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, max-len
-  private static async detectPlugins(origin: IGrowiPluginOrigin, ghOrganizationName: string, ghReposName: string, parentPackageJson?: any): Promise<IGrowiPlugin[]> {
-    const packageJsonPath = path.resolve(pluginStoringPath, ghReposName, 'package.json');
-    const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
+  private static async detectPlugins(
+      origin: IGrowiPluginOrigin, ghOrganizationName: string, ghReposName: string,
+      opts?: {
+        packageRootPath?: string,
+        parentPackageData?: GrowiPluginPackageData,
+      },
+  ): Promise<IGrowiPlugin[]> {
+    const packageRootPath = opts?.packageRootPath ?? path.resolve(PLUGIN_STORING_PATH, ghOrganizationName, ghReposName);
 
-    const { growiPlugin } = packageJson;
-    const {
-      name: packageName, description: packageDesc, author: packageAuthor,
-    } = parentPackageJson ?? packageJson;
+    // validate
+    const validationData = await validateGrowiDirective(packageRootPath);
 
+    const packageData = opts?.parentPackageData ?? importPackageJson(packageRootPath);
 
-    if (growiPlugin == null) {
-      throw new Error('This package does not include \'growiPlugin\' section.');
-    }
+    const { growiPlugin } = validationData;
+    const {
+      name: packageName, description: packageDesc, author: packageAuthor,
+    } = packageData;
 
     // detect sub plugins for monorepo
     if (growiPlugin.isMonorepo && growiPlugin.packages != null) {
       const plugins = await Promise.all(
         growiPlugin.packages.map(async(subPackagePath) => {
-          const subPackageInstalledPath = path.join(ghReposName, subPackagePath);
-          return this.detectPlugins(origin, subPackageInstalledPath, packageJson);
+          return this.detectPlugins(origin, ghOrganizationName, ghReposName, {
+            packageRootPath: path.join(packageRootPath, subPackagePath),
+            parentPackageData: packageData,
+          });
         }),
       );
       return plugins.flat();
     }
 
-    if (growiPlugin.types == null) {
-      throw new Error('\'growiPlugin\' section must have a \'types\' property.');
-    }
-    const plugin = {
+    const plugin: IGrowiPlugin = {
       isEnabled: true,
       installedPath: `${ghOrganizationName}/${ghReposName}`,
       organizationName: ghOrganizationName,
@@ -255,10 +266,11 @@ export class GrowiPluginService implements IGrowiPluginService {
 
     // add theme metadata
     if (growiPlugin.types.includes(GrowiPluginType.Theme)) {
-      (plugin as IGrowiPlugin<IGrowiThemePluginMeta>).meta = {
-        ...plugin.meta,
-        themes: growiPlugin.themes,
-      };
+      plugin.meta = await generateThemePluginMeta(plugin, validationData);
+    }
+    // add template metadata
+    if (growiPlugin.types.includes(GrowiPluginType.Template)) {
+      plugin.meta = await generateTemplatePluginMeta(plugin, validationData);
     }
 
     logger.info('Plugin detected => ', plugin);
@@ -285,12 +297,12 @@ export class GrowiPluginService implements IGrowiPluginService {
     }
 
     try {
-      const growiPluginsPath = path.join(pluginStoringPath, growiPlugins.installedPath);
+      const growiPluginsPath = path.join(PLUGIN_STORING_PATH, growiPlugins.installedPath);
       await deleteFolder(growiPluginsPath);
     }
     catch (err) {
       logger.error(err);
-      throw new Error('Filed to delete plugin repository.');
+      throw new Error('Failed to delete plugin repository.');
     }
 
     try {
@@ -298,7 +310,7 @@ export class GrowiPluginService implements IGrowiPluginService {
     }
     catch (err) {
       logger.error(err);
-      throw new Error('Filed to delete plugin from GrowiPlugin documents.');
+      throw new Error('Failed to delete plugin from GrowiPlugin documents.');
     }
 
     return growiPlugins.meta.name;
@@ -310,10 +322,10 @@ export class GrowiPluginService implements IGrowiPluginService {
 
     try {
       // retrieve plugin manifests
-      const growiPlugins = await GrowiPlugin.findEnabledPluginsIncludingAnyTypes([GrowiPluginType.Theme]) as IGrowiPlugin<IGrowiThemePluginMeta>[];
+      const growiPlugins = await GrowiPlugin.findEnabledPluginsByType(GrowiPluginType.Theme);
 
       growiPlugins
-        .forEach(async(growiPlugin) => {
+        .forEach((growiPlugin) => {
           const themeMetadatas = growiPlugin.meta.themes;
           const themeMetadata = themeMetadatas.find(t => t.name === theme);
 
@@ -335,7 +347,10 @@ export class GrowiPluginService implements IGrowiPluginService {
     let themeHref;
     try {
       const manifest = retrievePluginManifest(matchedPlugin);
-      themeHref = `${PLUGINS_STATIC_DIR}/${matchedPlugin.installedPath}/dist/${manifest[matchedThemeMetadata.manifestKey].file}`;
+      if (manifest == null) {
+        throw new Error('The manifest file does not exists');
+      }
+      themeHref = `${PLUGIN_EXPRESS_STATIC_DIR}/${matchedPlugin.installedPath}/dist/${manifest[matchedThemeMetadata.manifestKey].file}`;
     }
     catch (e) {
       logger.error(`Could not read manifest file for the theme '${theme}'`, e);
@@ -356,14 +371,18 @@ export class GrowiPluginService implements IGrowiPluginService {
           const { types } = growiPlugin.meta;
           const manifest = await retrievePluginManifest(growiPlugin);
 
+          if (manifest == null) {
+            return;
+          }
+
           // add script
           if (types.includes(GrowiPluginType.Script)) {
-            const href = `${PLUGINS_STATIC_DIR}/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].file}`;
+            const href = `${PLUGIN_EXPRESS_STATIC_DIR}/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].file}`;
             entries.push([growiPlugin.installedPath, href]);
           }
           // add link
           if (types.includes(GrowiPluginType.Script) || types.includes(GrowiPluginType.Style)) {
-            const href = `${PLUGINS_STATIC_DIR}/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].css}`;
+            const href = `${PLUGIN_EXPRESS_STATIC_DIR}/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].css}`;
             entries.push([growiPlugin.installedPath, href]);
           }
         }

+ 1 - 0
apps/app/src/features/growi-plugin/server/services/growi-plugin/index.ts

@@ -0,0 +1 @@
+export * from './growi-plugin';

+ 62 - 11
apps/app/src/features/templates/server/routes/apiv3/index.ts

@@ -1,7 +1,13 @@
-import { scanAllTemplateStatus, getMarkdown } from '@growi/pluginkit/dist/v4/server';
+import path from 'path';
+
+import { GrowiPluginType } from '@growi/core';
+import { TemplateSummary } from '@growi/pluginkit/dist/v4';
+import { scanAllTemplates, getMarkdown } from '@growi/pluginkit/dist/v4/server';
 import express from 'express';
 import { param, query } from 'express-validator';
 
+import { PLUGIN_STORING_PATH } from '~/features/growi-plugin/server/consts';
+import { GrowiPlugin } from '~/features/growi-plugin/server/models';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
@@ -21,18 +27,48 @@ const validator = {
   ],
 };
 
+
+// cache object
+let presetTemplateSummaries: TemplateSummary[];
+
+
 module.exports = (crowi) => {
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
   router.get('/', loginRequiredStrictly, validator.list, apiV3FormValidator, async(req, res: ApiV3Response) => {
     const { includeInvalidTemplates } = req.query;
 
-    const presetTemplatesRoot = resolveFromRoot('../../node_modules/@growi/preset-templates');
-    const summaries = await scanAllTemplateStatus(presetTemplatesRoot, {
-      returnsInvalidTemplates: includeInvalidTemplates,
-    });
+    // scan preset templates
+    if (presetTemplateSummaries == null) {
+      const presetTemplatesRoot = resolveFromRoot('../../node_modules/@growi/preset-templates');
+
+      try {
+        presetTemplateSummaries = await scanAllTemplates(presetTemplatesRoot, {
+          returnsInvalidTemplates: includeInvalidTemplates,
+        });
+      }
+      catch (err) {
+        logger.error(err);
+        presetTemplateSummaries = [];
+      }
+    }
+
+    // load plugin templates
+    let pluginsTemplateSummaries: TemplateSummary[] = [];
+    try {
+      const plugins = await GrowiPlugin.findEnabledPluginsByType(GrowiPluginType.Template);
+      pluginsTemplateSummaries = plugins.flatMap(p => p.meta.templateSummaries);
+    }
+    catch (err) {
+      logger.error(err);
+    }
 
-    return res.apiv3({ summaries });
+    return res.apiv3({
+      summaries: [
+        ...presetTemplateSummaries,
+        ...pluginsTemplateSummaries,
+      ],
+    });
   });
 
   router.get('/preset-templates/:templateId/:locale', loginRequiredStrictly, validator.get, apiV3FormValidator, async(req, res: ApiV3Response) => {
@@ -41,17 +77,32 @@ module.exports = (crowi) => {
     } = req.params;
 
     const presetTemplatesRoot = resolveFromRoot('../../node_modules/@growi/preset-templates');
-    const markdown = await getMarkdown(presetTemplatesRoot, templateId, locale);
 
-    return res.apiv3({ markdown });
+    try {
+      const markdown = await getMarkdown(presetTemplatesRoot, templateId, locale);
+      return res.apiv3({ markdown });
+    }
+    catch (err) {
+      res.apiv3Err(err);
+    }
   });
 
-  router.get('/plugin-templates/:pluginId/:templateId/:locale', loginRequiredStrictly, validator.get, apiV3FormValidator, async(req, res: ApiV3Response) => {
+  router.get('/plugin-templates/:organizationId/:reposId/:templateId/:locale', loginRequiredStrictly, validator.get, apiV3FormValidator, async(
+      req, res: ApiV3Response,
+  ) => {
     const {
-      pluginId, templateId, locale,
+      organizationId, reposId, templateId, locale,
     } = req.params;
 
-    return res.apiv3({});
+    const pluginRoot = path.join(PLUGIN_STORING_PATH, `${organizationId}/${reposId}`);
+
+    try {
+      const markdown = await getMarkdown(pluginRoot, templateId, locale);
+      return res.apiv3({ markdown });
+    }
+    catch (err) {
+      res.apiv3Err(err);
+    }
   });
 
   return router;

+ 4 - 3
apps/app/src/features/templates/stores/template.tsx

@@ -1,10 +1,11 @@
 import { getLocalizedTemplate, type TemplateSummary } from '@growi/pluginkit/dist/v4';
-import useSWR, { type SWRResponse } from 'swr';
+import type { SWRResponse } from 'swr';
+import useSWRImmutable from 'swr/immutable';
 
 import { apiv3Get } from '~/client/util/apiv3-client';
 
 export const useSWRxTemplates = (): SWRResponse<TemplateSummary[], Error> => {
-  return useSWR(
+  return useSWRImmutable(
     '/templates',
     endpoint => apiv3Get<{ summaries: TemplateSummary[] }>(endpoint).then(res => res.data.summaries),
   );
@@ -14,7 +15,7 @@ export const useSWRxTemplate = (summary: TemplateSummary | undefined, locale?: s
   const pluginId = summary?.default.pluginId;
   const targetTemplate = getLocalizedTemplate(summary, locale);
 
-  return useSWR(
+  return useSWRImmutable(
     () => {
       if (targetTemplate == null) {
         return null;

+ 5 - 3
apps/app/src/pages/[[...path]].page.tsx

@@ -241,12 +241,11 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
 
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
+  const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
+  const { data: currentPageId, mutate: mutateCurrentPageId } = useCurrentPageId();
 
   const { mutate: mutateIsNotFound } = useIsNotFound();
 
-  const { data: currentPageId, mutate: mutateCurrentPageId } = useCurrentPageId();
-
-  const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { mutate: mutateIsLatestRevision } = useIsLatestRevision();
 
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
@@ -268,6 +267,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
     : false;
 
 
+  // Store initial data (When revisionBody is not SSR)
   useEffect(() => {
     if (!props.skipSSR) {
       return;
@@ -279,6 +279,8 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
         mutateEditingMarkdown(pageData?.revision.body);
       };
 
+      // If skipSSR is true, use the API to retrieve page data.
+      // Because pageWIthMeta does not contain revision.body
       mutatePageData();
     }
   }, [currentPageId, mutateCurrentPage, mutateEditingMarkdown, props.isNotFound, props.skipSSR]);

+ 2 - 1
apps/app/src/server/crowi/express-init.js

@@ -2,6 +2,7 @@ import { manifestPath as presetThemesManifestPath } from '@growi/preset-themes';
 import csrf from 'csurf';
 import qs from 'qs';
 
+import { PLUGIN_EXPRESS_STATIC_DIR, PLUGIN_STORING_PATH } from '~/features/growi-plugin/server/consts';
 import loggerFactory from '~/utils/logger';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 
@@ -88,7 +89,7 @@ module.exports = function(crowi, app) {
   app.use('/static/preset-themes', express.static(
     resolveFromRoot(`../../node_modules/@growi/preset-themes/${path.dirname(presetThemesManifestPath)}`),
   ));
-  app.use('/static/plugins', express.static(path.resolve(__dirname, '../../../tmp/plugins')));
+  app.use(PLUGIN_EXPRESS_STATIC_DIR, express.static(PLUGIN_STORING_PATH));
 
   app.use(methodOverride());
 

+ 1 - 1
apps/app/src/server/routes/apiv3/bookmark-folder.ts

@@ -85,7 +85,7 @@ module.exports = (crowi) => {
               model: 'User',
             },
           },
-        }).exec();
+        }).exec() as never as BookmarkFolderItems[];
 
       const returnValue: BookmarkFolderItems[] = [];
 

+ 1 - 1
apps/app/src/server/routes/apiv3/customize-setting.js

@@ -273,7 +273,7 @@ module.exports = (crowi) => {
       const currentTheme = await crowi.configManager.getConfig('crowi', 'customize:theme');
 
       // retrieve plugin manifests
-      const themePlugins = await GrowiPlugin.findEnabledPluginsIncludingAnyTypes([GrowiPluginType.Theme]);
+      const themePlugins = await GrowiPlugin.findEnabledPluginsByType(GrowiPluginType.Theme);
 
       const pluginThemesMetadatas = themePlugins
         .map(themePlugin => themePlugin.meta.themes)

+ 1 - 1
apps/app/src/server/routes/apiv3/users.js

@@ -189,7 +189,7 @@ module.exports = (crowi) => {
     await mailService.send({
       to: user.email,
       subject: `New password for ${appTitle}`,
-      template: path.join(crowi.localeDir, 'en_US/admin/userResetPassword.txt'),
+      template: path.join(crowi.localeDir, 'en_US/admin/userResetPassword.ejs'),
       vars: {
         email: user.email,
         password: user.password,

+ 19 - 5
apps/app/src/server/service/search-delegator/elasticsearch-client.ts

@@ -1,7 +1,12 @@
 /* eslint-disable implicit-arrow-linebreak */
 /* eslint-disable no-confusing-arrow */
-import { Client as ES7Client, ApiResponse as ES7ApiResponse, RequestParams as ES7RequestParams } from '@elastic/elasticsearch7';
-import { Client as ES8Client, estypes } from '@elastic/elasticsearch8';
+import {
+  Client as ES7Client,
+  ClientOptions as ES7ClientOptions,
+  ApiResponse as ES7ApiResponse,
+  RequestParams as ES7RequestParams,
+} from '@elastic/elasticsearch7';
+import { ClientOptions as ES8ClientOptions, Client as ES8Client, estypes } from '@elastic/elasticsearch8';
 
 import {
   BulkResponse,
@@ -17,12 +22,21 @@ import {
   ReindexResponse,
 } from './elasticsearch-client-types';
 
+
+type ElasticsearchClientParams =
+  | [ isES7: true, options: ES7ClientOptions, rejectUnauthorized: boolean ]
+  | [ isES7: false, options: ES8ClientOptions, rejectUnauthorized: boolean ]
+
 export default class ElasticsearchClient {
 
-  client: ES7Client | ES8Client;
+  private client: ES7Client | ES8Client;
+
+  constructor(...params: ElasticsearchClientParams) {
+    const [isES7, options, rejectUnauthorized] = params;
 
-  constructor(client: ES7Client | ES8Client) {
-    this.client = client;
+    this.client = isES7
+      ? new ES7Client({ ...options, ssl: { rejectUnauthorized } })
+      : new ES8Client({ ...options, tls: { rejectUnauthorized } });
   }
 
   async bulk(params: ES7RequestParams.Bulk & estypes.BulkRequest): Promise<BulkResponse | estypes.BulkResponse> {

+ 6 - 6
apps/app/src/server/service/search-delegator/elasticsearch.ts

@@ -1,8 +1,6 @@
 import { Writable, Transform } from 'stream';
 import { URL } from 'url';
 
-import elasticsearch7 from '@elastic/elasticsearch7';
-import elasticsearch8 from '@elastic/elasticsearch8';
 import gc from 'expose-gc/function';
 import mongoose from 'mongoose';
 import streamToPromise from 'stream-to-promise';
@@ -81,7 +79,6 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
     this.isElasticsearchV7 = elasticsearchVersion === 7;
 
-    this.elasticsearch = this.isElasticsearchV7 ? elasticsearch7 : elasticsearch8;
     this.isElasticsearchReindexOnBoot = this.configManager.getConfig('crowi', 'app:elasticsearchReindexOnBoot');
     this.client = null;
 
@@ -119,12 +116,15 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   initClient() {
     const { host, auth, indexName } = this.getConnectionInfo();
 
-    this.client = new ElasticsearchClient(new this.elasticsearch.Client({
+    const rejectUnauthorized = this.configManager.getConfig('crowi', 'app:elasticsearchRejectUnauthorized');
+
+    const options = {
       node: host,
-      ssl: { rejectUnauthorized: this.configManager.getConfig('crowi', 'app:elasticsearchRejectUnauthorized') },
       auth,
       requestTimeout: this.configManager.getConfig('crowi', 'app:elasticsearchRequestTimeout'),
-    }));
+    };
+
+    this.client = new ElasticsearchClient(this.isElasticsearchV7, options, rejectUnauthorized);
     this.indexName = indexName;
   }
 

+ 1 - 1
apps/app/src/server/util/mongoose-utils.ts

@@ -26,7 +26,7 @@ export const getModelSafely = <T>(modelName: string): Model<T & Document> | null
 };
 
 // TODO: Do not use any type
-export const getOrCreateModel = <Interface, Method>(modelName: string, schema: any): any => {
+export const getOrCreateModel = <Interface, Method>(modelName: string, schema: any): Method & Model<Interface & Document> => {
   if (mongoose.modelNames().includes(modelName)) {
     return mongoose.model<Interface & Document, Method & Model<Interface & Document>>(modelName);
   }

+ 2 - 2
apps/app/src/services/renderer/remark-plugins/attachment.ts

@@ -7,10 +7,10 @@ import { visit } from 'unist-util-visit';
 
 const SUPPORTED_ATTRIBUTES = ['attachmentId', 'url', 'attachmentName'];
 
-const isAttachmentLink = (url: string) => {
+const isAttachmentLink = (url: string): boolean => {
   // https://regex101.com/r/9qZhiK/1
   const attachmentUrlFormat = new RegExp(/^\/(attachment)\/([^/^\n]+)$/);
-  return url.match(attachmentUrlFormat);
+  return attachmentUrlFormat.test(url);
 };
 
 const rewriteNode = (node: Node) => {

+ 70 - 0
apps/app/src/services/renderer/remark-plugins/slides.ts

@@ -0,0 +1,70 @@
+import type { Schema as SanitizeOption } from 'hast-util-sanitize';
+import type { Root } from 'mdast';
+import { frontmatterToMarkdown } from 'mdast-util-frontmatter';
+import { gfmToMarkdown } from 'mdast-util-gfm';
+import { toMarkdown } from 'mdast-util-to-markdown';
+import type { Plugin } from 'unified';
+import type { Node } from 'unist';
+import { visit } from 'unist-util-visit';
+
+const SUPPORTED_ATTRIBUTES = ['children', 'marp'];
+
+const rewriteNode = (tree: Node, node: Node) => {
+  let slide = false;
+  let marp = false;
+
+  const lines = (node.value as string).split('\n');
+
+  lines.forEach((line) => {
+    const [key, value] = line.split(':').map(part => part.trim());
+
+    if (key === 'slide' && value === 'true') {
+      slide = true;
+    }
+    else if (key === 'marp' && value === 'true') {
+      marp = true;
+    }
+  });
+
+  if (marp || slide) {
+
+    const newNode: Node = {
+      type: 'root',
+      data: {},
+      position: tree.position,
+      children: tree.children,
+    };
+
+    const data = newNode.data ?? (newNode.data = {});
+    tree.children = [newNode];
+    data.hName = 'slide';
+    data.hProperties = {
+      marp: marp ? 'marp' : '',
+      children: toMarkdown(tree as Root, {
+        extensions: [
+          frontmatterToMarkdown(['yaml']),
+          gfmToMarkdown(),
+          // TODO: add new extension remark-growi-directive to markdown
+          // https://redmine.weseek.co.jp/issues/126744
+        ],
+      }),
+    };
+  }
+};
+
+export const remarkPlugin: Plugin = function() {
+  return (tree) => {
+    visit(tree, (node) => {
+      if (node.type === 'yaml' && node.value != null) {
+        rewriteNode(tree, node);
+      }
+    });
+  };
+};
+
+export const sanitizeOption: SanitizeOption = {
+  tagNames: ['slide'],
+  attributes: {
+    slide: SUPPORTED_ATTRIBUTES,
+  },
+};

+ 1 - 1
apps/app/src/stores/modal.tsx

@@ -633,7 +633,7 @@ type TemplateSelectedCallback = (templateText: string) => void;
 type TemplateModalOptions = {
   onSubmit?: TemplateSelectedCallback,
 }
-type TemplateModalStatus = TemplateModalOptions & {
+export type TemplateModalStatus = TemplateModalOptions & {
   isOpened: boolean,
 }
 

+ 10 - 10
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts

@@ -81,8 +81,9 @@ context('Access to page', () => {
     cy.screenshot(`${ssPrefix}-Sandbox-edit-page`);
   })
 
+  const body1 = 'hello';
+  const body2 = ' world!';
   it('View and Edit contents are successfully loaded', () => {
-    const body1 = 'hello';
     cy.visit('/Sandbox/testForUseEditingMarkdown');
 
     openEditor();
@@ -98,22 +99,21 @@ context('Access to page', () => {
   })
 
   it('Editing contents are successfully loaded with shortcut key', () => {
-    const body2 = ' world!';
     const savePageShortcutKey = '{ctrl+s}';
 
     cy.visit('/Sandbox/testForUseEditingMarkdown');
 
     openEditor();
 
+    cy.get('.CodeMirror').contains(body1);
+
     // check editing contents with shortcut key
-    cy.get('.CodeMirror-line').children().first().invoke('text').then((text) => {
-      cy.get('.CodeMirror').type(body2);
-      cy.get('.CodeMirror').contains(text+body2);
-      cy.get('.page-editor-preview-body').contains(text+body2);
-      cy.get('.CodeMirror').type(savePageShortcutKey);
-      cy.get('.CodeMirror').contains(text+body2);
-      cy.get('.page-editor-preview-body').contains(text+body2);
-    })
+    cy.get('.CodeMirror').type(body2);
+    cy.get('.CodeMirror').contains(body1+body2);
+    cy.get('.page-editor-preview-body').contains(body1+body2);
+    cy.get('.CodeMirror').type(savePageShortcutKey);
+    cy.get('.CodeMirror').contains(body1+body2);
+    cy.get('.page-editor-preview-body').contains(body1+body2);
   })
 
   it('/user/admin is successfully loaded', () => {

+ 1 - 0
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--sticky-features.cy.ts

@@ -116,6 +116,7 @@ context('Access to any page', () => {
       return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
     });
     cy.get('.grw-editor-navbar-bottom').should('be.visible');
+    cy.get('.CodeMirror').should('be.visible');
     cy.screenshot(`${ssPrefix}open-editor-when-sticky`);
   });
 

+ 90 - 0
apps/app/test/cypress/e2e/23-editor/23-editor--template-modal.cy.ts

@@ -0,0 +1,90 @@
+context('TemplateModal', () => {
+
+  const ssPrefix = 'template-modal-';
+
+  beforeEach(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+  });
+
+  it("TemplateModal is shown and closed successfully", () => {
+    cy.visit('/Sandbox/TemplateModal');
+    cy.collapseSidebar(true, true);
+
+    // move to edit mode
+    cy.get('#grw-page-editor-mode-manager').as('pageEditorModeManager').should('be.visible');
+    cy.waitUntil(() => {
+      // do
+      cy.get('@pageEditorModeManager').within(() => {
+        cy.get('button:nth-child(2)').click();
+      });
+      // until
+      return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
+    })
+    cy.get('.grw-editor-navbar-bottom').should('be.visible');
+
+    // show TemplateModal
+    cy.waitUntil(() => {
+      // do
+      cy.get('.navbar-editor > ul > li:nth-child(16) > button').click({force: true});
+      // wait until
+      return cy.getByTestid('template-modal').then($elem => $elem.is(':visible'));
+    });
+
+    // close TemplateModal
+    cy.getByTestid('template-modal').should('be.visible').within(() => {
+      cy.screenshot(`${ssPrefix}opened`);
+      cy.get('button.close').click();
+    });
+
+    cy.screenshot(`${ssPrefix}close`);
+  });
+
+  it("Successfully select template and template locale", () => {
+    cy.visit('/Sandbox/TemplateModal');
+    cy.collapseSidebar(true, true);
+
+    // move to edit mode
+    cy.get('#grw-page-editor-mode-manager').as('pageEditorModeManager').should('be.visible');
+    cy.waitUntil(() => {
+      // do
+      cy.get('@pageEditorModeManager').within(() => {
+        cy.get('button:nth-child(2)').click();
+      });
+      // until
+      return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
+    })
+    cy.get('.grw-editor-navbar-bottom').should('be.visible');
+
+    // show TemplateModal
+    cy.waitUntil(() => {
+      // do
+      cy.get('.navbar-editor > ul > li:nth-child(16) > button').click({force: true});
+      // wait until
+      return cy.getByTestid('template-modal').then($elem => $elem.is(':visible'));
+    });
+
+    // select template and template locale
+    cy.getByTestid('template-modal').should('be.visible').within(() => {
+      // select first template
+      cy.get('.list-group > .list-group-item:nth-child(1)').click();
+      // check preview exist
+      cy.get('.card-body > .page-editor-preview-body > .wiki').should('exist');
+      cy.screenshot(`${ssPrefix}select-template`);
+
+      // change template locale
+      cy.get('.modal-body > div:nth-child(1) > div:nth-child(3) > div:nth-child(1) > div:nth-child(2) > .dropdown > button').click();
+      cy.get('.modal-body > div:nth-child(1) > div:nth-child(3) > div:nth-child(1) > div:nth-child(2) > .dropdown > div > button:nth-child(2)').click();
+      cy.screenshot(`${ssPrefix}select-template-locale`);
+
+      // click insert button
+      cy.get('.modal-footer > button:nth-child(2)').click();
+    });
+
+    // check show template on markdown
+    cy.screenshot(`${ssPrefix}insert-template`);
+  });
+
+});

+ 6 - 0
apps/app/test/cypress/e2e/23-editor/23-editor--with-navigation.cy.ts

@@ -65,10 +65,16 @@ context('Editor while uploading to a new page', () => {
     cy.screenshot(`${ssPrefix}-prevent-grantselector-modified-2`);
 
     // drag-drop a file
+    cy.intercept('POST', '/_api/attachments.add').as('attachmentsAdd');
     const filePath = path.relative('/', path.resolve(Cypress.spec.relative, '../assets/example.txt'));
     cy.get('.dropzone').selectFile(filePath, { action: 'drag-drop' });
+    cy.wait('@attachmentsAdd')
+
+    // Update page using shortcut keys
+    cy.get('.CodeMirror').type('{ctrl+s}');
 
     // expect
+    cy.get('.Toastify__toast').should('contain.text', 'Saved successfully');
     cy.get('.CodeMirror').should('contain.text', body);
     cy.get('.CodeMirror').should('contain.text', '[example.txt](/attachment/');
     cy.getByTestid('grw-grant-selector').find('.dropdown-toggle').should('contain.text', 'Only me');

+ 1 - 1
apps/app/tsconfig.json

@@ -20,7 +20,7 @@
   "include": [
     "next-env.d.ts",
     "config",
-    "src",
+    "src"
   ],
   "ts-node": {
     "transpileOnly": true,

+ 2 - 2
apps/slackbot-proxy/package.json

@@ -1,10 +1,10 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "6.1.5-slackbot-proxy.0",
+  "version": "6.1.7-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "cp:public": "cp -RT ./src/public ./dist/public",
     "cp:views": "cp -RT ./src/views ./dist/views",
     "cp:bootstrap": "cp -RT ./node_modules/bootstrap/dist ./dist/public/bootstrap",

+ 2 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.1.5-RC.0",
+  "version": "6.1.7-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -58,7 +58,6 @@
     "@types/estree": "^1.0.1",
     "@types/node": "^17.0.43",
     "@types/path-browserify": "^1.0.0",
-    "@types/rewire": "^2.5.28",
     "@typescript-eslint/eslint-plugin": "^5.59.7",
     "@typescript-eslint/parser": "^5.59.7",
     "@vitejs/plugin-react": "^3.1.0",
@@ -85,12 +84,12 @@
     "reg-notify-slack-plugin": "^0.11.0",
     "reg-publish-s3-plugin": "^0.11.0",
     "reg-suit": "^0.12.1",
+    "shx": "^0.3.4",
     "stylelint": "^14.2.0",
     "stylelint-config-recess-order": "^3.0.0",
     "ts-node-dev": "^2.0.0",
     "tsconfig-paths": "^3.9.0",
     "typescript": "~4.9",
-    "unplugin-swc": "^1.3.2",
     "vite": "^4.3.8",
     "vite-plugin-dts": "^2.0.0-beta.0",
     "vite-tsconfig-paths": "^4.2.0",

+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "6.1.5-RC.0",
+  "version": "6.1.7-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [
@@ -14,7 +14,7 @@
   ],
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,ts}",

+ 0 - 5
packages/core/src/interfaces/growi-facade.ts

@@ -1,5 +1,3 @@
-import type { ITemplate } from './template';
-
 export type GrowiFacade = {
   markdownRenderer?: {
     optionsGenerators?: {
@@ -10,7 +8,4 @@ export type GrowiFacade = {
     },
     optionsMutators?: any,
   },
-  customTemplates?: {
-    [pluginName: string]: ITemplate,
-  }
 };

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

@@ -9,6 +9,5 @@ export * from './page';
 export * from './revision';
 export * from './subscription';
 export * from './tag';
-export * from './template';
 export * from './user';
 export * from './vite';

+ 0 - 9
packages/core/src/interfaces/template.ts

@@ -1,9 +0,0 @@
-export type ITemplateIdentification = {
-  id: string,
-  locale: string,
-}
-
-export type ITemplate = ITemplateIdentification & {
-  name: string,
-  markdown: string,
-}

+ 2 - 2
packages/hackmd/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/hackmd",
-  "version": "6.1.5-RC.0",
+  "version": "6.1.7-RC.0",
   "description": "GROWI js and css files to use hackmd",
   "license": "MIT",
   "type": "module",
@@ -9,7 +9,7 @@
   "types": "dist/index.d.ts",
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,ts}",

+ 1 - 1
packages/pluginkit/package.json

@@ -7,7 +7,7 @@
   "types": "dist/index.d.ts",
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,ts}",

+ 12 - 0
packages/pluginkit/src/model/growi-plugin-package-data.ts

@@ -0,0 +1,12 @@
+import { GrowiPluginType } from '@growi/core';
+
+export type GrowiPluginDirective = {
+  [key: string]: any,
+  schemaVersion: number,
+  types: GrowiPluginType[],
+}
+
+export type GrowiPluginPackageData = {
+  [key: string]: any,
+  growiPlugin: GrowiPluginDirective,
+}

+ 4 - 1
packages/pluginkit/src/model/growi-plugin-validation-data.ts

@@ -1,8 +1,11 @@
 import { GrowiPluginType } from '@growi/core/dist/consts';
 
+import { GrowiPluginDirective } from './growi-plugin-package-data';
+
 export type GrowiPluginValidationData = {
   projectDirRoot: string,
-  schemaVersion?: number,
+  growiPlugin: GrowiPluginDirective,
+  schemaVersion: number,
   expectedPluginType?: GrowiPluginType,
   actualPluginTypes?: GrowiPluginType[],
 };

+ 1 - 1
packages/pluginkit/src/model/growi-plugin-validation-error.ts

@@ -3,7 +3,7 @@ import ExtensibleCustomError from 'extensible-custom-error';
 import type { GrowiPluginValidationData } from './growi-plugin-validation-data';
 
 
-export class GrowiPluginValidationError<E extends GrowiPluginValidationData = GrowiPluginValidationData> extends ExtensibleCustomError {
+export class GrowiPluginValidationError<E extends Partial<GrowiPluginValidationData> = Partial<GrowiPluginValidationData>> extends ExtensibleCustomError {
 
   data?: E;
 

+ 1 - 0
packages/pluginkit/src/model/index.ts

@@ -1,2 +1,3 @@
+export * from './growi-plugin-package-data';
 export * from './growi-plugin-validation-data';
 export * from './growi-plugin-validation-error';

+ 11 - 0
packages/pluginkit/src/v4/server/utils/common/import-package-json.spec.ts

@@ -0,0 +1,11 @@
+import path from 'path';
+
+import { importPackageJson } from './import-package-json';
+
+it('importPackageJson() returns an object', async() => {
+  // when
+  const pkg = importPackageJson(path.resolve(__dirname, '../../../../../test/fixtures/example-package/template1'));
+
+  // then
+  expect(pkg).not.toBeNull();
+});

+ 9 - 0
packages/pluginkit/src/v4/server/utils/common/import-package-json.ts

@@ -0,0 +1,9 @@
+import { readFileSync } from 'fs';
+import path from 'path';
+
+import type { GrowiPluginPackageData } from '../../../../model';
+
+export const importPackageJson = (projectDirRoot: string): GrowiPluginPackageData => {
+  const packageJsonUrl = path.resolve(projectDirRoot, 'package.json');
+  return JSON.parse(readFileSync(packageJsonUrl, 'utf-8'));
+};

+ 2 - 0
packages/pluginkit/src/v4/server/utils/common/index.ts

@@ -0,0 +1,2 @@
+export * from './import-package-json';
+export * from './validate-growi-plugin-directive';

+ 117 - 0
packages/pluginkit/src/v4/server/utils/common/validate-growi-plugin-directive.spec.ts

@@ -0,0 +1,117 @@
+import { GrowiPluginType } from '@growi/core/dist/consts';
+
+import examplePkg from '../../../../../test/fixtures/example-package/template1/package.json';
+
+
+import { validateGrowiDirective } from './validate-growi-plugin-directive';
+
+const mocks = vi.hoisted(() => {
+  return {
+    importPackageJsonMock: vi.fn(),
+  };
+});
+
+vi.mock('./import-package-json', () => {
+  return { importPackageJson: mocks.importPackageJsonMock };
+});
+
+describe('validateGrowiDirective()', () => {
+
+  it('returns a data object', async() => {
+    // setup
+    mocks.importPackageJsonMock.mockReturnValue(examplePkg);
+
+    // when
+    const data = validateGrowiDirective('package.json');
+
+    // then
+    expect(data).not.toBeNull();
+  });
+
+  it("with the 'expectedPluginType' argument returns a data object", async() => {
+    // setup
+    mocks.importPackageJsonMock.mockReturnValue(examplePkg);
+
+    // when
+    const data = validateGrowiDirective('package.json', GrowiPluginType.Template);
+
+    // then
+    expect(data).not.toBeNull();
+  });
+
+  describe('should throw an GrowiPluginValidationError', () => {
+
+    it("when the pkg does not have 'growiPlugin' directive", () => {
+      // setup
+      mocks.importPackageJsonMock.mockReturnValue({});
+
+      // when
+      const caller = () => { validateGrowiDirective('package.json') };
+
+      // then
+      expect(caller).toThrow("The package.json does not have 'growiPlugin' directive.");
+    });
+
+    it("when the 'schemaVersion' is NaN", () => {
+      // setup
+      mocks.importPackageJsonMock.mockReturnValue({
+        growiPlugin: {
+          schemaVersion: 'foo',
+        },
+      });
+
+      // when
+      const caller = () => { validateGrowiDirective('package.json') };
+
+      // then
+      expect(caller).toThrow("The growiPlugin directive must have a valid 'schemaVersion' directive.");
+    });
+
+    it("when the 'schemaVersion' is less than 4", () => {
+      // setup
+      mocks.importPackageJsonMock.mockReturnValue({
+        growiPlugin: {
+          schemaVersion: 3,
+        },
+      });
+
+      // when
+      const caller = () => { validateGrowiDirective('package.json') };
+
+      // then
+      expect(caller).toThrow("The growiPlugin directive must have a valid 'schemaVersion' directive.");
+    });
+
+    it("when the 'types' directive does not exist", () => {
+      // setup
+      mocks.importPackageJsonMock.mockReturnValue({
+        growiPlugin: {
+          schemaVersion: 4,
+        },
+      });
+
+      // when
+      const caller = () => { validateGrowiDirective('package.json') };
+
+      // then
+      expect(caller).toThrow("The growiPlugin directive does not have 'types' directive.");
+    });
+
+    it("when the 'types' directive does not have expected plugin type", () => {
+      // setup
+      mocks.importPackageJsonMock.mockReturnValue({
+        growiPlugin: {
+          schemaVersion: 4,
+          types: [GrowiPluginType.Template],
+        },
+      });
+
+      // when
+      const caller = () => { validateGrowiDirective('package.json', GrowiPluginType.Script) };
+
+      // then
+      expect(caller).toThrow("The growiPlugin directive does not have expected plugin type in 'types' directive.");
+    });
+  });
+
+});

+ 6 - 6
packages/pluginkit/src/v4/server/utils/package-json/validate.ts → packages/pluginkit/src/v4/server/utils/common/validate-growi-plugin-directive.ts

@@ -1,17 +1,17 @@
 import { GrowiPluginType } from '@growi/core/dist/consts';
 
-import { type GrowiPluginValidationData, GrowiPluginValidationError } from '~/model';
+import { type GrowiPluginValidationData, GrowiPluginValidationError } from '../../../../model';
 
-import { importPackageJson } from './import';
+import { importPackageJson } from './import-package-json';
 
 
-export const validatePackageJson = async(projectDirRoot: string, expectedPluginType?: GrowiPluginType): Promise<GrowiPluginValidationData> => {
-  const pkg = await importPackageJson(projectDirRoot);
-
-  const data: GrowiPluginValidationData = { projectDirRoot };
+export const validateGrowiDirective = (projectDirRoot: string, expectedPluginType?: GrowiPluginType): GrowiPluginValidationData => {
+  const pkg = importPackageJson(projectDirRoot);
 
   const { growiPlugin } = pkg;
 
+  const data: GrowiPluginValidationData = { projectDirRoot, schemaVersion: NaN, growiPlugin };
+
   if (growiPlugin == null) {
     throw new GrowiPluginValidationError("The package.json does not have 'growiPlugin' directive.", data);
   }

+ 1 - 1
packages/pluginkit/src/v4/server/utils/index.ts

@@ -1,2 +1,2 @@
-export * from './package-json';
+export * from './common';
 export * from './template';

+ 0 - 11
packages/pluginkit/src/v4/server/utils/package-json/import.spec.ts

@@ -1,11 +0,0 @@
-import path from 'path';
-
-import { importPackageJson } from './import';
-
-it('importPackageJson() returns an object', async() => {
-  // when
-  const pkg = await importPackageJson(path.resolve(__dirname, '../../../../../test/fixtures/example-package/template1'));
-
-  // then
-  expect(pkg).not.toBeNull();
-});

+ 0 - 6
packages/pluginkit/src/v4/server/utils/package-json/import.ts

@@ -1,6 +0,0 @@
-import path from 'path';
-
-export const importPackageJson = async(projectDirRoot: string): Promise<any> => {
-  const packageJsonUrl = path.resolve(projectDirRoot, 'package.json');
-  return import(packageJsonUrl);
-};

+ 0 - 2
packages/pluginkit/src/v4/server/utils/package-json/index.ts

@@ -1,2 +0,0 @@
-export * from './import';
-export * from './validate';

+ 0 - 117
packages/pluginkit/src/v4/server/utils/package-json/validate.spec.ts

@@ -1,117 +0,0 @@
-import { GrowiPluginType } from '@growi/core/dist/consts';
-
-import examplePkg from '^/test/fixtures/example-package/template1/package.json';
-
-
-import { validatePackageJson } from './validate';
-
-const mocks = vi.hoisted(() => {
-  return {
-    importPackageJsonMock: vi.fn(),
-  };
-});
-
-vi.mock('./import', () => {
-  return { importPackageJson: mocks.importPackageJsonMock };
-});
-
-describe('validatePackageJson()', () => {
-
-  it('returns a data object', async() => {
-    // setup
-    mocks.importPackageJsonMock.mockResolvedValue(examplePkg);
-
-    // when
-    const data = await validatePackageJson('package.json');
-
-    // then
-    expect(data).not.toBeNull();
-  });
-
-  it("with the 'expectedPluginType' argument returns a data object", async() => {
-    // setup
-    mocks.importPackageJsonMock.mockResolvedValue(examplePkg);
-
-    // when
-    const data = await validatePackageJson('package.json', GrowiPluginType.Template);
-
-    // then
-    expect(data).not.toBeNull();
-  });
-
-  describe('should throw an GrowiPluginValidationError', () => {
-
-    it("when the pkg does not have 'growiPlugin' directive", async() => {
-      // setup
-      mocks.importPackageJsonMock.mockResolvedValue({});
-
-      // when
-      const caller = async() => { await validatePackageJson('package.json') };
-
-      // then
-      await expect(caller).rejects.toThrow("The package.json does not have 'growiPlugin' directive.");
-    });
-
-    it("when the 'schemaVersion' is NaN", async() => {
-      // setup
-      mocks.importPackageJsonMock.mockResolvedValue({
-        growiPlugin: {
-          schemaVersion: 'foo',
-        },
-      });
-
-      // when
-      const caller = async() => { await validatePackageJson('package.json') };
-
-      // then
-      await expect(caller).rejects.toThrow("The growiPlugin directive must have a valid 'schemaVersion' directive.");
-    });
-
-    it("when the 'schemaVersion' is less than 4", async() => {
-      // setup
-      mocks.importPackageJsonMock.mockResolvedValue({
-        growiPlugin: {
-          schemaVersion: 3,
-        },
-      });
-
-      // when
-      const caller = async() => { await validatePackageJson('package.json') };
-
-      // then
-      await expect(caller).rejects.toThrow("The growiPlugin directive must have a valid 'schemaVersion' directive.");
-    });
-
-    it("when the 'types' directive does not exist", async() => {
-      // setup
-      mocks.importPackageJsonMock.mockResolvedValue({
-        growiPlugin: {
-          schemaVersion: 4,
-        },
-      });
-
-      // when
-      const caller = async() => { await validatePackageJson('package.json') };
-
-      // then
-      await expect(caller).rejects.toThrow("The growiPlugin directive does not have 'types' directive.");
-    });
-
-    it("when the 'types' directive does not have expected plugin type", async() => {
-      // setup
-      mocks.importPackageJsonMock.mockResolvedValue({
-        growiPlugin: {
-          schemaVersion: 4,
-          types: [GrowiPluginType.Template],
-        },
-      });
-
-      // when
-      const caller = async() => { await validatePackageJson('package.json', GrowiPluginType.Script) };
-
-      // then
-      await expect(caller).rejects.toThrow("The growiPlugin directive does not have expected plugin type in 'types' directive.");
-    });
-  });
-
-});

+ 0 - 205
packages/pluginkit/src/v4/server/utils/template.ts

@@ -1,205 +0,0 @@
-import fs from 'fs';
-import path from 'path';
-import { promisify } from 'util';
-
-import { GrowiPluginType } from '@growi/core/dist/consts';
-
-import type { GrowiPluginValidationData, GrowiTemplatePluginValidationData } from '~/model';
-import { GrowiPluginValidationError } from '~/model';
-
-import { isTemplateStatusValid, type TemplateStatus, type TemplateSummary } from '../../interfaces';
-
-import { importPackageJson, validatePackageJson } from './package-json';
-
-
-const statAsync = promisify(fs.stat);
-
-
-/**
- * An utility for template plugin which wrap 'validatePackageJson' of './package-json.ts' module
- * @param projectDirRoot
- */
-export const validateTemplatePluginPackageJson = async(projectDirRoot: string): Promise<GrowiTemplatePluginValidationData> => {
-  const data = await validatePackageJson(projectDirRoot, GrowiPluginType.Template);
-
-  const pkg = await importPackageJson(projectDirRoot);
-
-  // check supporting locales
-  const supportingLocales: string[] | undefined = pkg.growiPlugin.locales;
-  if (supportingLocales == null || supportingLocales.length === 0) {
-    throw new GrowiPluginValidationError<GrowiPluginValidationData & { supportingLocales?: string[] }>(
-      "Template plugin must have 'supportingLocales' and that must have one or more locales",
-      {
-        ...data,
-        supportingLocales,
-      },
-    );
-  }
-
-  return {
-    ...data,
-    supportingLocales,
-  };
-};
-
-
-type TemplateDirStatus = {
-  isTemplateExists: boolean,
-  meta?: { [key: string]: string },
-}
-
-async function getStats(tplDir: string): Promise<TemplateDirStatus> {
-  const markdownPath = path.resolve(tplDir, 'template.md');
-  const statForMarkdown = await statAsync(markdownPath);
-  const isTemplateExists = statForMarkdown.isFile();
-
-  const metaDataPath = path.resolve(tplDir, 'meta.json');
-  const statForMetaDataFile = await statAsync(metaDataPath);
-  const isMetaDataFileExists = statForMetaDataFile.isFile();
-
-  const result: TemplateDirStatus = {
-    isTemplateExists,
-    meta: isMetaDataFileExists ? await import(metaDataPath) : undefined,
-  };
-
-  return result;
-}
-
-
-export const scanTemplateStatus = async(
-    projectDirRoot: string,
-    templateId: string,
-    data: GrowiTemplatePluginValidationData,
-    opts?: {
-      pluginId?: string,
-    },
-): Promise<TemplateStatus[]> => {
-  const status: TemplateStatus[] = [];
-
-  const tplRootDirPath = path.resolve(projectDirRoot, 'dist', templateId);
-
-  let isDefaultPushed = false;
-  for await (const locale of data.supportingLocales) {
-    const tplDir = path.resolve(tplRootDirPath, locale);
-
-    try {
-      const stats = await getStats(tplDir);
-      const {
-        isTemplateExists, meta,
-      } = stats;
-
-      if (!isTemplateExists) throw new Error("'template.md does not exist.");
-      if (meta == null) throw new Error("'meta.md does not exist.");
-      if (meta?.title == null) throw new Error("'meta.md does not contain the title.");
-
-      const isDefault = !isDefaultPushed;
-      status.push({
-        pluginId: opts?.pluginId,
-        id: templateId,
-        locale,
-        isValid: true,
-        isDefault,
-        title: meta.title,
-        desc: meta.desc,
-      });
-      isDefaultPushed = true;
-    }
-    catch (err) {
-      status.push({
-        pluginId: opts?.pluginId,
-        id: templateId,
-        locale,
-        isValid: false,
-        invalidReason: err.message,
-      });
-    }
-  }
-
-  // eslint-disable-next-line no-console
-  console.debug({ status });
-
-  return status;
-};
-
-export const scanAllTemplateStatus = async(
-    projectDirRoot: string,
-    opts?: {
-      data?: GrowiTemplatePluginValidationData,
-      pluginId?: string,
-      returnsInvalidTemplates?: boolean,
-    },
-): Promise<TemplateSummary[]> => {
-
-  const data = opts?.data ?? await validateTemplatePluginPackageJson(projectDirRoot);
-
-  const summaries: TemplateSummary[] = [];
-
-  const distDirPath = path.resolve(projectDirRoot, 'dist');
-  const distDirFiles = fs.readdirSync(distDirPath);
-
-  for await (const templateId of distDirFiles) {
-    const status = (await scanTemplateStatus(projectDirRoot, templateId, data, { pluginId: opts?.pluginId }))
-      // omit invalid templates if `returnsInvalidTemplates` is true
-      .filter(s => (opts?.returnsInvalidTemplates ? true : s.isValid));
-
-    // determine default locale
-    const defaultTemplateStatus = status.find(s => 'isDefault' in s && s.isDefault);
-
-    if (defaultTemplateStatus == null || !isTemplateStatusValid(defaultTemplateStatus)) {
-      continue;
-    }
-
-    summaries.push({
-      // for the 'default' key
-      default: defaultTemplateStatus,
-      // for each locale keys
-      ...Object.fromEntries(status.map(templateStatus => [templateStatus.locale, templateStatus])),
-    });
-  }
-
-  return summaries;
-};
-
-export const validateTemplatePlugin = async(projectDirRoot: string): Promise<boolean> => {
-  const data = await validateTemplatePluginPackageJson(projectDirRoot);
-
-  const results = await scanAllTemplateStatus(projectDirRoot, { data, returnsInvalidTemplates: true });
-
-  if (Object.keys(results).length === 0) {
-    throw new Error('This plugin does not have any templates');
-  }
-
-  // construct map
-  // key: id
-  // value: isValid properties
-  const idValidMap: { [id: string]: boolean[] } = {};
-  results.forEach((summary) => {
-    idValidMap[summary.default.id] = Object.values(summary).map(s => s?.isValid ?? false);
-  });
-
-  for (const [id, validMap] of Object.entries(idValidMap)) {
-    // warn
-    if (!validMap.every(bool => bool)) {
-      // eslint-disable-next-line no-console
-      console.warn(`[WARN] Template '${id}' has some locales that status is invalid`);
-    }
-
-    // This means the template directory does not have any valid template
-    if (!validMap.some(bool => bool)) {
-      return false;
-    }
-  }
-
-  return true;
-};
-
-export const getMarkdown = async(projectDirRoot: string, templateId: string, locale: string): Promise<string> => {
-  const tplDir = path.resolve(projectDirRoot, 'dist', templateId, locale);
-
-  const { isTemplateExists } = await getStats(tplDir);
-
-  if (!isTemplateExists) throw new Error("'template.md does not exist.");
-
-  const markdownPath = path.resolve(tplDir, 'template.md');
-  return fs.readFileSync(markdownPath, { encoding: 'utf-8' });
-};

+ 16 - 0
packages/pluginkit/src/v4/server/utils/template/get-markdown.ts

@@ -0,0 +1,16 @@
+import fs from 'fs';
+import path from 'path';
+
+import { getStatus } from './get-status';
+
+
+export const getMarkdown = async(projectDirRoot: string, templateId: string, locale: string): Promise<string> => {
+  const tplDir = path.resolve(projectDirRoot, 'dist', templateId, locale);
+
+  const { isTemplateExists } = await getStatus(tplDir);
+
+  if (!isTemplateExists) throw new Error("'template.md does not exist.");
+
+  const markdownPath = path.resolve(tplDir, 'template.md');
+  return fs.readFileSync(markdownPath, { encoding: 'utf-8' });
+};

+ 29 - 0
packages/pluginkit/src/v4/server/utils/template/get-status.ts

@@ -0,0 +1,29 @@
+import fs, { readFileSync } from 'fs';
+import path from 'path';
+import { promisify } from 'util';
+
+
+const statAsync = promisify(fs.stat);
+
+
+type TemplateDirStatus = {
+  isTemplateExists: boolean,
+  meta?: { [key: string]: string },
+}
+
+export async function getStatus(tplDir: string): Promise<TemplateDirStatus> {
+  const markdownPath = path.resolve(tplDir, 'template.md');
+  const statForMarkdown = await statAsync(markdownPath);
+  const isTemplateExists = statForMarkdown.isFile();
+
+  const metaDataPath = path.resolve(tplDir, 'meta.json');
+  const statForMetaDataFile = await statAsync(metaDataPath);
+  const isMetaDataFileExists = statForMetaDataFile.isFile();
+
+  const result: TemplateDirStatus = {
+    isTemplateExists,
+    meta: isMetaDataFileExists ? JSON.parse(readFileSync(metaDataPath, 'utf-8')) : undefined,
+  };
+
+  return result;
+}

+ 4 - 0
packages/pluginkit/src/v4/server/utils/template/index.ts

@@ -0,0 +1,4 @@
+export * from './get-markdown';
+export * from './scan';
+export * from './validate-all-locales';
+export * from './validate-growi-plugin-directive';

+ 103 - 0
packages/pluginkit/src/v4/server/utils/template/scan.ts

@@ -0,0 +1,103 @@
+import fs from 'fs';
+import path from 'path';
+
+import type { GrowiTemplatePluginValidationData } from '../../../../model';
+import { isTemplateStatusValid, type TemplateStatus, type TemplateSummary } from '../../../interfaces';
+
+import { getStatus } from './get-status';
+import { validateTemplatePluginGrowiDirective } from './validate-growi-plugin-directive';
+
+
+export const scanTemplate = async(
+    projectDirRoot: string,
+    templateId: string,
+    data: GrowiTemplatePluginValidationData,
+    opts?: {
+      pluginId?: string,
+    },
+): Promise<TemplateStatus[]> => {
+  const status: TemplateStatus[] = [];
+
+  const tplRootDirPath = path.resolve(projectDirRoot, 'dist', templateId);
+
+  let isDefaultPushed = false;
+  for await (const locale of data.supportingLocales) {
+    const tplDir = path.resolve(tplRootDirPath, locale);
+
+    try {
+      const stats = await getStatus(tplDir);
+      const {
+        isTemplateExists, meta,
+      } = stats;
+
+      if (!isTemplateExists) throw new Error("'template.md does not exist.");
+      if (meta == null) throw new Error("'meta.md does not exist.");
+      if (meta?.title == null) throw new Error("'meta.md does not contain the title.");
+
+      const isDefault = !isDefaultPushed;
+      status.push({
+        pluginId: opts?.pluginId,
+        id: templateId,
+        locale,
+        isValid: true,
+        isDefault,
+        title: meta.title,
+        desc: meta.desc,
+      });
+      isDefaultPushed = true;
+    }
+    catch (err) {
+      status.push({
+        pluginId: opts?.pluginId,
+        id: templateId,
+        locale,
+        isValid: false,
+        invalidReason: err.message,
+      });
+    }
+  }
+
+  // eslint-disable-next-line no-console
+  console.debug(`Template directory (${projectDirRoot}) has scanned`, { status });
+
+  return status;
+};
+
+export const scanAllTemplates = async(
+    projectDirRoot: string,
+    opts?: {
+      data?: GrowiTemplatePluginValidationData,
+      pluginId?: string,
+      returnsInvalidTemplates?: boolean,
+    },
+): Promise<TemplateSummary[]> => {
+
+  const data = opts?.data ?? validateTemplatePluginGrowiDirective(projectDirRoot);
+
+  const summaries: TemplateSummary[] = [];
+
+  const distDirPath = path.resolve(projectDirRoot, 'dist');
+  const distDirFiles = fs.readdirSync(distDirPath);
+
+  for await (const templateId of distDirFiles) {
+    const status = (await scanTemplate(projectDirRoot, templateId, data, { pluginId: opts?.pluginId }))
+      // omit invalid templates if `returnsInvalidTemplates` is true
+      .filter(s => (opts?.returnsInvalidTemplates ? true : s.isValid));
+
+    // determine default locale
+    const defaultTemplateStatus = status.find(s => 'isDefault' in s && s.isDefault);
+
+    if (defaultTemplateStatus == null || !isTemplateStatusValid(defaultTemplateStatus)) {
+      continue;
+    }
+
+    summaries.push({
+      // for the 'default' key
+      default: defaultTemplateStatus,
+      // for each locale keys
+      ...Object.fromEntries(status.map(templateStatus => [templateStatus.locale, templateStatus])),
+    });
+  }
+
+  return summaries;
+};

+ 36 - 0
packages/pluginkit/src/v4/server/utils/template/validate-all-locales.ts

@@ -0,0 +1,36 @@
+import { scanAllTemplates } from './scan';
+import { validateTemplatePluginGrowiDirective } from './validate-growi-plugin-directive';
+
+
+export const validateAllTemplateLocales = async(projectDirRoot: string): Promise<boolean> => {
+  const data = validateTemplatePluginGrowiDirective(projectDirRoot);
+
+  const results = await scanAllTemplates(projectDirRoot, { data, returnsInvalidTemplates: true });
+
+  if (Object.keys(results).length === 0) {
+    throw new Error('This plugin does not have any templates');
+  }
+
+  // construct map
+  // key: id
+  // value: isValid properties
+  const idValidMap: { [id: string]: boolean[] } = {};
+  results.forEach((summary) => {
+    idValidMap[summary.default.id] = Object.values(summary).map(s => s?.isValid ?? false);
+  });
+
+  for (const [id, validMap] of Object.entries(idValidMap)) {
+    // warn
+    if (!validMap.every(bool => bool)) {
+      // eslint-disable-next-line no-console
+      console.warn(`[WARN] Template '${id}' has some locales that status is invalid`);
+    }
+
+    // This means the template directory does not have any valid template
+    if (!validMap.some(bool => bool)) {
+      return false;
+    }
+  }
+
+  return true;
+};

+ 33 - 0
packages/pluginkit/src/v4/server/utils/template/validate-growi-plugin-directive.ts

@@ -0,0 +1,33 @@
+import { GrowiPluginType } from '@growi/core/dist/consts';
+
+import type { GrowiPluginValidationData, GrowiTemplatePluginValidationData } from '../../../../model';
+import { GrowiPluginValidationError } from '../../../../model';
+import { validateGrowiDirective } from '../common';
+
+
+/**
+ * An utility for template plugin which wrap 'validateGrowiDirective' of './common' module
+ * @param projectDirRoot
+ */
+export const validateTemplatePluginGrowiDirective = (projectDirRoot: string): GrowiTemplatePluginValidationData => {
+  const data = validateGrowiDirective(projectDirRoot, GrowiPluginType.Template);
+
+  const { growiPlugin } = data;
+
+  // check supporting locales
+  const supportingLocales: string[] | undefined = growiPlugin.locales;
+  if (supportingLocales == null || supportingLocales.length === 0) {
+    throw new GrowiPluginValidationError<GrowiPluginValidationData & { supportingLocales?: string[] }>(
+      "Template plugin must have 'supportingLocales' and that must have one or more locales",
+      {
+        ...data,
+        supportingLocales,
+      },
+    );
+  }
+
+  return {
+    ...data,
+    supportingLocales,
+  };
+};

+ 9 - 1
packages/pluginkit/src/v4/utils/template.ts

@@ -5,7 +5,15 @@ export const getLocalizedTemplate = (templateSummary: TemplateSummary | undefine
     return undefined;
   }
 
-  return locale != null
+  return locale != null && locale in templateSummary
     ? templateSummary[locale]
     : templateSummary.default;
 };
+
+export const extractSupportedLocales = (templateSummary: TemplateSummary | undefined): Set<string> | undefined => {
+  if (templateSummary == null) {
+    return undefined;
+  }
+
+  return new Set(Object.values(templateSummary).map(s => s.locale));
+};

+ 1 - 7
packages/pluginkit/tsconfig.json

@@ -4,14 +4,8 @@
   "compilerOptions": {
     "module": "CommonJS",
     "types": [
-      "node",
       "vitest/globals"
-    ],
-    "baseUrl": ".",
-    "paths": {
-      "^/*": ["./*"],
-      "~/*": ["./src/*"]
-    }
+    ]
   },
   "include": [
     "src"

+ 0 - 2
packages/pluginkit/vite.config.ts

@@ -4,13 +4,11 @@ import path from 'path';
 import glob from 'glob';
 import { defineConfig } from 'vite';
 import dts from 'vite-plugin-dts';
-import tsconfigPaths from 'vite-tsconfig-paths';
 
 // https://vitejs.dev/config/
 export default defineConfig({
   plugins: [
     dts(),
-    tsconfigPaths(),
   ],
   build: {
     outDir: 'dist',

+ 2 - 2
packages/presentation/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/presentation",
-  "version": "6.1.5-RC.0",
+  "version": "6.1.7-RC.0",
   "description": "GROWI plugin for presentation",
   "license": "MIT",
   "keywords": [
@@ -14,7 +14,7 @@
   ],
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,jsx,ts,tsx}",

+ 1 - 1
packages/preset-templates/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/preset-templates",
-  "version": "6.1.5-RC.0",
+  "version": "6.1.7-RC.0",
   "scripts": {
     "test": "vitest run",
     "version": "yarn version --no-git-tag-version --preid=RC"

+ 24 - 7
packages/preset-templates/test/index.test.ts

@@ -1,6 +1,6 @@
 import path from 'node:path';
 
-import { scanAllTemplateStatus, validateTemplatePluginPackageJson, validateTemplatePlugin } from '@growi/pluginkit/dist/v4/server';
+import { scanAllTemplates, validateTemplatePluginGrowiDirective, validateAllTemplateLocales } from '@growi/pluginkit/dist/v4/server';
 
 
 const projectDirRoot = path.resolve(__dirname, '../');
@@ -9,27 +9,44 @@ const projectDirRoot = path.resolve(__dirname, '../');
 it('Validation for package.json should be passed', () => {
 
   // when
-  const caller = () => validateTemplatePluginPackageJson(projectDirRoot);
+  const caller = () => validateTemplatePluginGrowiDirective(projectDirRoot);
 
   // then
   expect(caller).not.toThrow();
 });
 
+it('Validation for package.json should be return data', () => {
+
+  // when
+  const data = validateTemplatePluginGrowiDirective(projectDirRoot);
+
+  // then
+  expect(data).not.toBeNull();
+});
+
 it('Scanning the templates ends up with no errors', async() => {
+  // when
+  const results = await scanAllTemplates(projectDirRoot);
+
+  // then
+  expect(results).not.toBeNull();
+});
+
+it('Scanning the templates ends up with no errors with opts.data', async() => {
 
   // setup
-  const data = await validateTemplatePluginPackageJson(projectDirRoot);
+  const data = validateTemplatePluginGrowiDirective(projectDirRoot);
 
   // when
-  const caller = () => scanAllTemplateStatus(projectDirRoot, data);
+  const results = await scanAllTemplates(projectDirRoot, { data });
 
   // then
-  expect(caller).not.toThrow();
+  expect(results).not.toBeNull();
 });
 
-it('Validation templates returns true', async() => {
+it('Validation templates returns true', () => {
   // when
-  const result = await validateTemplatePlugin(projectDirRoot);
+  const result = validateAllTemplateLocales(projectDirRoot);
 
   // then
   expect(result).toBeTruthy();

+ 1 - 1
packages/preset-themes/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
-  "version": "6.1.5-RC.0",
+  "version": "6.1.7-RC.0",
   "license": "MIT",
   "main": "dist/libs/preset-themes.umd.js",
   "module": "dist/libs/preset-themes.mjs",

+ 2 - 2
packages/remark-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-attachment-refs",
-  "version": "6.1.5-RC.0",
+  "version": "6.1.7-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [
@@ -17,7 +17,7 @@
     "build": "run-p build:*",
     "build:server": "vite build -c vite.server.config.ts",
     "build:client": "vite build -c vite.client.config.ts",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "run-p dev:*",
     "dev:server": "vite build -c vite.server.config.ts --mode dev",
     "dev:client": "vite build -c vite.client.config.ts --mode dev",

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-drawio",
-  "version": "6.1.5-RC.0",
+  "version": "6.1.7-RC.0",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "keywords": [
@@ -21,7 +21,7 @@
   "typings": "dist/index.d.ts",
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,jsx,ts,tsx}",

+ 2 - 2
packages/remark-growi-directive/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-growi-directive",
-  "version": "6.1.5-RC.0",
+  "version": "6.1.7-RC.0",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "keywords": [
@@ -17,7 +17,7 @@
   "typings": "dist/index.d.ts",
   "scripts": {
     "build": "yarn tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "yarn build",
     "watch": "yarn tsc -w",
     "test": "cross-env NODE_ENV=test npm run test-coverage",

+ 2 - 2
packages/remark-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-lsx",
-  "version": "6.1.5-RC.0",
+  "version": "6.1.7-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [
@@ -14,7 +14,7 @@
     "build": "run-p build:*",
     "build:client": "vite build -c vite.client.config.ts",
     "build:server": "vite build -c vite.server.config.ts",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "run-p dev:*",
     "dev:client": "vite build -c vite.client.config.ts --mode dev",
     "dev:server": "vite build -c vite.server.config.ts --mode dev",

+ 1 - 1
packages/remark-lsx/src/server/routes/list-pages/index.ts

@@ -52,7 +52,7 @@ function addFilterCondition(query, pagePath, optionsFilter, isExceptFilter = fal
 }
 
 function addExceptCondition(query, pagePath, optionsFilter): PageQuery {
-  return this.addFilterCondition(query, pagePath, optionsFilter, true);
+  return addFilterCondition(query, pagePath, optionsFilter, true);
 }
 
 

+ 0 - 1
packages/remark-lsx/tsconfig.json

@@ -6,7 +6,6 @@
 
     "plugins": [{ "name": "typescript-plugin-css-modules" }],
 
-    "typeRoots": ["./src/@types"],
     "types": [
       "vitest/globals"
     ]

+ 2 - 2
packages/slack/package.json

@@ -1,13 +1,13 @@
 {
   "name": "@growi/slack",
-  "version": "6.1.5-RC.0",
+  "version": "6.1.7-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "module": "dist/index.mjs",
   "types": "dist/index.d.ts",
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,ts}",

+ 2 - 2
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "6.1.5-RC.0",
+  "version": "6.1.7-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [
@@ -12,7 +12,7 @@
   ],
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,ts}",

+ 1 - 1
packages/ui/tsconfig.json

@@ -6,7 +6,7 @@
 
     "baseUrl": ".",
     "paths": {
-      "~/*": ["./src/*"],
+      "~/*": ["./src/*"]
     }
   },
   "include": [

+ 0 - 3
tsconfig.base.json

@@ -25,9 +25,6 @@
 
     /* Module Resolution Options */
     "moduleResolution": "node",
-    "typeRoots": [
-      "./node_modules/@types"
-    ],
     "allowSyntheticDefaultImports": true,
     "esModuleInterop": true,
 

+ 471 - 45
yarn.lock

@@ -2312,13 +2312,13 @@
     xdg-basedir "^4.0.0"
 
 "@growi/core@link:packages/core":
-  version "6.1.5-RC.0"
+  version "6.1.7-RC.0"
   dependencies:
     bson-objectid "^2.0.4"
     escape-string-regexp "^4.0.0"
 
 "@growi/hackmd@link:packages/hackmd":
-  version "6.1.5-RC.0"
+  version "6.1.7-RC.0"
 
 "@growi/pluginkit@link:packages/pluginkit":
   version "0.1.0"
@@ -2327,18 +2327,18 @@
     extensible-custom-error "^0.0.7"
 
 "@growi/presentation@link:packages/presentation":
-  version "6.1.5-RC.0"
+  version "6.1.7-RC.0"
   dependencies:
     "@growi/core" "link:packages/core"
 
 "@growi/preset-templates@link:packages/preset-templates":
-  version "6.1.5-RC.0"
+  version "6.1.7-RC.0"
 
 "@growi/preset-themes@link:packages/preset-themes":
-  version "6.1.5-RC.0"
+  version "6.1.7-RC.0"
 
 "@growi/remark-attachment-refs@link:packages/remark-attachment-refs":
-  version "6.1.5-RC.0"
+  version "6.1.7-RC.0"
   dependencies:
     "@growi/core" "link:packages/core"
     "@growi/remark-growi-directive" "link:packages/remark-growi-directive"
@@ -2347,12 +2347,12 @@
     universal-bunyan "^0.9.2"
 
 "@growi/remark-drawio@link:packages/remark-drawio":
-  version "6.1.5-RC.0"
+  version "6.1.7-RC.0"
   dependencies:
     pako "^2.1.0"
 
 "@growi/remark-growi-directive@link:packages/remark-growi-directive":
-  version "6.1.5-RC.0"
+  version "6.1.7-RC.0"
   dependencies:
     "@types/mdast" "^3.0.0"
     "@types/unist" "^2.0.0"
@@ -2369,7 +2369,7 @@
     uvu "^0.5.0"
 
 "@growi/remark-lsx@link:packages/remark-lsx":
-  version "6.1.5-RC.0"
+  version "6.1.7-RC.0"
   dependencies:
     "@growi/core" "link:packages/core"
     "@growi/remark-growi-directive" "link:packages/remark-growi-directive"
@@ -2380,7 +2380,7 @@
     swr "^2.0.3"
 
 "@growi/slack@link:packages/slack":
-  version "6.1.5-RC.0"
+  version "6.1.7-RC.0"
   dependencies:
     "@slack/oauth" "^2.0.1"
     axios "^0.24.0"
@@ -2393,7 +2393,7 @@
     url-join "^4.0.0"
 
 "@growi/ui@link:packages/ui":
-  version "6.1.5-RC.0"
+  version "6.1.7-RC.0"
   dependencies:
     "@growi/core" "link:packages/core"
 
@@ -3875,6 +3875,13 @@
   dependencies:
     "@types/unist" "*"
 
+"@types/mdast@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.0.tgz#9f9462d4584a8b3e3711ea8bb4a94c485559ab90"
+  integrity sha512-YLeG8CujC9adtj/kuDzq1N4tCDYKoZ5l/bnjq8d74+t/3q/tHquJOJKUQXJrLCflOHpKjXgcI/a929gpmLOEng==
+  dependencies:
+    "@types/unist" "*"
+
 "@types/mime-types@^2.1.0":
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.1.tgz#d9ba43490fa3a3df958759adf69396c3532cf2c1"
@@ -4006,11 +4013,6 @@
   resolved "https://registry.yarnpkg.com/@types/reveal.js/-/reveal.js-4.4.1.tgz#970e29b6ed6c07ad693797d5157ba2fb3e94840c"
   integrity sha512-oMcIAaP9rFCODHdGC2RM71cbBGOPc4prolkPEN98LUQm9b64ePjNpD/Fb2b/oep2w0XDN4aYCdUmip6kuLEJBg==
 
-"@types/rewire@^2.5.28":
-  version "2.5.28"
-  resolved "https://registry.yarnpkg.com/@types/rewire/-/rewire-2.5.28.tgz#ff34de38c4269fe74e2597195d4918c25d42ebad"
-  integrity sha512-uD0j/AQOa5le7afuK+u+woi8jNKF1vf3DN0H7LCJhft/lNNibUr7VcAesdgtWfEKveZol3ZG1CJqwx2Bhrnl8w==
-
 "@types/scheduler@*":
   version "0.16.2"
   resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
@@ -4066,6 +4068,11 @@
   resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
   integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
 
+"@types/unist@^3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.0.tgz#988ae8af1e5239e89f9fbb1ade4c935f4eeedf9a"
+  integrity sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==
+
 "@types/warning@^3.0.0":
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52"
@@ -5715,7 +5722,7 @@ check-node-version@^4.1.0:
     run-parallel "^1.1.4"
     semver "^6.3.0"
 
-"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.0, chokidar@^3.5.1, chokidar@^3.5.3:
+"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.0, chokidar@^3.5.1:
   version "3.5.3"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
   integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@@ -7066,6 +7073,13 @@ detect-newline@^3.0.0:
   resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
   integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
 
+devlop@^1.0.0, devlop@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018"
+  integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==
+  dependencies:
+    dequal "^2.0.0"
+
 dezalgo@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456"
@@ -8847,7 +8861,7 @@ glob@^6.0.1:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0:
+glob@^7.0.0, glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0:
   version "7.2.3"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
   integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@@ -9715,6 +9729,11 @@ internal-slot@^1.0.3:
   resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
   integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
 
+interpret@^1.0.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
+  integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
+
 invariant@^2.2.1, invariant@^2.2.4:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@@ -9850,6 +9869,13 @@ is-core-module@^2.1.0, is-core-module@^2.2.0, is-core-module@^2.8.1, is-core-mod
   dependencies:
     has "^1.0.3"
 
+is-core-module@^2.11.0:
+  version "2.12.1"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd"
+  integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==
+  dependencies:
+    has "^1.0.3"
+
 is-data-descriptor@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -11607,6 +11633,16 @@ mdast-util-find-and-replace@^2.0.0:
     unist-util-is "^5.0.0"
     unist-util-visit-parents "^5.0.0"
 
+mdast-util-find-and-replace@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.0.tgz#ca0a9232afdc53f4260582ac2d0227cf94b4a220"
+  integrity sha512-8wLPIKAvGdA5jgkI8AYKfSorV3og3vE6HA+gKeKEZydbi1EtUu2g4XCxIBj3R+AsFqY/uRtoYbH30tiWsFKkBQ==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+    escape-string-regexp "^5.0.0"
+    unist-util-is "^6.0.0"
+    unist-util-visit-parents "^6.0.0"
+
 mdast-util-from-markdown@^1.0.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz#84df2924ccc6c995dec1e2368b2b208ad0a76268"
@@ -11625,7 +11661,25 @@ mdast-util-from-markdown@^1.0.0:
     unist-util-stringify-position "^3.0.0"
     uvu "^0.5.0"
 
-mdast-util-frontmatter@^1.0.0:
+mdast-util-from-markdown@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz#52f14815ec291ed061f2922fd14d6689c810cb88"
+  integrity sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+    "@types/unist" "^3.0.0"
+    decode-named-character-reference "^1.0.0"
+    devlop "^1.0.0"
+    mdast-util-to-string "^4.0.0"
+    micromark "^4.0.0"
+    micromark-util-decode-numeric-character-reference "^2.0.0"
+    micromark-util-decode-string "^2.0.0"
+    micromark-util-normalize-identifier "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+    unist-util-stringify-position "^4.0.0"
+
+mdast-util-frontmatter@1.0.1, mdast-util-frontmatter@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/mdast-util-frontmatter/-/mdast-util-frontmatter-1.0.1.tgz#79c46d7414eb9d3acabe801ee4a70a70b75e5af1"
   integrity sha512-JjA2OjxRqAa8wEG8hloD0uTU0kdn8kbtOWpPP94NBkfAlbxn4S8gCGf/9DwFtEeGPXrDcNXdiDjVaRdUFqYokw==
@@ -11644,6 +11698,17 @@ mdast-util-gfm-autolink-literal@^1.0.0:
     mdast-util-find-and-replace "^2.0.0"
     micromark-util-character "^1.0.0"
 
+mdast-util-gfm-autolink-literal@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz#5baf35407421310a08e68c15e5d8821e8898ba2a"
+  integrity sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+    ccount "^2.0.0"
+    devlop "^1.0.0"
+    mdast-util-find-and-replace "^3.0.0"
+    micromark-util-character "^2.0.0"
+
 mdast-util-gfm-footnote@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.1.tgz#11d2d40a1a673a399c459e467fa85e00223191fe"
@@ -11653,6 +11718,17 @@ mdast-util-gfm-footnote@^1.0.0:
     mdast-util-to-markdown "^1.3.0"
     micromark-util-normalize-identifier "^1.0.0"
 
+mdast-util-gfm-footnote@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz#25a1753c7d16db8bfd53cd84fe50562bd1e6d6a9"
+  integrity sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+    devlop "^1.1.0"
+    mdast-util-from-markdown "^2.0.0"
+    mdast-util-to-markdown "^2.0.0"
+    micromark-util-normalize-identifier "^2.0.0"
+
 mdast-util-gfm-strikethrough@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.1.tgz#a4a74c36864ec6a6e3bbd31e1977f29beb475789"
@@ -11661,6 +11737,15 @@ mdast-util-gfm-strikethrough@^1.0.0:
     "@types/mdast" "^3.0.0"
     mdast-util-to-markdown "^1.3.0"
 
+mdast-util-gfm-strikethrough@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16"
+  integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+    mdast-util-from-markdown "^2.0.0"
+    mdast-util-to-markdown "^2.0.0"
+
 mdast-util-gfm-table@^1.0.0:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.4.tgz#0dbb25f04fd9c0877dc63b76203ecbdf5d945755"
@@ -11670,6 +11755,17 @@ mdast-util-gfm-table@^1.0.0:
     mdast-util-from-markdown "^1.0.0"
     mdast-util-to-markdown "^1.3.0"
 
+mdast-util-gfm-table@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38"
+  integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+    devlop "^1.0.0"
+    markdown-table "^3.0.0"
+    mdast-util-from-markdown "^2.0.0"
+    mdast-util-to-markdown "^2.0.0"
+
 mdast-util-gfm-task-list-item@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.1.tgz#6f35f09c6e2bcbe88af62fdea02ac199cc802c5c"
@@ -11678,6 +11774,16 @@ mdast-util-gfm-task-list-item@^1.0.0:
     "@types/mdast" "^3.0.0"
     mdast-util-to-markdown "^1.3.0"
 
+mdast-util-gfm-task-list-item@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936"
+  integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+    devlop "^1.0.0"
+    mdast-util-from-markdown "^2.0.0"
+    mdast-util-to-markdown "^2.0.0"
+
 mdast-util-gfm@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.1.tgz#16fcf70110ae689a06d77e8f4e346223b64a0ea6"
@@ -11691,6 +11797,19 @@ mdast-util-gfm@^2.0.0:
     mdast-util-gfm-task-list-item "^1.0.0"
     mdast-util-to-markdown "^1.0.0"
 
+mdast-util-gfm@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz#3f2aecc879785c3cb6a81ff3a243dc11eca61095"
+  integrity sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==
+  dependencies:
+    mdast-util-from-markdown "^2.0.0"
+    mdast-util-gfm-autolink-literal "^2.0.0"
+    mdast-util-gfm-footnote "^2.0.0"
+    mdast-util-gfm-strikethrough "^2.0.0"
+    mdast-util-gfm-table "^2.0.0"
+    mdast-util-gfm-task-list-item "^2.0.0"
+    mdast-util-to-markdown "^2.0.0"
+
 mdast-util-math@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/mdast-util-math/-/mdast-util-math-2.0.1.tgz#141b8e7e43731d2a7423c5eb8c0335c05d257ad2"
@@ -11700,6 +11819,22 @@ mdast-util-math@^2.0.0:
     longest-streak "^3.0.0"
     mdast-util-to-markdown "^1.3.0"
 
+mdast-util-phrasing@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463"
+  integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==
+  dependencies:
+    "@types/mdast" "^3.0.0"
+    unist-util-is "^5.0.0"
+
+mdast-util-phrasing@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.0.0.tgz#468cbbb277375523de807248b8ad969feb02a5c7"
+  integrity sha512-xadSsJayQIucJ9n053dfQwVu1kuXg7jCTdYsMK8rqzKZh52nLfSH/k0sAxE0u+pj/zKZX+o5wB+ML5mRayOxFA==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+    unist-util-is "^6.0.0"
+
 mdast-util-to-hast@^12.1.0:
   version "12.3.0"
   resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49"
@@ -11739,6 +11874,34 @@ mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0:
     unist-util-visit "^4.0.0"
     zwitch "^2.0.0"
 
+mdast-util-to-markdown@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6"
+  integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==
+  dependencies:
+    "@types/mdast" "^3.0.0"
+    "@types/unist" "^2.0.0"
+    longest-streak "^3.0.0"
+    mdast-util-phrasing "^3.0.0"
+    mdast-util-to-string "^3.0.0"
+    micromark-util-decode-string "^1.0.0"
+    unist-util-visit "^4.0.0"
+    zwitch "^2.0.0"
+
+mdast-util-to-markdown@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.0.0.tgz#9aaf572f11c152520eea867a2a09295558ba0f49"
+  integrity sha512-Ov3aWCYpb31/SkHNRlREvSZsF9ETBW/rXw4PooawZO8qy2MviDq4TI6kxX6zFmGa/zUX36STKIC/IpASQk596w==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+    "@types/unist" "^3.0.0"
+    longest-streak "^3.0.0"
+    mdast-util-phrasing "^4.0.0"
+    mdast-util-to-string "^4.0.0"
+    micromark-util-decode-string "^2.0.0"
+    unist-util-visit "^5.0.0"
+    zwitch "^2.0.0"
+
 mdast-util-to-string@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b"
@@ -11749,6 +11912,13 @@ mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0:
   resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz#56c506d065fbf769515235e577b5a261552d56e9"
   integrity sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==
 
+mdast-util-to-string@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814"
+  integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+
 mdast-util-toc@^6.0.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/mdast-util-toc/-/mdast-util-toc-6.1.0.tgz#1f38419f5ce774449c8daa87b39a4d940b24be7c"
@@ -11919,6 +12089,28 @@ micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1:
     micromark-util-types "^1.0.1"
     uvu "^0.5.0"
 
+micromark-core-commonmark@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz#50740201f0ee78c12a675bf3e68ffebc0bf931a3"
+  integrity sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==
+  dependencies:
+    decode-named-character-reference "^1.0.0"
+    devlop "^1.0.0"
+    micromark-factory-destination "^2.0.0"
+    micromark-factory-label "^2.0.0"
+    micromark-factory-space "^2.0.0"
+    micromark-factory-title "^2.0.0"
+    micromark-factory-whitespace "^2.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-chunked "^2.0.0"
+    micromark-util-classify-character "^2.0.0"
+    micromark-util-html-tag-name "^2.0.0"
+    micromark-util-normalize-identifier "^2.0.0"
+    micromark-util-resolve-all "^2.0.0"
+    micromark-util-subtokenize "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+
 micromark-extension-frontmatter@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/micromark-extension-frontmatter/-/micromark-extension-frontmatter-1.1.1.tgz#2946643938e491374145d0c9aacc3249e38a865f"
@@ -12038,6 +12230,15 @@ micromark-factory-destination@^1.0.0:
     micromark-util-symbol "^1.0.0"
     micromark-util-types "^1.0.0"
 
+micromark-factory-destination@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz#857c94debd2c873cba34e0445ab26b74f6a6ec07"
+  integrity sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==
+  dependencies:
+    micromark-util-character "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+
 micromark-factory-label@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz#6be2551fa8d13542fcbbac478258fb7a20047137"
@@ -12048,6 +12249,16 @@ micromark-factory-label@^1.0.0:
     micromark-util-types "^1.0.0"
     uvu "^0.5.0"
 
+micromark-factory-label@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz#17c5c2e66ce39ad6f4fc4cbf40d972f9096f726a"
+  integrity sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==
+  dependencies:
+    devlop "^1.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+
 micromark-factory-space@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz#cebff49968f2b9616c0fcb239e96685cb9497633"
@@ -12056,6 +12267,14 @@ micromark-factory-space@^1.0.0:
     micromark-util-character "^1.0.0"
     micromark-util-types "^1.0.0"
 
+micromark-factory-space@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz#5e7afd5929c23b96566d0e1ae018ae4fcf81d030"
+  integrity sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==
+  dependencies:
+    micromark-util-character "^2.0.0"
+    micromark-util-types "^2.0.0"
+
 micromark-factory-title@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz#7e09287c3748ff1693930f176e1c4a328382494f"
@@ -12067,6 +12286,16 @@ micromark-factory-title@^1.0.0:
     micromark-util-types "^1.0.0"
     uvu "^0.5.0"
 
+micromark-factory-title@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz#726140fc77892af524705d689e1cf06c8a83ea95"
+  integrity sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==
+  dependencies:
+    micromark-factory-space "^2.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+
 micromark-factory-whitespace@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz#e991e043ad376c1ba52f4e49858ce0794678621c"
@@ -12077,6 +12306,16 @@ micromark-factory-whitespace@^1.0.0:
     micromark-util-symbol "^1.0.0"
     micromark-util-types "^1.0.0"
 
+micromark-factory-whitespace@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz#9e92eb0f5468083381f923d9653632b3cfb5f763"
+  integrity sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==
+  dependencies:
+    micromark-factory-space "^2.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+
 micromark-util-character@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.1.0.tgz#d97c54d5742a0d9611a68ca0cd4124331f264d86"
@@ -12085,6 +12324,14 @@ micromark-util-character@^1.0.0:
     micromark-util-symbol "^1.0.0"
     micromark-util-types "^1.0.0"
 
+micromark-util-character@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.0.1.tgz#52b824c2e2633b6fb33399d2ec78ee2a90d6b298"
+  integrity sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==
+  dependencies:
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+
 micromark-util-chunked@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz#5b40d83f3d53b84c4c6bce30ed4257e9a4c79d06"
@@ -12092,6 +12339,13 @@ micromark-util-chunked@^1.0.0:
   dependencies:
     micromark-util-symbol "^1.0.0"
 
+micromark-util-chunked@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz#e51f4db85fb203a79dbfef23fd41b2f03dc2ef89"
+  integrity sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==
+  dependencies:
+    micromark-util-symbol "^2.0.0"
+
 micromark-util-classify-character@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz#cbd7b447cb79ee6997dd274a46fc4eb806460a20"
@@ -12101,6 +12355,15 @@ micromark-util-classify-character@^1.0.0:
     micromark-util-symbol "^1.0.0"
     micromark-util-types "^1.0.0"
 
+micromark-util-classify-character@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz#8c7537c20d0750b12df31f86e976d1d951165f34"
+  integrity sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==
+  dependencies:
+    micromark-util-character "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+
 micromark-util-combine-extensions@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz#91418e1e74fb893e3628b8d496085639124ff3d5"
@@ -12109,6 +12372,14 @@ micromark-util-combine-extensions@^1.0.0:
     micromark-util-chunked "^1.0.0"
     micromark-util-types "^1.0.0"
 
+micromark-util-combine-extensions@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz#75d6ab65c58b7403616db8d6b31315013bfb7ee5"
+  integrity sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==
+  dependencies:
+    micromark-util-chunked "^2.0.0"
+    micromark-util-types "^2.0.0"
+
 micromark-util-decode-numeric-character-reference@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz#dcc85f13b5bd93ff8d2868c3dba28039d490b946"
@@ -12116,6 +12387,13 @@ micromark-util-decode-numeric-character-reference@^1.0.0:
   dependencies:
     micromark-util-symbol "^1.0.0"
 
+micromark-util-decode-numeric-character-reference@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.0.tgz#a798808d02cc74113e2c939fc95363096ade7f1d"
+  integrity sha512-pIgcsGxpHEtTG/rPJRz/HOLSqp5VTuIIjXlPI+6JSDlK2oljApusG6KzpS8AF0ENUMCHlC/IBb5B9xdFiVlm5Q==
+  dependencies:
+    micromark-util-symbol "^2.0.0"
+
 micromark-util-decode-string@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz#942252ab7a76dec2dbf089cc32505ee2bc3acf02"
@@ -12126,16 +12404,36 @@ micromark-util-decode-string@^1.0.0:
     micromark-util-decode-numeric-character-reference "^1.0.0"
     micromark-util-symbol "^1.0.0"
 
+micromark-util-decode-string@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz#7dfa3a63c45aecaa17824e656bcdb01f9737154a"
+  integrity sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==
+  dependencies:
+    decode-named-character-reference "^1.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-decode-numeric-character-reference "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+
 micromark-util-encode@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz#2c1c22d3800870ad770ece5686ebca5920353383"
   integrity sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==
 
+micromark-util-encode@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1"
+  integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==
+
 micromark-util-html-tag-name@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.1.0.tgz#eb227118befd51f48858e879b7a419fc0df20497"
   integrity sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==
 
+micromark-util-html-tag-name@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz#ae34b01cbe063363847670284c6255bb12138ec4"
+  integrity sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==
+
 micromark-util-normalize-identifier@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz#4a3539cb8db954bbec5203952bfe8cedadae7828"
@@ -12143,6 +12441,13 @@ micromark-util-normalize-identifier@^1.0.0:
   dependencies:
     micromark-util-symbol "^1.0.0"
 
+micromark-util-normalize-identifier@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz#91f9a4e65fe66cc80c53b35b0254ad67aa431d8b"
+  integrity sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==
+  dependencies:
+    micromark-util-symbol "^2.0.0"
+
 micromark-util-resolve-all@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz#a7c363f49a0162e931960c44f3127ab58f031d88"
@@ -12150,6 +12455,13 @@ micromark-util-resolve-all@^1.0.0:
   dependencies:
     micromark-util-types "^1.0.0"
 
+micromark-util-resolve-all@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz#189656e7e1a53d0c86a38a652b284a252389f364"
+  integrity sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==
+  dependencies:
+    micromark-util-types "^2.0.0"
+
 micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.1.0.tgz#f12e07a85106b902645e0364feb07cf253a85aee"
@@ -12159,6 +12471,15 @@ micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0:
     micromark-util-encode "^1.0.0"
     micromark-util-symbol "^1.0.0"
 
+micromark-util-sanitize-uri@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de"
+  integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==
+  dependencies:
+    micromark-util-character "^2.0.0"
+    micromark-util-encode "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+
 micromark-util-subtokenize@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz#ff6f1af6ac836f8bfdbf9b02f40431760ad89105"
@@ -12169,16 +12490,36 @@ micromark-util-subtokenize@^1.0.0:
     micromark-util-types "^1.0.0"
     uvu "^0.5.0"
 
+micromark-util-subtokenize@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz#9f412442d77e0c5789ffdf42377fa8a2bcbdf581"
+  integrity sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==
+  dependencies:
+    devlop "^1.0.0"
+    micromark-util-chunked "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+
 micromark-util-symbol@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz#b90344db62042ce454f351cf0bebcc0a6da4920e"
   integrity sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==
 
+micromark-util-symbol@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044"
+  integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==
+
 micromark-util-types@^1.0.0, micromark-util-types@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.0.2.tgz#f4220fdb319205812f99c40f8c87a9be83eded20"
   integrity sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==
 
+micromark-util-types@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e"
+  integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==
+
 micromark@^3.0.0:
   version "3.0.10"
   resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.0.10.tgz#1eac156f0399d42736458a14b0ca2d86190b457c"
@@ -12202,6 +12543,29 @@ micromark@^3.0.0:
     micromark-util-types "^1.0.1"
     uvu "^0.5.0"
 
+micromark@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.0.tgz#84746a249ebd904d9658cfabc1e8e5f32cbc6249"
+  integrity sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==
+  dependencies:
+    "@types/debug" "^4.0.0"
+    debug "^4.0.0"
+    decode-named-character-reference "^1.0.0"
+    devlop "^1.0.0"
+    micromark-core-commonmark "^2.0.0"
+    micromark-factory-space "^2.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-chunked "^2.0.0"
+    micromark-util-combine-extensions "^2.0.0"
+    micromark-util-decode-numeric-character-reference "^2.0.0"
+    micromark-util-encode "^2.0.0"
+    micromark-util-normalize-identifier "^2.0.0"
+    micromark-util-resolve-all "^2.0.0"
+    micromark-util-sanitize-uri "^2.0.0"
+    micromark-util-subtokenize "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+
 micromatch@4.0.2:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
@@ -12337,6 +12701,11 @@ minimist@1, minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5,
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
   integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
 
+minimist@^1.2.3:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+  integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
 minipass@^3.0.0:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
@@ -14469,6 +14838,13 @@ readdirp@~3.6.0:
   dependencies:
     picomatch "^2.2.1"
 
+rechoir@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
+  integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==
+  dependencies:
+    resolve "^1.1.6"
+
 reconnecting-websocket@^4.4.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
@@ -15007,6 +15383,15 @@ resolve@^1.0.0, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.20.0, resolve@^1.22.
     path-parse "^1.0.7"
     supports-preserve-symlinks-flag "^1.0.0"
 
+resolve@^1.1.6:
+  version "1.22.2"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f"
+  integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==
+  dependencies:
+    is-core-module "^2.11.0"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
 resolve@^2.0.0-next.3:
   version "2.0.0-next.3"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46"
@@ -15166,6 +15551,13 @@ safe-regex@^1.1.0:
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
+sanitize-filename@^1.6.3:
+  version "1.6.3"
+  resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378"
+  integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==
+  dependencies:
+    truncate-utf8-bytes "^1.0.0"
+
 saslprep@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226"
@@ -15363,6 +15755,15 @@ shell-quote@^1.6.1:
   resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
   integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
 
+shelljs@^0.8.5:
+  version "0.8.5"
+  resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c"
+  integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==
+  dependencies:
+    glob "^7.0.0"
+    interpret "^1.0.0"
+    rechoir "^0.6.2"
+
 should-equal@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"
@@ -15407,6 +15808,14 @@ should@^13.2.1:
     should-type-adaptors "^1.0.1"
     should-util "^1.0.0"
 
+shx@^0.3.4:
+  version "0.3.4"
+  resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02"
+  integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==
+  dependencies:
+    minimist "^1.2.3"
+    shelljs "^0.8.5"
+
 side-channel@^1.0.3, side-channel@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
@@ -16582,6 +16991,13 @@ trough@^2.0.0:
   resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876"
   integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==
 
+truncate-utf8-bytes@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b"
+  integrity sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==
+  dependencies:
+    utf8-byte-length "^1.0.1"
+
 ts-dedent@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
@@ -16958,6 +17374,13 @@ unist-util-is@^5.0.0:
   resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.1.1.tgz#e8aece0b102fa9bc097b0fef8f870c496d4a6236"
   integrity sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==
 
+unist-util-is@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424"
+  integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==
+  dependencies:
+    "@types/unist" "^3.0.0"
+
 unist-util-position@^4.0.0:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.3.tgz#5290547b014f6222dff95c48d5c3c13a88fadd07"
@@ -16980,6 +17403,13 @@ unist-util-stringify-position@^3.0.0:
   dependencies:
     "@types/unist" "^2.0.0"
 
+unist-util-stringify-position@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2"
+  integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==
+  dependencies:
+    "@types/unist" "^3.0.0"
+
 unist-util-visit-parents@^3.0.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6"
@@ -17004,6 +17434,14 @@ unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1:
     "@types/unist" "^2.0.0"
     unist-util-is "^5.0.0"
 
+unist-util-visit-parents@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815"
+  integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==
+  dependencies:
+    "@types/unist" "^3.0.0"
+    unist-util-is "^6.0.0"
+
 unist-util-visit@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c"
@@ -17031,6 +17469,15 @@ unist-util-visit@^4.0.0, unist-util-visit@^4.1.0, unist-util-visit@~4.1.0:
     unist-util-is "^5.0.0"
     unist-util-visit-parents "^5.1.1"
 
+unist-util-visit@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6"
+  integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==
+  dependencies:
+    "@types/unist" "^3.0.0"
+    unist-util-is "^6.0.0"
+    unist-util-visit-parents "^6.0.0"
+
 universal-bunyan@^0.9.2:
   version "0.9.2"
   resolved "https://registry.yarnpkg.com/universal-bunyan/-/universal-bunyan-0.9.2.tgz#4cf09dc34070390d8f5df4fe9af6a80fcd0dd574"
@@ -17052,22 +17499,6 @@ unpipe@1.0.0, unpipe@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
 
-unplugin-swc@^1.3.2:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/unplugin-swc/-/unplugin-swc-1.3.2.tgz#4c859896ce33a46f66033ba66290b33fb886a248"
-  integrity sha512-x0+NTM4NR1jWpailVhK5sXO9svyL4iWZ+yah0WZ1G+SaI1hPysa95nVSzXqLP7y4+MKTO17wP+EVqjrREqgBsw==
-  dependencies:
-    unplugin "^0.6.0"
-
-unplugin@^0.6.0:
-  version "0.6.3"
-  resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-0.6.3.tgz#b8721e2b163a410a7efed726e6a0fc6fbadf975a"
-  integrity sha512-CoW88FQfCW/yabVc4bLrjikN9HC8dEvMU4O7B6K2jsYMPK0l6iAnd9dpJwqGcmXJKRCU9vwSsy653qg+RK0G6A==
-  dependencies:
-    chokidar "^3.5.3"
-    webpack-sources "^3.2.3"
-    webpack-virtual-modules "^0.4.3"
-
 unset-value@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
@@ -17175,6 +17606,11 @@ useragent@^2.2.1:
     lru-cache "4.1.x"
     tmp "0.0.x"
 
+utf8-byte-length@^1.0.1:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61"
+  integrity sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==
+
 util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -17448,16 +17884,6 @@ webpack-bundle-analyzer@4.7.0:
     sirv "^1.0.7"
     ws "^7.3.1"
 
-webpack-sources@^3.2.3:
-  version "3.2.3"
-  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
-  integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
-
-webpack-virtual-modules@^0.4.3:
-  version "0.4.6"
-  resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.4.6.tgz#3e4008230731f1db078d9cb6f68baf8571182b45"
-  integrity sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==
-
 well-known-symbols@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/well-known-symbols/-/well-known-symbols-2.0.0.tgz#e9c7c07dbd132b7b84212c8174391ec1f9871ba5"