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

Merge branch 'master' into feat/enhanced-access-token

Shun Miyazawa 9 месяцев назад
Родитель
Сommit
e4ad89e77d
100 измененных файлов с 1765 добавлено и 1164 удалено
  1. 1 1
      apps/app/src/client/components/Admin/UserGroup/UserGroupForm.tsx
  2. 1 1
      apps/app/src/client/components/Admin/UserGroup/UserGroupTable.tsx
  3. 2 2
      apps/app/src/client/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  4. 1 1
      apps/app/src/client/components/Admin/Users/ExternalAccountTable.tsx
  5. 1 1
      apps/app/src/client/components/Admin/Users/UserTable.tsx
  6. 1 1
      apps/app/src/client/components/Me/ExternalAccountRow.jsx
  7. 1 1
      apps/app/src/pages/utils/commons.ts
  8. 92 0
      apps/app/src/server/models/openapi/page.ts
  9. 5 37
      apps/app/src/server/routes/apiv3/page-listing.ts
  10. 10 49
      apps/app/src/server/routes/apiv3/page/index.ts
  11. 1 1
      apps/app/src/server/routes/apiv3/pages/index.js
  12. 2 1
      apps/app/src/server/service/global-notification/global-notification-mail.js
  13. 285 0
      apps/app/src/utils/axios-date-conversion.spec.ts
  14. 46 0
      apps/app/src/utils/axios.ts
  15. 3 7
      biome.json
  16. 4 0
      packages/core/.eslintignore
  17. 1 3
      packages/core/.eslintrc.cjs
  18. 1 1
      packages/core/package.json
  19. 2 1
      packages/core/src/consts/accepted-upload-file-type.ts
  20. 2 1
      packages/core/src/consts/growi-plugin.ts
  21. 4 2
      packages/core/src/consts/system.ts
  22. 1 1
      packages/core/src/consts/ydoc-status.ts
  23. 14 14
      packages/core/src/interfaces/attachment.ts
  24. 1 1
      packages/core/src/interfaces/color-scheme.ts
  25. 1 6
      packages/core/src/interfaces/common.spec.ts
  26. 18 14
      packages/core/src/interfaces/common.ts
  27. 23 12
      packages/core/src/interfaces/config-manager.ts
  28. 4 4
      packages/core/src/interfaces/external-account.ts
  29. 17 17
      packages/core/src/interfaces/growi-app-info.ts
  30. 10 4
      packages/core/src/interfaces/growi-facade.ts
  31. 42 26
      packages/core/src/interfaces/growi-theme-metadata.ts
  32. 1 1
      packages/core/src/interfaces/has-object-id.ts
  33. 1 1
      packages/core/src/interfaces/lang.ts
  34. 122 89
      packages/core/src/interfaces/page.ts
  35. 121 99
      packages/core/src/interfaces/primitive/string.spec.ts
  36. 16 6
      packages/core/src/interfaces/primitive/string.ts
  37. 18 18
      packages/core/src/interfaces/revision.ts
  38. 9 8
      packages/core/src/interfaces/subscription.ts
  39. 3 3
      packages/core/src/interfaces/tag.ts
  40. 33 33
      packages/core/src/interfaces/user.ts
  41. 7 7
      packages/core/src/interfaces/vite.ts
  42. 3 3
      packages/core/src/remark-plugins/interfaces/option-parser.ts
  43. 14 13
      packages/core/src/remark-plugins/util/option-parser.spec.ts
  44. 6 9
      packages/core/src/remark-plugins/util/option-parser.ts
  45. 23 10
      packages/core/src/swr/use-swr-static.ts
  46. 6 1
      packages/core/src/swr/with-utils.ts
  47. 2 6
      packages/core/tsconfig.json
  48. 1 1
      packages/core/vite.config.ts
  49. 1 3
      packages/core/vitest.config.ts
  50. 1 0
      packages/pdf-converter-client/.eslintignore
  51. 0 11
      packages/pdf-converter-client/.eslintrc.cjs
  52. 1 1
      packages/pdf-converter-client/package.json
  53. 1 1
      packages/pluginkit/.eslintignore
  54. 0 5
      packages/pluginkit/.eslintrc.cjs
  55. 1 1
      packages/pluginkit/package.json
  56. 9 7
      packages/pluginkit/src/model/growi-plugin-package-data.ts
  57. 9 9
      packages/pluginkit/src/model/growi-plugin-validation-data.ts
  58. 4 4
      packages/pluginkit/src/model/growi-plugin-validation-error.ts
  59. 6 2
      packages/pluginkit/src/v4/client/utils/growi-facade/growi-react.spec.ts
  60. 1 2
      packages/pluginkit/src/v4/client/utils/growi-facade/growi-react.ts
  61. 18 16
      packages/pluginkit/src/v4/interfaces/template.ts
  62. 8 3
      packages/pluginkit/src/v4/server/utils/common/import-package-json.spec.ts
  63. 5 3
      packages/pluginkit/src/v4/server/utils/common/import-package-json.ts
  64. 36 17
      packages/pluginkit/src/v4/server/utils/common/validate-growi-plugin-directive.spec.ts
  65. 29 8
      packages/pluginkit/src/v4/server/utils/common/validate-growi-plugin-directive.ts
  66. 7 4
      packages/pluginkit/src/v4/server/utils/template/get-markdown.ts
  67. 9 9
      packages/pluginkit/src/v4/server/utils/template/get-status.ts
  68. 47 33
      packages/pluginkit/src/v4/server/utils/template/scan.ts
  69. 17 9
      packages/pluginkit/src/v4/server/utils/template/validate-all-locales.ts
  70. 16 11
      packages/pluginkit/src/v4/server/utils/template/validate-growi-plugin-directive.spec.ts
  71. 10 4
      packages/pluginkit/src/v4/server/utils/template/validate-growi-plugin-directive.ts
  72. 23 13
      packages/pluginkit/src/v4/server/utils/theme/validate-growi-plugin-directive.spec.ts
  73. 15 8
      packages/pluginkit/src/v4/server/utils/theme/validate-growi-plugin-directive.ts
  74. 13 5
      packages/pluginkit/src/v4/utils/template.spec.ts
  75. 9 4
      packages/pluginkit/src/v4/utils/template.ts
  76. 2 6
      packages/pluginkit/tsconfig.json
  77. 1 1
      packages/pluginkit/vite.config.ts
  78. 2 4
      packages/pluginkit/vitest.config.ts
  79. 1 1
      packages/presentation/.eslintignore
  80. 0 5
      packages/presentation/.eslintrc.cjs
  81. 1 1
      packages/presentation/package.json
  82. 25 18
      packages/presentation/src/client/components/GrowiSlides.tsx
  83. 4 4
      packages/presentation/src/client/components/MarpSlides.tsx
  84. 12 10
      packages/presentation/src/client/components/Presentation.tsx
  85. 35 34
      packages/presentation/src/client/components/RichSlideSection.tsx
  86. 13 13
      packages/presentation/src/client/components/Slides.tsx
  87. 5 5
      packages/presentation/src/client/consts/index.ts
  88. 11 11
      packages/presentation/src/client/services/growi-marpit.ts
  89. 26 29
      packages/presentation/src/client/services/renderer/extract-sections.ts
  90. 29 28
      packages/presentation/src/services/use-slides-by-frontmatter.ts
  91. 2 5
      packages/presentation/tsconfig.json
  92. 1 1
      packages/presentation/vite.config.ts
  93. 1 1
      packages/remark-attachment-refs/.eslintignore
  94. 0 5
      packages/remark-attachment-refs/.eslintrc.cjs
  95. 1 1
      packages/remark-attachment-refs/package.json
  96. 33 19
      packages/remark-attachment-refs/src/client/components/AttachmentList.tsx
  97. 187 176
      packages/remark-attachment-refs/src/client/components/ExtractedAttachments.tsx
  98. 9 5
      packages/remark-attachment-refs/src/client/components/Gallery.tsx
  99. 31 28
      packages/remark-attachment-refs/src/client/components/Ref.tsx
  100. 54 42
      packages/remark-attachment-refs/src/client/components/RefImg.tsx

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

@@ -76,7 +76,7 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
           userGroup?.createdAt != null && (
             <div className="row mb-3">
               <p className="col-md-2 col-form-label">{t('Created')}</p>
-              <p className="col-md-6 my-auto">{dateFnsFormat(new Date(userGroup.createdAt), 'yyyy-MM-dd')}</p>
+              <p className="col-md-6 my-auto">{dateFnsFormat(userGroup.createdAt, 'yyyy-MM-dd')}</p>
             </div>
           )
         }

+ 1 - 1
apps/app/src/client/components/Admin/UserGroup/UserGroupTable.tsx

@@ -218,7 +218,7 @@ export const UserGroupTable: FC<Props> = ({
                     })}
                   </ul>
                 </td>
-                <td>{dateFnsFormat(new Date(group.createdAt), 'yyyy-MM-dd')}</td>
+                <td>{dateFnsFormat(group.createdAt, 'yyyy-MM-dd')}</td>
                 {isAclEnabled
                   ? (
                     <td>

+ 2 - 2
apps/app/src/client/components/Admin/UserGroupDetail/UserGroupUserTable.tsx

@@ -43,8 +43,8 @@ export const UserGroupUserTable = (props: Props): JSX.Element => {
                 <strong>{relatedUser.username}</strong>
               </td>
               <td>{relatedUser.name}</td>
-              <td>{relatedUser.createdAt ? dateFnsFormat(new Date(relatedUser.createdAt), 'yyyy-MM-dd') : ''}</td>
-              <td>{relatedUser.lastLoginAt ? dateFnsFormat(new Date(relatedUser.lastLoginAt), 'yyyy-MM-dd HH:mm:ss') : ''}</td>
+              <td>{relatedUser.createdAt ? dateFnsFormat(relatedUser.createdAt, 'yyyy-MM-dd') : ''}</td>
+              <td>{relatedUser.lastLoginAt ? dateFnsFormat(relatedUser.lastLoginAt, 'yyyy-MM-dd HH:mm:ss') : ''}</td>
               {!props.isExternalGroup && (
                 <td>
                   <div className="btn-group admin-user-menu">

+ 1 - 1
apps/app/src/client/components/Admin/Users/ExternalAccountTable.tsx

@@ -89,7 +89,7 @@ const ExternalAccountTable = (props: ExternalAccountTableProps): JSX.Element =>
                     : (<span className="badge bg-warning text-dark">{t('user_management.unset')}</span>)
                   }
                 </td>
-                <td>{dateFnsFormat(new Date(ea.createdAt), 'yyyy-MM-dd')}</td>
+                <td>{dateFnsFormat(ea.createdAt, 'yyyy-MM-dd')}</td>
                 <td>
                   <div className="btn-group admin-user-menu">
                     <button type="button" className="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown">

+ 1 - 1
apps/app/src/client/components/Admin/Users/UserTable.tsx

@@ -168,7 +168,7 @@ const UserTable = (props: UserTableProps) => {
                 </td>
                 <td>{user.name}</td>
                 <td>{user.email}</td>
-                <td>{dateFnsFormat(new Date(user.createdAt), 'yyyy-MM-dd')}</td>
+                <td>{dateFnsFormat(user.createdAt, 'yyyy-MM-dd')}</td>
                 <td>
                   {user.lastLoginAt && <span>{dateFnsFormat(new Date(user.lastLoginAt), 'yyyy-MM-dd HH:mm')}</span>}
                 </td>

+ 1 - 1
apps/app/src/client/components/Me/ExternalAccountRow.jsx

@@ -15,7 +15,7 @@ const ExternalAccountRow = (props) => {
       <td>
         <strong>{ account.accountId }</strong>
       </td>
-      <td>{dateFnsFormat(new Date(account.createdAt), 'yyyy-MM-dd')}</td>
+      <td>{dateFnsFormat(account.createdAt, 'yyyy-MM-dd')}</td>
       <td className="text-center">
         <button
           type="button"

+ 1 - 1
apps/app/src/pages/utils/commons.ts

@@ -88,7 +88,7 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     namespacesRequired: ['translation'],
     currentPathname,
     appTitle: appService.getAppTitle(),
-    siteUrl: configManager.getConfig('app:siteUrl'), // DON'T USE appService.getSiteUrl()
+    siteUrl: configManager.getConfig('app:siteUrl'), // DON'T USE growiInfoService.getSiteUrl()
     confidential: appService.getAppConfidential() || '',
     customTitleTemplate: customizeService.customTitleTemplate,
     csrfToken: req.csrfToken(),

+ 92 - 0
apps/app/src/server/models/openapi/page.ts

@@ -11,6 +11,98 @@
  *        description: Grant for page
  *        type: number
  *        example: 1
+ *      PageInfo:
+ *        description: Basic page information
+ *        type: object
+ *        properties:
+ *          isV5Compatible:
+ *            type: boolean
+ *            description: Whether the page is compatible with v5
+ *          isEmpty:
+ *            type: boolean
+ *            description: Whether the page is empty
+ *          isMovable:
+ *            type: boolean
+ *            description: Whether the page is movable
+ *          isDeletable:
+ *            type: boolean
+ *            description: Whether the page is deletable
+ *          isAbleToDeleteCompletely:
+ *            type: boolean
+ *            description: Whether the page is able to delete completely
+ *          isRevertible:
+ *            type: boolean
+ *            description: Whether the page is revertible
+ *      PageInfoForEntity:
+ *        description: Page information for entity (extends IPageInfo)
+ *        allOf:
+ *          - $ref: '#/components/schemas/PageInfo'
+ *          - type: object
+ *            properties:
+ *              bookmarkCount:
+ *                type: number
+ *                description: Number of bookmarks
+ *              sumOfLikers:
+ *                type: number
+ *                description: Number of users who have liked the page
+ *              likerIds:
+ *                type: array
+ *                items:
+ *                  type: string
+ *                description: Ids of users who have liked the page
+ *                example: ["5e07345972560e001761fa63"]
+ *              sumOfSeenUsers:
+ *                type: number
+ *                description: Number of users who have seen the page
+ *              seenUserIds:
+ *                type: array
+ *                items:
+ *                  type: string
+ *                description: Ids of users who have seen the page
+ *                example: ["5e07345972560e001761fa63"]
+ *              contentAge:
+ *                type: number
+ *                description: Content age
+ *              descendantCount:
+ *                type: number
+ *                description: Number of descendant pages
+ *              commentCount:
+ *                type: number
+ *                description: Number of comments
+ *      PageInfoForOperation:
+ *        description: Page information for operation (extends IPageInfoForEntity)
+ *        allOf:
+ *          - $ref: '#/components/schemas/PageInfoForEntity'
+ *          - type: object
+ *            properties:
+ *              isBookmarked:
+ *                type: boolean
+ *                description: Whether the page is bookmarked by the logged in user
+ *              isLiked:
+ *                type: boolean
+ *                description: Whether the page is liked by the logged in user
+ *              subscriptionStatus:
+ *                type: string
+ *                description: Subscription status
+ *                enum:
+ *                  - 'SUBSCRIBE'
+ *                  - 'UNSUBSCRIBE'
+ *      PageInfoForListing:
+ *        description: Page information for listing (extends IPageInfoForEntity with revision short body)
+ *        allOf:
+ *          - $ref: '#/components/schemas/PageInfoForEntity'
+ *          - type: object
+ *            properties:
+ *              revisionShortBody:
+ *                type: string
+ *                description: Short body of the revision
+ *      PageInfoAll:
+ *        description: Page information (union of all page info types)
+ *        oneOf:
+ *          - $ref: '#/components/schemas/PageInfo'
+ *          - $ref: '#/components/schemas/PageInfoForEntity'
+ *          - $ref: '#/components/schemas/PageInfoForOperation'
+ *          - $ref: '#/components/schemas/PageInfoForListing'
  *      Page:
  *        description: Page
  *        type: object

+ 5 - 37
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -239,16 +239,18 @@ const routerFactory = (crowi: Crowi): Router => {
    *       - bearer: []
    *       - accessTokenInQuery: []
    *     summary: /page-listing/info
-   *     description: Get the information of a page
+   *     description: Get summary information of pages
    *     parameters:
    *       - name: pageIds
    *         in: query
+   *         description: Array of page IDs to retrieve information for (One of pageIds or path is required)
    *         schema:
    *           type: array
    *           items:
    *             type: string
    *       - name: path
    *         in: query
+   *         description: Path of the page to retrieve information for (One of pageIds or path is required)
    *         schema:
    *           type: string
    *       - name: attachBookmarkCount
@@ -266,42 +268,8 @@ const routerFactory = (crowi: Crowi): Router => {
    *           application/json:
    *             schema:
    *               type: object
-   *               properties:
-   *                 idToPageInfoMap:
-   *                   type: object
-   *                   additionalProperties:
-   *                     type: object
-   *                     properties:
-   *                       commentCount:
-   *                         type: integer
-   *                       contentAge:
-   *                         type: integer
-   *                       descendantCount:
-   *                         type: integer
-   *                       isAbleToDeleteCompletely:
-   *                         type: boolean
-   *                       isDeletable:
-   *                         type: boolean
-   *                       isEmpty:
-   *                         type: boolean
-   *                       isMovable:
-   *                         type: boolean
-   *                       isRevertible:
-   *                         type: boolean
-   *                       isV5Compatible:
-   *                         type: boolean
-   *                       likerIds:
-   *                         type: array
-   *                         items:
-   *                           type: string
-   *                       seenUserIds:
-   *                         type: array
-   *                         items:
-   *                           type: string
-   *                       sumOfLikers:
-   *                         type: integer
-   *                       sumOfSeenUsers:
-   *                         type: integer
+   *               additionalProperties:
+   *                 $ref: '#/components/schemas/PageInfoAll'
    */
   router.get('/info',
     accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),

+ 10 - 49
apps/app/src/server/routes/apiv3/page/index.ts

@@ -70,47 +70,6 @@ const router = express.Router();
  *            type: boolean
  *            description: boolean for like status
  *
- *      PageInfo:
- *        description: PageInfo
- *        type: object
- *        required:
- *          - sumOfLikers
- *          - likerIds
- *          - sumOfSeenUsers
- *          - seenUserIds
- *        properties:
- *          isLiked:
- *            type: boolean
- *            description: Whether the page is liked by the logged in user
- *          sumOfLikers:
- *            type: number
- *            description: Number of users who have liked the page
- *          likerIds:
- *            type: array
- *            items:
- *              type: string
- *            description: Ids of users who have liked the page
- *            example: ["5e07345972560e001761fa63"]
- *          sumOfSeenUsers:
- *            type: number
- *            description: Number of users who have seen the page
- *          seenUserIds:
- *            type: array
- *            items:
- *              type: string
- *            description: Ids of users who have seen the page
- *            example: ["5e07345972560e001761fa63"]
- *
- *      PageParams:
- *        description: PageParams
- *        type: object
- *        required:
- *          - pageId
- *        properties:
- *          pageId:
- *            type: string
- *            description: page ID
- *            example: 5e07345972560e001761fa63
  */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
@@ -492,20 +451,22 @@ module.exports = (crowi) => {
    *    /page/info:
    *      get:
    *        tags: [Page]
-   *        summary: Get page info
-   *        description: Retrieve current page info
-   *        requestBody:
-   *          content:
-   *            application/json:
-   *              schema:
-   *                $ref: '#/components/schemas/PageParams'
+   *        summary: /page/info
+   *        description: Get summary informations for a page
+   *        parameters:
+   *          - name: pageId
+   *            in: query
+   *            required: true
+   *            description: page id
+   *            schema:
+   *              $ref: '#/components/schemas/ObjectId'
    *        responses:
    *          200:
    *            description: Successfully retrieved current page info.
    *            content:
    *              application/json:
    *                schema:
-   *                  $ref: '#/components/schemas/PageInfo'
+   *                  $ref: '#/components/schemas/PageInfoAll'
    *          500:
    *            description: Internal server error.
    */

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

@@ -198,7 +198,7 @@ module.exports = (crowi) => {
    *
    *
    *    /pages/rename:
-   *      post:
+   *      put:
    *        tags: [Pages]
    *        description: Rename page
    *        requestBody:

+ 2 - 1
apps/app/src/server/service/global-notification/global-notification-mail.js

@@ -2,6 +2,7 @@ import nodePath from 'path';
 
 import { GlobalNotificationSettingEvent, GlobalNotificationSettingType } from '~/server/models/GlobalNotificationSetting';
 import { configManager } from '~/server/service/config-manager';
+import { growiInfoService } from '~/server/service/growi-info';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:service:GlobalNotificationMailService'); // eslint-disable-line no-unused-vars
@@ -65,7 +66,7 @@ class GlobalNotificationMailService {
 
     const path = page.path;
     const appTitle = this.crowi.appService.getAppTitle();
-    const siteUrl = this.crowi.appService.getSiteUrl();
+    const siteUrl = growiInfoService.getSiteUrl();
     const pageUrl = new URL(page._id, siteUrl);
 
     let subject;

+ 285 - 0
apps/app/src/utils/axios-date-conversion.spec.ts

@@ -0,0 +1,285 @@
+import { convertDateStringsToDates } from './axios';
+
+describe('convertDateStringsToDates', () => {
+
+  // Test case 1: Basic conversion in a flat object
+  test('should convert ISO date strings to Date objects in a flat object', () => {
+    const dateString = '2023-01-15T10:00:00.000Z';
+    const input = {
+      id: 1,
+      createdAt: dateString,
+      name: 'Test Item',
+    };
+    const expected = {
+      id: 1,
+      createdAt: new Date(dateString),
+      name: 'Test Item',
+    };
+    const result = convertDateStringsToDates(input);
+    expect(result.createdAt).toBeInstanceOf(Date);
+    expect(result.createdAt.toISOString()).toEqual(dateString);
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 2: Nested objects
+  test('should recursively convert ISO date strings in nested objects', () => {
+    const dateString1 = '2023-02-20T12:30:00.000Z';
+    const dateString2 = '2023-03-01T08:00:00.000Z';
+    const input = {
+      data: {
+        item1: {
+          updatedAt: dateString1,
+          value: 10,
+        },
+        item2: {
+          nested: {
+            deletedAt: dateString2,
+            isActive: false,
+          },
+        },
+      },
+    };
+    const expected = {
+      data: {
+        item1: {
+          updatedAt: new Date(dateString1),
+          value: 10,
+        },
+        item2: {
+          nested: {
+            deletedAt: new Date(dateString2),
+            isActive: false,
+          },
+        },
+      },
+    };
+    const result = convertDateStringsToDates(input);
+    expect(result.data.item1.updatedAt).toBeInstanceOf(Date);
+    expect(result.data.item1.updatedAt.toISOString()).toEqual(dateString1);
+    expect(result.data.item2.nested.deletedAt).toBeInstanceOf(Date);
+    expect(result.data.item2.nested.deletedAt.toISOString()).toEqual(dateString2);
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 3: Arrays of objects
+  test('should recursively convert ISO date strings in arrays of objects', () => {
+    const dateString1 = '2023-04-05T14:15:00.000Z';
+    const dateString2 = '2023-05-10T16:00:00.000Z';
+    const input = [
+      { id: 1, eventDate: dateString1 },
+      { id: 2, eventDate: dateString2, data: { nestedProp: 'value' } },
+    ];
+    const expected = [
+      { id: 1, eventDate: new Date(dateString1) },
+      { id: 2, eventDate: new Date(dateString2), data: { nestedProp: 'value' } },
+    ];
+    const result = convertDateStringsToDates(input);
+    expect(result[0].eventDate).toBeInstanceOf(Date);
+    expect(result[0].eventDate.toISOString()).toEqual(dateString1);
+    expect(result[1].eventDate).toBeInstanceOf(Date);
+    expect(result[1].eventDate.toISOString()).toEqual(dateString2);
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 4: Array containing date strings directly (though less common for this function)
+  test('should handle arrays containing date strings directly', () => {
+    const dateString = '2023-06-20T18:00:00.000Z';
+    const input = ['text', dateString, 123];
+    const expected = ['text', new Date(dateString), 123];
+    const result = convertDateStringsToDates(input);
+    expect(result[1]).toBeInstanceOf(Date);
+    expect(result[1].toISOString()).toEqual(dateString);
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 5: Data without date strings should remain unchanged
+  test('should not modify data without ISO date strings', () => {
+    const input = {
+      name: 'Product A',
+      price: 99.99,
+      tags: ['electronic', 'sale'],
+      description: 'Some text',
+    };
+    const originalInput = JSON.parse(JSON.stringify(input)); // Deep copy to ensure no mutation
+    const result = convertDateStringsToDates(input);
+    expect(result).toEqual(originalInput); // Should be deeply equal
+    expect(result).toBe(input); // Confirm it mutated the original object
+  });
+
+  // Test case 6: Null, undefined, and primitive values
+  test('should return primitive values as is', () => {
+    expect(convertDateStringsToDates(null)).toBeNull();
+    expect(convertDateStringsToDates(undefined)).toBeUndefined();
+    expect(convertDateStringsToDates(123)).toBe(123);
+    expect(convertDateStringsToDates('hello')).toBe('hello');
+    expect(convertDateStringsToDates(true)).toBe(true);
+  });
+
+  // Test case 7: Edge case - empty objects/arrays
+  test('should handle empty objects and arrays correctly', () => {
+    const emptyObject = {};
+    const emptyArray = [];
+    expect(convertDateStringsToDates(emptyObject)).toEqual({});
+    expect(convertDateStringsToDates(emptyArray)).toEqual([]);
+    expect(convertDateStringsToDates(emptyObject)).toBe(emptyObject);
+    expect(convertDateStringsToDates(emptyArray)).toEqual(emptyArray);
+  });
+
+  // Test case 8: Date string with different milliseconds (isoDateRegex without .000)
+  test('should handle date strings with varied milliseconds', () => {
+    const dateString = '2023-01-15T10:00:00Z'; // No milliseconds
+    const input = { createdAt: dateString };
+    const expected = { createdAt: new Date(dateString) };
+    const result = convertDateStringsToDates(input);
+    expect(result.createdAt).toBeInstanceOf(Date);
+    expect(result.createdAt.toISOString()).toEqual('2023-01-15T10:00:00.000Z');
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 9: Object with null properties
+  test('should handle objects with null properties', () => {
+    const dateString = '2023-07-01T00:00:00.000Z';
+    const input = {
+      prop1: dateString,
+      prop2: null,
+      prop3: {
+        nestedNull: null,
+        nestedDate: dateString,
+      },
+    };
+    const expected = {
+      prop1: new Date(dateString),
+      prop2: null,
+      prop3: {
+        nestedNull: null,
+        nestedDate: new Date(dateString),
+      },
+    };
+    const result = convertDateStringsToDates(input);
+    expect(result.prop1).toBeInstanceOf(Date);
+    expect(result.prop3.nestedDate).toBeInstanceOf(Date);
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 10: Date string with UTC offset (e.g., +09:00)
+  test('should convert ISO date strings with UTC offset to Date objects', () => {
+    const dateStringWithOffset = '2025-06-12T14:00:00+09:00';
+    const input = {
+      id: 2,
+      eventTime: dateStringWithOffset,
+      details: {
+        lastActivity: '2025-06-12T05:00:00-04:00',
+      },
+    };
+    const expected = {
+      id: 2,
+      eventTime: new Date(dateStringWithOffset),
+      details: {
+        lastActivity: new Date('2025-06-12T05:00:00-04:00'),
+      },
+    };
+
+    const result = convertDateStringsToDates(input);
+
+    expect(result.eventTime).toBeInstanceOf(Date);
+    expect(result.eventTime.toISOString()).toEqual(new Date(dateStringWithOffset).toISOString());
+    expect(result.details.lastActivity).toBeInstanceOf(Date);
+    expect(result.details.lastActivity.toISOString()).toEqual(new Date('2025-06-12T05:00:00-04:00').toISOString());
+
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 11: Date string with negative UTC offset
+  test('should convert ISO date strings with negative UTC offset (-05:00) to Date objects', () => {
+    const dateStringWithNegativeOffset = '2025-01-01T10:00:00-05:00';
+    const input = {
+      startTime: dateStringWithNegativeOffset,
+    };
+    const expected = {
+      startTime: new Date(dateStringWithNegativeOffset),
+    };
+
+    const result = convertDateStringsToDates(input);
+
+    expect(result.startTime).toBeInstanceOf(Date);
+    expect(result.startTime.toISOString()).toEqual(new Date(dateStringWithNegativeOffset).toISOString());
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 12: Date string with zero UTC offset (+00:00)
+  test('should convert ISO date strings with explicit zero UTC offset (+00:00) to Date objects', () => {
+    const dateStringWithZeroOffset = '2025-03-15T12:00:00+00:00';
+    const input = {
+      zeroOffsetDate: dateStringWithZeroOffset,
+    };
+    const expected = {
+      zeroOffsetDate: new Date(dateStringWithZeroOffset),
+    };
+
+    const result = convertDateStringsToDates(input);
+
+    expect(result.zeroOffsetDate).toBeInstanceOf(Date);
+    expect(result.zeroOffsetDate.toISOString()).toEqual(new Date(dateStringWithZeroOffset).toISOString());
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 13: Date string with milliseconds and UTC offset
+  test('should convert ISO date strings with milliseconds and UTC offset to Date objects', () => {
+    const dateStringWithMsAndOffset = '2025-10-20T23:59:59.999-07:00';
+    const input = {
+      detailedTime: dateStringWithMsAndOffset,
+    };
+    const expected = {
+      detailedTime: new Date(dateStringWithMsAndOffset),
+    };
+
+    const result = convertDateStringsToDates(input);
+
+    expect(result.detailedTime).toBeInstanceOf(Date);
+    expect(result.detailedTime.toISOString()).toEqual(new Date(dateStringWithMsAndOffset).toISOString());
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 14: Should NOT convert strings that look like dates but are NOT ISO 8601 or missing timezone
+  test('should NOT convert non-ISO 8601 date-like strings or strings missing timezone', () => {
+    const nonIsoDate1 = '2025/06/12 14:00:00Z'; // Wrong separator
+    const nonIsoDate2 = '2025-06-12T14:00:00'; // Missing timezone
+    const nonIsoDate3 = 'June 12, 2025 14:00:00 GMT'; // Different format
+    const nonIsoDate4 = '2025-06-12T14:00:00+0900'; // Missing colon in offset
+    const nonIsoDate5 = '2025-06-12'; // Date only
+
+    const input = {
+      date1: nonIsoDate1,
+      date2: nonIsoDate2,
+      date3: nonIsoDate3,
+      date4: nonIsoDate4,
+      date5: nonIsoDate5,
+      someOtherString: 'hello world',
+    };
+
+    // Deep copy to ensure comparison is accurate since the function modifies in place
+    const expected = JSON.parse(JSON.stringify(input));
+
+    const result = convertDateStringsToDates(input);
+
+    // Assert that they remain strings (or whatever their original type was)
+    expect(typeof result.date1).toBe('string');
+    expect(typeof result.date2).toBe('string');
+    expect(typeof result.date3).toBe('string');
+    expect(typeof result.date4).toBe('string');
+    expect(typeof result.date5).toBe('string');
+    expect(typeof result.someOtherString).toBe('string');
+
+    // Ensure the entire object is unchanged for these properties
+    expect(result.date1).toEqual(nonIsoDate1);
+    expect(result.date2).toEqual(nonIsoDate2);
+    expect(result.date3).toEqual(nonIsoDate3);
+    expect(result.date4).toEqual(nonIsoDate4);
+    expect(result.date5).toEqual(nonIsoDate5);
+    expect(result.someOtherString).toEqual('hello world');
+
+    // Finally, assert that the overall result is identical to the input for these non-matching strings
+    expect(result).toEqual(expected);
+  });
+
+});

+ 46 - 0
apps/app/src/utils/axios.ts

@@ -6,11 +6,57 @@ import qs from 'qs';
 // eslint-disable-next-line no-restricted-imports
 export * from 'axios';
 
+const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(Z|[+-]\d{2}:\d{2})$/;
+
+export function convertDateStringsToDates(data: any): any {
+  if (typeof data !== 'object' || data === null) {
+    if (typeof data === 'string' && isoDateRegex.test(data)) {
+      return new Date(data);
+    }
+    return data;
+  }
+
+  if (Array.isArray(data)) {
+    return data.map(item => convertDateStringsToDates(item));
+  }
+
+  for (const key of Object.keys(data)) {
+    const value = data[key];
+    if (typeof value === 'string' && isoDateRegex.test(value)) {
+      data[key] = new Date(value);
+    }
+
+    else if (typeof value === 'object' && value !== null) {
+      data[key] = convertDateStringsToDates(value);
+    }
+  }
+  return data;
+}
+
+// Determine the base array of transformers
+let baseTransformers = axios.defaults.transformResponse;
+
+if (baseTransformers == null) {
+  baseTransformers = [];
+}
+
+else if (!Array.isArray(baseTransformers)) {
+  // If it's a single transformer function, wrap it in an array
+  baseTransformers = [baseTransformers];
+}
+
+
 const customAxios = axios.create({
   headers: {
     'X-Requested-With': 'XMLHttpRequest',
     'Content-Type': 'application/json',
   },
+
+  transformResponse: baseTransformers.concat(
+    (data) => {
+      return convertDateStringsToDates(data);
+    },
+  ),
 });
 
 // serialize Date config: https://github.com/axios/axios/issues/1548#issuecomment-548306666

+ 3 - 7
biome.json

@@ -19,14 +19,10 @@
       ".stylelintrc.json",
       "package.json",
       "./apps/**",
-      "./packages/core/**",
-      "./packages/core-styles/**",
-      "./packages/custom-icons/**",
+      "./packages/core/src/utils/**",
+      "./packages/core/src/models/**",
       "./packages/editor/**",
-      "./packages/pdf-converter-client/**",
-      "./packages/pluginkit/**",
-      "./packages/presentation/**",
-      "./packages/remark-attachment-refs/**"
+      "./packages/pdf-converter-client/src/index.ts"
     ]
   },
   "formatter": {

+ 4 - 0
packages/core/.eslintignore

@@ -1 +1,5 @@
 /dist/**
+/src/consts/**
+/src/interfaces/**
+/src/remark-plugins/**
+/src/swr/**

+ 1 - 3
packages/core/.eslintrc.cjs

@@ -1,5 +1,3 @@
 module.exports = {
-  extends: [
-    'plugin:vitest/recommended',
-  ],
+  extends: ['plugin:vitest/recommended'],
 };

+ 1 - 1
packages/core/package.json

@@ -60,7 +60,7 @@
     "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "pnpm run dev -w --emptyOutDir=false",
-    "lint:js": "eslint **/*.{js,ts}",
+    "lint:js": "biome check",
     "lint:typecheck": "vue-tsc --noEmit",
     "lint": "npm-run-all -p lint:*",
     "test": "vitest run --coverage"

+ 2 - 1
packages/core/src/consts/accepted-upload-file-type.ts

@@ -3,4 +3,5 @@ export const AcceptedUploadFileType = {
   IMAGE: 'image',
   NONE: 'none',
 } as const;
-export type AcceptedUploadFileType = typeof AcceptedUploadFileType[keyof typeof AcceptedUploadFileType];
+export type AcceptedUploadFileType =
+  (typeof AcceptedUploadFileType)[keyof typeof AcceptedUploadFileType];

+ 2 - 1
packages/core/src/consts/growi-plugin.ts

@@ -4,4 +4,5 @@ export const GrowiPluginType = {
   Theme: 'theme',
   Script: 'script',
 } as const;
-export type GrowiPluginType = typeof GrowiPluginType[keyof typeof GrowiPluginType];
+export type GrowiPluginType =
+  (typeof GrowiPluginType)[keyof typeof GrowiPluginType];

+ 4 - 2
packages/core/src/consts/system.ts

@@ -12,6 +12,8 @@ export const GrowiDeploymentType = {
   others: 'others',
 } as const;
 
-export type GrowiServiceType = typeof GrowiServiceType[keyof typeof GrowiServiceType]
+export type GrowiServiceType =
+  (typeof GrowiServiceType)[keyof typeof GrowiServiceType];
 
-export type GrowiDeploymentType = typeof GrowiDeploymentType[keyof typeof GrowiDeploymentType]
+export type GrowiDeploymentType =
+  (typeof GrowiDeploymentType)[keyof typeof GrowiDeploymentType];

+ 1 - 1
packages/core/src/consts/ydoc-status.ts

@@ -12,4 +12,4 @@ export const YDocStatus = {
   OUTDATED: 'outdated',
   ISOLATED: 'isolated',
 } as const;
-export type YDocStatus = typeof YDocStatus[keyof typeof YDocStatus]
+export type YDocStatus = (typeof YDocStatus)[keyof typeof YDocStatus];

+ 14 - 14
packages/core/src/interfaces/attachment.ts

@@ -1,25 +1,25 @@
 import type { Ref } from './common';
-import { HasObjectId } from './has-object-id';
+import type { HasObjectId } from './has-object-id';
 import type { IPage } from './page';
 import type { IUser } from './user';
 
 export type IAttachment = {
-  page?: Ref<IPage>,
-  creator?: Ref<IUser>,
-  filePath?: string, // DEPRECATED: remains for backward compatibility for v3.3.x or below
-  fileName: string,
-  fileFormat: string,
-  fileSize: number,
-  originalName: string,
-  temporaryUrlCached?: string,
-  temporaryUrlExpiredAt?: Date,
-  attachmentType: string,
+  page?: Ref<IPage>;
+  creator?: Ref<IUser>;
+  filePath?: string; // DEPRECATED: remains for backward compatibility for v3.3.x or below
+  fileName: string;
+  fileFormat: string;
+  fileSize: number;
+  originalName: string;
+  temporaryUrlCached?: string;
+  temporaryUrlExpiredAt?: Date;
+  attachmentType: string;
 
-  createdAt: Date,
+  createdAt: Date;
 
   // virtual property
-  filePathProxied: string,
-  downloadPathProxied: string,
+  filePathProxied: string;
+  downloadPathProxied: string;
 };
 
 export type IAttachmentHasId = IAttachment & HasObjectId;

+ 1 - 1
packages/core/src/interfaces/color-scheme.ts

@@ -2,4 +2,4 @@ export const ColorScheme = {
   LIGHT: 'light',
   DARK: 'dark',
 } as const;
-export type ColorScheme = typeof ColorScheme[keyof typeof ColorScheme];
+export type ColorScheme = (typeof ColorScheme)[keyof typeof ColorScheme];

+ 1 - 6
packages/core/src/interfaces/common.spec.ts

@@ -4,10 +4,9 @@ import { mock } from 'vitest-mock-extended';
 
 import { getIdForRef, isPopulated } from './common';
 import type { IPageHasId } from './page';
-import { type IPage } from './page';
+import type { IPage } from './page';
 
 describe('isPopulated', () => {
-
   it('should return true when the argument implements HasObjectId', () => {
     // Arrange
     const ref = mock<IPageHasId>();
@@ -51,12 +50,9 @@ describe('isPopulated', () => {
     // Assert
     expect(result).toBe(false);
   });
-
 });
 
-
 describe('getIdForRef', () => {
-
   it('should return the id string when the argument is populated', () => {
     // Arrange
     const id = new Types.ObjectId();
@@ -106,5 +102,4 @@ describe('getIdForRef', () => {
     // Assert
     expect(result).toStrictEqual(ref);
   });
-
 });

+ 18 - 14
packages/core/src/interfaces/common.ts

@@ -9,29 +9,33 @@ import { isValidObjectId } from '../utils/objectid-utils';
 type ObjectId = Types.ObjectId;
 
 // Foreign key field
-export type Ref<T> = string | ObjectId | T & { _id: string | ObjectId };
+export type Ref<T> = string | ObjectId | (T & { _id: string | ObjectId });
 
 export type Nullable<T> = T | null | undefined;
 
 export const isRef = <T>(obj: unknown): obj is Ref<T> => {
-  return obj != null
-    && (
-      (typeof obj === 'string' && isValidObjectId(obj))
-        || (typeof obj === 'object' && '_bsontype' in obj && obj._bsontype === 'ObjectID')
-        || (typeof obj === 'object' && '_id' in obj)
-    );
+  return (
+    obj != null &&
+    ((typeof obj === 'string' && isValidObjectId(obj)) ||
+      (typeof obj === 'object' &&
+        '_bsontype' in obj &&
+        obj._bsontype === 'ObjectID') ||
+      (typeof obj === 'object' && '_id' in obj))
+  );
 };
 
-export const isPopulated = <T>(ref: Ref<T>): ref is T & { _id: string | ObjectId } => {
-  return ref != null
-    && typeof ref !== 'string'
-    && !('_bsontype' in ref && ref._bsontype === 'ObjectID');
+export const isPopulated = <T>(
+  ref: Ref<T>,
+): ref is T & { _id: string | ObjectId } => {
+  return (
+    ref != null &&
+    typeof ref !== 'string' &&
+    !('_bsontype' in ref && ref._bsontype === 'ObjectID')
+  );
 };
 
 export const getIdForRef = <T>(ref: Ref<T>): string | ObjectId => {
-  return isPopulated(ref)
-    ? ref._id
-    : ref;
+  return isPopulated(ref) ? ref._id : ref;
 };
 
 export const getIdStringForRef = <T>(ref: Ref<T>): string => {

+ 23 - 12
packages/core/src/interfaces/config-manager.ts

@@ -5,7 +5,7 @@ export const ConfigSource = {
   env: 'env',
   db: 'db',
 } as const;
-export type ConfigSource = typeof ConfigSource[keyof typeof ConfigSource];
+export type ConfigSource = (typeof ConfigSource)[keyof typeof ConfigSource];
 
 /**
  * Metadata for a configuration value
@@ -19,12 +19,14 @@ export interface ConfigDefinition<T> {
 /**
  * Helper function for defining configurations with type safety
  */
-export const defineConfig = <T>(config: ConfigDefinition<T>): ConfigDefinition<T> => config;
+export const defineConfig = <T>(
+  config: ConfigDefinition<T>,
+): ConfigDefinition<T> => config;
 
 /**
  * Interface for loading configuration values
  */
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
+// biome-ignore lint/suspicious/noExplicitAny: ignore
 export interface IConfigLoader<K extends string, V extends Record<K, any>> {
   /**
    * Load configurations from environment variables
@@ -37,11 +39,14 @@ export interface IConfigLoader<K extends string, V extends Record<K, any>> {
   loadFromDB(): Promise<RawConfigData<K, V>>;
 }
 
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type RawConfigData<K extends string, V extends Record<K, any>> = Record<K, {
-  value: V[K];
-  definition?: ConfigDefinition<V[K]>;
-}>;
+// biome-ignore lint/suspicious/noExplicitAny: ignore
+export type RawConfigData<K extends string, V extends Record<K, any>> = Record<
+  K,
+  {
+    value: V[K];
+    definition?: ConfigDefinition<V[K]>;
+  }
+>;
 
 export type UpdateConfigOptions = {
   skipPubsub?: boolean;
@@ -51,7 +56,7 @@ export type UpdateConfigOptions = {
 /**
  * Interface for managing configuration values
  */
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
+// biome-ignore lint/suspicious/noExplicitAny: ignore
 export interface IConfigManager<K extends string, V extends Record<K, any>> {
   /**
    * Load configurations
@@ -67,12 +72,19 @@ export interface IConfigManager<K extends string, V extends Record<K, any>> {
   /**
    * Update a configuration value
    */
-  updateConfig<T extends K>(key: T, value: V[T], options?: UpdateConfigOptions): Promise<void>;
+  updateConfig<T extends K>(
+    key: T,
+    value: V[T],
+    options?: UpdateConfigOptions,
+  ): Promise<void>;
 
   /**
    * Update multiple configuration values
    */
-  updateConfigs(updates: Partial<{ [T in K]: V[T] }>, options?: UpdateConfigOptions): Promise<void>;
+  updateConfigs(
+    updates: Partial<{ [T in K]: V[T] }>,
+    options?: UpdateConfigOptions,
+  ): Promise<void>;
 
   /**
    * Remove multiple configuration values
@@ -83,5 +95,4 @@ export interface IConfigManager<K extends string, V extends Record<K, any>> {
    * Get environment variables managed with ConfigDefinitions
    */
   getManagedEnvVars(showSecretValues: boolean): Record<string, string>;
-
 }

+ 4 - 4
packages/core/src/interfaces/external-account.ts

@@ -2,7 +2,7 @@ import type { Ref } from './common';
 import type { IUser } from './user';
 
 export type IExternalAccount<P> = {
-  providerType: P,
-  accountId: string,
-  user: Ref<IUser>,
-}
+  providerType: P;
+  accountId: string;
+  user: Ref<IUser>;
+};

+ 17 - 17
packages/core/src/interfaces/growi-app-info.ts

@@ -3,29 +3,29 @@ import type * as os from 'node:os';
 import type { GrowiDeploymentType, GrowiServiceType } from '../consts/system';
 
 export const GrowiWikiType = { open: 'open', closed: 'closed' } as const;
-type GrowiWikiType = typeof GrowiWikiType[keyof typeof GrowiWikiType]
+type GrowiWikiType = (typeof GrowiWikiType)[keyof typeof GrowiWikiType];
 
 interface IGrowiOSInfo {
-  type?: ReturnType<typeof os.type>
-  platform?: ReturnType<typeof os.platform>
-  arch?: ReturnType<typeof os.arch>
-  totalmem?: ReturnType<typeof os.totalmem>
+  type?: ReturnType<typeof os.type>;
+  platform?: ReturnType<typeof os.platform>;
+  arch?: ReturnType<typeof os.arch>;
+  totalmem?: ReturnType<typeof os.totalmem>;
 }
 
 export interface IGrowiAdditionalInfo {
-  installedAt: Date
-  installedAtByOldestUser: Date | null
-  currentUsersCount: number
-  currentActiveUsersCount: number
+  installedAt: Date;
+  installedAtByOldestUser: Date | null;
+  currentUsersCount: number;
+  currentActiveUsersCount: number;
 }
 
 export interface IGrowiInfo<A extends object = IGrowiAdditionalInfo> {
-  serviceInstanceId: string
-  appSiteUrl: string
-  osInfo: IGrowiOSInfo
-  version: string
-  type: GrowiServiceType
-  wikiType: GrowiWikiType
-  deploymentType: GrowiDeploymentType
-  additionalInfo?: A
+  serviceInstanceId: string;
+  appSiteUrl: string;
+  osInfo: IGrowiOSInfo;
+  version: string;
+  type: GrowiServiceType;
+  wikiType: GrowiWikiType;
+  deploymentType: GrowiDeploymentType;
+  additionalInfo?: A;
 }

+ 10 - 4
packages/core/src/interfaces/growi-facade.ts

@@ -1,12 +1,18 @@
 export type GrowiFacade = {
   markdownRenderer?: {
     optionsGenerators?: {
+      // biome-ignore lint/suspicious/noExplicitAny: ignore
       generateViewOptions?: any;
+      // biome-ignore lint/suspicious/noExplicitAny: ignore
       customGenerateViewOptions?: any;
+      // biome-ignore lint/suspicious/noExplicitAny: ignore
       generatePreviewOptions?: any;
+      // biome-ignore lint/suspicious/noExplicitAny: ignore
       customGeneratePreviewOptions?: any;
-    },
-    optionsMutators?: any,
-  },
-  react?: any,
+    };
+    // biome-ignore lint/suspicious/noExplicitAny: ignore
+    optionsMutators?: any;
+  };
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
+  react?: any;
 };

+ 42 - 26
packages/core/src/interfaces/growi-theme-metadata.ts

@@ -4,36 +4,52 @@ export const GrowiThemeSchemeType = {
   ...ColorScheme,
   BOTH: 'both',
 } as const;
-export type GrowiThemeSchemeType = typeof GrowiThemeSchemeType[keyof typeof GrowiThemeSchemeType];
+export type GrowiThemeSchemeType =
+  (typeof GrowiThemeSchemeType)[keyof typeof GrowiThemeSchemeType];
 
 export type GrowiThemeMetadata = {
-  name: string,
-  manifestKey: string,
-  schemeType: GrowiThemeSchemeType,
-  lightBg: string,
-  darkBg: string,
-  lightSidebar: string,
-  darkSidebar: string,
-  lightIcon: string,
-  darkIcon: string,
-  createBtn: string,
-  isPresetTheme?: boolean,
+  name: string;
+  manifestKey: string;
+  schemeType: GrowiThemeSchemeType;
+  lightBg: string;
+  darkBg: string;
+  lightSidebar: string;
+  darkSidebar: string;
+  lightIcon: string;
+  darkIcon: string;
+  createBtn: string;
+  isPresetTheme?: boolean;
 };
 
-export const isGrowiThemeMetadata = (obj: unknown): obj is GrowiThemeMetadata => {
+export const isGrowiThemeMetadata = (
+  obj: unknown,
+): obj is GrowiThemeMetadata => {
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
   const objAny = obj as any;
 
-  return objAny != null
-    && typeof objAny === 'object'
-    && Array.isArray(objAny) === false
-    && 'name' in objAny && typeof objAny.name === 'string'
-    && 'manifestKey' in objAny && typeof objAny.manifestKey === 'string'
-    && 'schemeType' in objAny && typeof objAny.schemeType === 'string'
-    && 'lightBg' in objAny && typeof objAny.lightBg === 'string'
-    && 'darkBg' in objAny && typeof objAny.darkBg === 'string'
-    && 'lightSidebar' in objAny && typeof objAny.lightSidebar === 'string'
-    && 'darkSidebar' in objAny && typeof objAny.darkSidebar === 'string'
-    && 'lightIcon' in objAny && typeof objAny.lightIcon === 'string'
-    && 'darkIcon' in objAny && typeof objAny.darkIcon === 'string'
-    && 'createBtn' in objAny && typeof objAny.createBtn === 'string';
+  return (
+    objAny != null &&
+    typeof objAny === 'object' &&
+    Array.isArray(objAny) === false &&
+    'name' in objAny &&
+    typeof objAny.name === 'string' &&
+    'manifestKey' in objAny &&
+    typeof objAny.manifestKey === 'string' &&
+    'schemeType' in objAny &&
+    typeof objAny.schemeType === 'string' &&
+    'lightBg' in objAny &&
+    typeof objAny.lightBg === 'string' &&
+    'darkBg' in objAny &&
+    typeof objAny.darkBg === 'string' &&
+    'lightSidebar' in objAny &&
+    typeof objAny.lightSidebar === 'string' &&
+    'darkSidebar' in objAny &&
+    typeof objAny.darkSidebar === 'string' &&
+    'lightIcon' in objAny &&
+    typeof objAny.lightIcon === 'string' &&
+    'darkIcon' in objAny &&
+    typeof objAny.darkIcon === 'string' &&
+    'createBtn' in objAny &&
+    typeof objAny.createBtn === 'string'
+  );
 };

+ 1 - 1
packages/core/src/interfaces/has-object-id.ts

@@ -1,3 +1,3 @@
 export type HasObjectId = {
-  _id: string,
+  _id: string;
 };

+ 1 - 1
packages/core/src/interfaces/lang.ts

@@ -5,4 +5,4 @@ export const Lang = {
   fr_FR: 'fr_FR',
 } as const;
 export const AllLang = Object.values(Lang);
-export type Lang = typeof Lang[keyof typeof Lang];
+export type Lang = (typeof Lang)[keyof typeof Lang];

+ 122 - 89
packages/core/src/interfaces/page.ts

@@ -1,56 +1,69 @@
 import type { Ref } from './common';
 import type { HasObjectId } from './has-object-id';
-import type { IRevision, HasRevisionShortbody, IRevisionHasId } from './revision';
+import type {
+  HasRevisionShortbody,
+  IRevision,
+  IRevisionHasId,
+} from './revision';
 import type { SubscriptionStatusType } from './subscription';
 import type { ITag } from './tag';
-import type {
-  IUser, IUserGroup, IUserGroupHasId, IUserHasId,
-} from './user';
+import type { IUser, IUserGroup, IUserGroupHasId, IUserHasId } from './user';
 
-export const GroupType = { userGroup: 'UserGroup', externalUserGroup: 'ExternalUserGroup' } as const;
-export type GroupType = typeof GroupType[keyof typeof GroupType];
+export const GroupType = {
+  userGroup: 'UserGroup',
+  externalUserGroup: 'ExternalUserGroup',
+} as const;
+export type GroupType = (typeof GroupType)[keyof typeof GroupType];
 
 export type IGrantedGroup = {
-  type: GroupType,
-  item: Ref<IUserGroup>,
-}
+  type: GroupType;
+  item: Ref<IUserGroup>;
+};
 
 export type IPage = {
-  path: string,
-  status: string,
-  revision?: Ref<IRevision>,
-  tags: Ref<ITag>[],
-  creator?: Ref<IUser>,
-  createdAt: Date,
-  updatedAt: Date,
-  seenUsers: Ref<IUser>[],
-  parent: Ref<IPage> | null,
-  descendantCount: number,
-  isEmpty: boolean,
-  grant: PageGrant,
-  grantedUsers: Ref<IUser>[],
-  grantedGroups: IGrantedGroup[],
-  lastUpdateUser?: Ref<IUser>,
-  liker: Ref<IUser>[],
-  commentCount: number
-  slackChannels: string,
-  deleteUser: Ref<IUser>,
-  deletedAt: Date,
-  latestRevision?: Ref<IRevision>,
-  latestRevisionBodyLength?: number,
-  expandContentWidth?: boolean,
-  wip?: boolean,
-  ttlTimestamp?: Date
-}
-
-export type IPagePopulatedToShowRevision = Omit<IPageHasId, 'lastUpdateUser'|'creator'|'deleteUser'|'grantedGroups'|'revision'|'author'> & {
-  lastUpdateUser?: IUserHasId,
-  creator?: IUserHasId,
-  deleteUser: IUserHasId,
-  grantedGroups: { type: GroupType, item: IUserGroupHasId }[],
-  revision?: IRevisionHasId,
-  author: IUserHasId,
-}
+  path: string;
+  status: string;
+  revision?: Ref<IRevision>;
+  tags: Ref<ITag>[];
+  creator?: Ref<IUser>;
+  createdAt: Date;
+  updatedAt: Date;
+  seenUsers: Ref<IUser>[];
+  parent: Ref<IPage> | null;
+  descendantCount: number;
+  isEmpty: boolean;
+  grant: PageGrant;
+  grantedUsers: Ref<IUser>[];
+  grantedGroups: IGrantedGroup[];
+  lastUpdateUser?: Ref<IUser>;
+  liker: Ref<IUser>[];
+  commentCount: number;
+  slackChannels: string;
+  deleteUser: Ref<IUser>;
+  deletedAt: Date;
+  latestRevision?: Ref<IRevision>;
+  latestRevisionBodyLength?: number;
+  expandContentWidth?: boolean;
+  wip?: boolean;
+  ttlTimestamp?: Date;
+};
+
+export type IPagePopulatedToShowRevision = Omit<
+  IPageHasId,
+  | 'lastUpdateUser'
+  | 'creator'
+  | 'deleteUser'
+  | 'grantedGroups'
+  | 'revision'
+  | 'author'
+> & {
+  lastUpdateUser?: IUserHasId;
+  creator?: IUserHasId;
+  deleteUser: IUserHasId;
+  grantedGroups: { type: GroupType; item: IUserGroupHasId }[];
+  revision?: IRevisionHasId;
+  author: IUserHasId;
+};
 
 export const PageGrant = {
   GRANT_PUBLIC: 1,
@@ -60,68 +73,83 @@ export const PageGrant = {
   GRANT_USER_GROUP: 5,
 } as const;
 type UnionPageGrantKeys = keyof typeof PageGrant;
-export type PageGrant = typeof PageGrant[UnionPageGrantKeys];
+export type PageGrant = (typeof PageGrant)[UnionPageGrantKeys];
 
 export const PageStatus = {
   STATUS_PUBLISHED: 'published',
   STATUS_DELETED: 'deleted',
 } as const;
-export type PageStatus = typeof PageStatus[keyof typeof PageStatus];
+export type PageStatus = (typeof PageStatus)[keyof typeof PageStatus];
 
 export type IPageHasId = IPage & HasObjectId;
 
 export type IPageInfo = {
-  isV5Compatible: boolean,
-  isEmpty: boolean,
-  isMovable: boolean,
-  isDeletable: boolean,
-  isAbleToDeleteCompletely: boolean,
-  isRevertible: boolean,
-}
+  isV5Compatible: boolean;
+  isEmpty: boolean;
+  isMovable: boolean;
+  isDeletable: boolean;
+  isAbleToDeleteCompletely: boolean;
+  isRevertible: boolean;
+};
 
 export type IPageInfoForEntity = IPageInfo & {
-  bookmarkCount: number,
-  sumOfLikers: number,
-  likerIds: string[],
-  sumOfSeenUsers: number,
-  seenUserIds: string[],
-  contentAge: number,
-  descendantCount: number,
-  commentCount: number,
-}
+  bookmarkCount: number;
+  sumOfLikers: number;
+  likerIds: string[];
+  sumOfSeenUsers: number;
+  seenUserIds: string[];
+  contentAge: number;
+  descendantCount: number;
+  commentCount: number;
+};
 
 export type IPageInfoForOperation = IPageInfoForEntity & {
-  isBookmarked?: boolean,
-  isLiked?: boolean,
-  subscriptionStatus?: SubscriptionStatusType,
-}
+  isBookmarked?: boolean;
+  isLiked?: boolean;
+  subscriptionStatus?: SubscriptionStatusType;
+};
 
 export type IPageInfoForListing = IPageInfoForEntity & HasRevisionShortbody;
 
-export type IPageInfoAll = IPageInfo | IPageInfoForEntity | IPageInfoForOperation | IPageInfoForListing;
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const isIPageInfo = (pageInfo: any | undefined): pageInfo is IPageInfo => {
-  return pageInfo != null && pageInfo instanceof Object
-    && ('isEmpty' in pageInfo);
+export type IPageInfoAll =
+  | IPageInfo
+  | IPageInfoForEntity
+  | IPageInfoForOperation
+  | IPageInfoForListing;
+
+export const isIPageInfo = (
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
+  pageInfo: any | undefined,
+): pageInfo is IPageInfo => {
+  return (
+    pageInfo != null && pageInfo instanceof Object && 'isEmpty' in pageInfo
+  );
 };
 
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const isIPageInfoForEntity = (pageInfo: any | undefined): pageInfo is IPageInfoForEntity => {
-  return isIPageInfo(pageInfo)
-    && pageInfo.isEmpty === false;
+export const isIPageInfoForEntity = (
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
+  pageInfo: any | undefined,
+): pageInfo is IPageInfoForEntity => {
+  return isIPageInfo(pageInfo) && pageInfo.isEmpty === false;
 };
 
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const isIPageInfoForOperation = (pageInfo: any | undefined): pageInfo is IPageInfoForOperation => {
-  return isIPageInfoForEntity(pageInfo)
-    && ('isBookmarked' in pageInfo || 'isLiked' in pageInfo || 'subscriptionStatus' in pageInfo);
+export const isIPageInfoForOperation = (
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
+  pageInfo: any | undefined,
+): pageInfo is IPageInfoForOperation => {
+  return (
+    isIPageInfoForEntity(pageInfo) &&
+    ('isBookmarked' in pageInfo ||
+      'isLiked' in pageInfo ||
+      'subscriptionStatus' in pageInfo)
+  );
 };
 
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const isIPageInfoForListing = (pageInfo: any | undefined): pageInfo is IPageInfoForListing => {
-  return isIPageInfoForEntity(pageInfo)
-    && 'revisionShortBody' in pageInfo;
+export const isIPageInfoForListing = (
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
+  pageInfo: any | undefined,
+): pageInfo is IPageInfoForListing => {
+  return isIPageInfoForEntity(pageInfo) && 'revisionShortBody' in pageInfo;
 };
 
 // export type IPageInfoTypeResolver<T extends IPageInfo> =
@@ -141,11 +169,16 @@ export const isIPageInfoForListing = (pageInfo: any | undefined): pageInfo is IP
 // };
 
 export type IDataWithMeta<D = unknown, M = unknown> = {
-  data: D,
-  meta?: M,
-}
+  data: D;
+  meta?: M;
+};
 
 export type IPageWithMeta<M = IPageInfoAll> = IDataWithMeta<IPageHasId, M>;
 
-export type IPageToDeleteWithMeta<T = IPageInfoForEntity | unknown> = IDataWithMeta<HasObjectId & (IPage | { path: string, revision: string | null}), T>;
-export type IPageToRenameWithMeta<T = IPageInfoForEntity | unknown> = IPageToDeleteWithMeta<T>;
+export type IPageToDeleteWithMeta<T = IPageInfoForEntity | unknown> =
+  IDataWithMeta<
+    HasObjectId & (IPage | { path: string; revision: string | null }),
+    T
+  >;
+export type IPageToRenameWithMeta<T = IPageInfoForEntity | unknown> =
+  IPageToDeleteWithMeta<T>;

+ 121 - 99
packages/core/src/interfaces/primitive/string.spec.ts

@@ -1,147 +1,169 @@
 import { describe, expect, it } from 'vitest';
 
 import {
-  isNonEmptyString,
-  toNonEmptyString,
-  toNonEmptyStringOrUndefined,
   isNonBlankString,
+  isNonEmptyString,
   toNonBlankString,
   toNonBlankStringOrUndefined,
+  toNonEmptyString,
+  toNonEmptyStringOrUndefined,
 } from './string';
 
 describe('isNonEmptyString', () => {
   /* eslint-disable indent */
   it.each`
-    input         | expected      | description
-    ${'hello'}    | ${true}       | ${'non-empty string'}
-    ${'world'}    | ${true}       | ${'non-empty string'}
-    ${'a'}        | ${true}       | ${'single character'}
-    ${'1'}        | ${true}       | ${'numeric string'}
-    ${' '}        | ${true}       | ${'space character'}
-    ${'   '}      | ${true}       | ${'multiple spaces'}
-    ${''}         | ${false}      | ${'empty string'}
-    ${null}       | ${false}      | ${'null'}
-    ${undefined}  | ${false}      | ${'undefined'}
-  `('should return $expected for $description: $input', ({ input, expected }) => {
-  /* eslint-enable indent */
-    expect(isNonEmptyString(input)).toBe(expected);
-  });
+    input        | expected | description
+    ${'hello'}   | ${true}  | ${'non-empty string'}
+    ${'world'}   | ${true}  | ${'non-empty string'}
+    ${'a'}       | ${true}  | ${'single character'}
+    ${'1'}       | ${true}  | ${'numeric string'}
+    ${' '}       | ${true}  | ${'space character'}
+    ${'   '}     | ${true}  | ${'multiple spaces'}
+    ${''}        | ${false} | ${'empty string'}
+    ${null}      | ${false} | ${'null'}
+    ${undefined} | ${false} | ${'undefined'}
+  `(
+    'should return $expected for $description: $input',
+    ({ input, expected }) => {
+      /* eslint-enable indent */
+      expect(isNonEmptyString(input)).toBe(expected);
+    },
+  );
 });
 
 describe('isNonBlankString', () => {
   /* eslint-disable indent */
   it.each`
-    input         | expected      | description
-    ${'hello'}    | ${true}       | ${'non-blank string'}
-    ${'world'}    | ${true}       | ${'non-blank string'}
-    ${'a'}        | ${true}       | ${'single character'}
-    ${'1'}        | ${true}       | ${'numeric string'}
-    ${' '}        | ${false}      | ${'space character'}
-    ${'   '}      | ${false}      | ${'multiple spaces'}
-    ${'\t'}       | ${false}      | ${'tab character'}
-    ${'\n'}       | ${false}      | ${'newline character'}
-    ${''}         | ${false}      | ${'empty string'}
-    ${null}       | ${false}      | ${'null'}
-    ${undefined}  | ${false}      | ${'undefined'}
-  `('should return $expected for $description: $input', ({ input, expected }) => {
-  /* eslint-enable indent */
-    expect(isNonBlankString(input)).toBe(expected);
-  });
+    input        | expected | description
+    ${'hello'}   | ${true}  | ${'non-blank string'}
+    ${'world'}   | ${true}  | ${'non-blank string'}
+    ${'a'}       | ${true}  | ${'single character'}
+    ${'1'}       | ${true}  | ${'numeric string'}
+    ${' '}       | ${false} | ${'space character'}
+    ${'   '}     | ${false} | ${'multiple spaces'}
+    ${'\t'}      | ${false} | ${'tab character'}
+    ${'\n'}      | ${false} | ${'newline character'}
+    ${''}        | ${false} | ${'empty string'}
+    ${null}      | ${false} | ${'null'}
+    ${undefined} | ${false} | ${'undefined'}
+  `(
+    'should return $expected for $description: $input',
+    ({ input, expected }) => {
+      /* eslint-enable indent */
+      expect(isNonBlankString(input)).toBe(expected);
+    },
+  );
 });
 
 describe('toNonEmptyStringOrUndefined', () => {
   /* eslint-disable indent */
   it.each`
-    input         | expected      | description
-    ${'hello'}    | ${'hello'}    | ${'non-empty string'}
-    ${'world'}    | ${'world'}    | ${'non-empty string'}
-    ${'a'}        | ${'a'}        | ${'single character'}
-    ${'1'}        | ${'1'}        | ${'numeric string'}
-    ${' '}        | ${' '}        | ${'space character'}
-    ${'   '}      | ${'   '}      | ${'multiple spaces'}
-    ${''}         | ${undefined}  | ${'empty string'}
-    ${null}       | ${undefined}  | ${'null'}
-    ${undefined}  | ${undefined}  | ${'undefined'}
-  `('should return $expected for $description: $input', ({ input, expected }) => {
-  /* eslint-enable indent */
-    expect(toNonEmptyStringOrUndefined(input)).toBe(expected);
-  });
+    input        | expected     | description
+    ${'hello'}   | ${'hello'}   | ${'non-empty string'}
+    ${'world'}   | ${'world'}   | ${'non-empty string'}
+    ${'a'}       | ${'a'}       | ${'single character'}
+    ${'1'}       | ${'1'}       | ${'numeric string'}
+    ${' '}       | ${' '}       | ${'space character'}
+    ${'   '}     | ${'   '}     | ${'multiple spaces'}
+    ${''}        | ${undefined} | ${'empty string'}
+    ${null}      | ${undefined} | ${'null'}
+    ${undefined} | ${undefined} | ${'undefined'}
+  `(
+    'should return $expected for $description: $input',
+    ({ input, expected }) => {
+      /* eslint-enable indent */
+      expect(toNonEmptyStringOrUndefined(input)).toBe(expected);
+    },
+  );
 });
 
 describe('toNonBlankStringOrUndefined', () => {
   /* eslint-disable indent */
   it.each`
-    input         | expected      | description
-    ${'hello'}    | ${'hello'}    | ${'non-blank string'}
-    ${'world'}    | ${'world'}    | ${'non-blank string'}
-    ${'a'}        | ${'a'}        | ${'single character'}
-    ${'1'}        | ${'1'}        | ${'numeric string'}
-    ${' '}        | ${undefined}  | ${'space character'}
-    ${'   '}      | ${undefined}  | ${'multiple spaces'}
-    ${'\t'}       | ${undefined}  | ${'tab character'}
-    ${'\n'}       | ${undefined}  | ${'newline character'}
-    ${''}         | ${undefined}  | ${'empty string'}
-    ${null}       | ${undefined}  | ${'null'}
-    ${undefined}  | ${undefined}  | ${'undefined'}
-  `('should return $expected for $description: $input', ({ input, expected }) => {
-  /* eslint-enable indent */
-    expect(toNonBlankStringOrUndefined(input)).toBe(expected);
-  });
+    input        | expected     | description
+    ${'hello'}   | ${'hello'}   | ${'non-blank string'}
+    ${'world'}   | ${'world'}   | ${'non-blank string'}
+    ${'a'}       | ${'a'}       | ${'single character'}
+    ${'1'}       | ${'1'}       | ${'numeric string'}
+    ${' '}       | ${undefined} | ${'space character'}
+    ${'   '}     | ${undefined} | ${'multiple spaces'}
+    ${'\t'}      | ${undefined} | ${'tab character'}
+    ${'\n'}      | ${undefined} | ${'newline character'}
+    ${''}        | ${undefined} | ${'empty string'}
+    ${null}      | ${undefined} | ${'null'}
+    ${undefined} | ${undefined} | ${'undefined'}
+  `(
+    'should return $expected for $description: $input',
+    ({ input, expected }) => {
+      /* eslint-enable indent */
+      expect(toNonBlankStringOrUndefined(input)).toBe(expected);
+    },
+  );
 });
 
 describe('toNonEmptyString', () => {
   /* eslint-disable indent */
   it.each`
-    input         | expected      | description
-    ${'hello'}    | ${'hello'}    | ${'non-empty string'}
-    ${'world'}    | ${'world'}    | ${'non-empty string'}
-    ${'a'}        | ${'a'}        | ${'single character'}
-    ${'1'}        | ${'1'}        | ${'numeric string'}
-    ${' '}        | ${' '}        | ${'space character'}
-    ${'   '}      | ${'   '}      | ${'multiple spaces'}
-  `('should return $expected for valid $description: $input', ({ input, expected }) => {
-  /* eslint-enable indent */
-    expect(toNonEmptyString(input)).toBe(expected);
-  });
+    input      | expected   | description
+    ${'hello'} | ${'hello'} | ${'non-empty string'}
+    ${'world'} | ${'world'} | ${'non-empty string'}
+    ${'a'}     | ${'a'}     | ${'single character'}
+    ${'1'}     | ${'1'}     | ${'numeric string'}
+    ${' '}     | ${' '}     | ${'space character'}
+    ${'   '}   | ${'   '}   | ${'multiple spaces'}
+  `(
+    'should return $expected for valid $description: $input',
+    ({ input, expected }) => {
+      /* eslint-enable indent */
+      expect(toNonEmptyString(input)).toBe(expected);
+    },
+  );
 
   /* eslint-disable indent */
   it.each`
-    input         | description
-    ${''}         | ${'empty string'}
-    ${null}       | ${'null'}
-    ${undefined}  | ${'undefined'}
+    input        | description
+    ${''}        | ${'empty string'}
+    ${null}      | ${'null'}
+    ${undefined} | ${'undefined'}
   `('should throw error for invalid $description: $input', ({ input }) => {
-  /* eslint-enable indent */
-    expect(() => toNonEmptyString(input)).toThrow('Expected a non-empty string, but received:');
+    /* eslint-enable indent */
+    expect(() => toNonEmptyString(input)).toThrow(
+      'Expected a non-empty string, but received:',
+    );
   });
 });
 
 describe('toNonBlankString', () => {
   /* eslint-disable indent */
   it.each`
-    input         | expected      | description
-    ${'hello'}    | ${'hello'}    | ${'non-blank string'}
-    ${'world'}    | ${'world'}    | ${'non-blank string'}
-    ${'a'}        | ${'a'}        | ${'single character'}
-    ${'1'}        | ${'1'}        | ${'numeric string'}
-  `('should return $expected for valid $description: $input', ({ input, expected }) => {
-  /* eslint-enable indent */
-    expect(toNonBlankString(input)).toBe(expected);
-  });
+    input      | expected   | description
+    ${'hello'} | ${'hello'} | ${'non-blank string'}
+    ${'world'} | ${'world'} | ${'non-blank string'}
+    ${'a'}     | ${'a'}     | ${'single character'}
+    ${'1'}     | ${'1'}     | ${'numeric string'}
+  `(
+    'should return $expected for valid $description: $input',
+    ({ input, expected }) => {
+      /* eslint-enable indent */
+      expect(toNonBlankString(input)).toBe(expected);
+    },
+  );
 
   /* eslint-disable indent */
   it.each`
-    input         | description
-    ${' '}        | ${'space character'}
-    ${'   '}      | ${'multiple spaces'}
-    ${'\t'}       | ${'tab character'}
-    ${'\n'}       | ${'newline character'}
-    ${''}         | ${'empty string'}
-    ${null}       | ${'null'}
-    ${undefined}  | ${'undefined'}
+    input        | description
+    ${' '}       | ${'space character'}
+    ${'   '}     | ${'multiple spaces'}
+    ${'\t'}      | ${'tab character'}
+    ${'\n'}      | ${'newline character'}
+    ${''}        | ${'empty string'}
+    ${null}      | ${'null'}
+    ${undefined} | ${'undefined'}
   `('should throw error for invalid $description: $input', ({ input }) => {
-  /* eslint-enable indent */
-    expect(() => toNonBlankString(input)).toThrow('Expected a non-blank string, but received:');
+    /* eslint-enable indent */
+    expect(() => toNonBlankString(input)).toThrow(
+      'Expected a non-blank string, but received:',
+    );
   });
 });
 

+ 16 - 6
packages/core/src/interfaces/primitive/string.ts

@@ -9,7 +9,9 @@ export type NonEmptyString = string & { readonly __brand: unique symbol };
  * @param value - The value to check
  * @returns True if the value is a string with length > 0, false otherwise
  */
-export const isNonEmptyString = (value: string | null | undefined): value is NonEmptyString => {
+export const isNonEmptyString = (
+  value: string | null | undefined,
+): value is NonEmptyString => {
   return value != null && value.length > 0;
 };
 
@@ -21,7 +23,8 @@ export const isNonEmptyString = (value: string | null | undefined): value is Non
  */
 export const toNonEmptyString = (value: string): NonEmptyString => {
   // throw Error if the value is null, undefined or empty
-  if (!isNonEmptyString(value)) throw new Error(`Expected a non-empty string, but received: ${value}`);
+  if (!isNonEmptyString(value))
+    throw new Error(`Expected a non-empty string, but received: ${value}`);
   return value;
 };
 
@@ -30,7 +33,9 @@ export const toNonEmptyString = (value: string): NonEmptyString => {
  * @param value - The string to convert
  * @returns The string as NonEmptyString type, or undefined if the value is null, undefined, or empty
  */
-export const toNonEmptyStringOrUndefined = (value: string | null | undefined): NonEmptyString | undefined => {
+export const toNonEmptyStringOrUndefined = (
+  value: string | null | undefined,
+): NonEmptyString | undefined => {
   // return undefined if the value is null, undefined or empty
   if (!isNonEmptyString(value)) return undefined;
   return value;
@@ -49,7 +54,9 @@ export type NonBlankString = string & { readonly __brand: unique symbol };
  * @param value - The value to check
  * @returns True if the value is a string with trimmed length > 0, false otherwise
  */
-export const isNonBlankString = (value: string | null | undefined): value is NonBlankString => {
+export const isNonBlankString = (
+  value: string | null | undefined,
+): value is NonBlankString => {
   return value != null && value.trim().length > 0;
 };
 
@@ -61,7 +68,8 @@ export const isNonBlankString = (value: string | null | undefined): value is Non
  */
 export const toNonBlankString = (value: string): NonBlankString => {
   // throw Error if the value is null, undefined or empty
-  if (!isNonBlankString(value)) throw new Error(`Expected a non-blank string, but received: ${value}`);
+  if (!isNonBlankString(value))
+    throw new Error(`Expected a non-blank string, but received: ${value}`);
   return value;
 };
 
@@ -70,7 +78,9 @@ export const toNonBlankString = (value: string): NonBlankString => {
  * @param value - The string to convert
  * @returns The string as NonBlankString type, or undefined if the value is null, undefined, empty, or contains only whitespace characters
  */
-export const toNonBlankStringOrUndefined = (value: string | null | undefined): NonBlankString | undefined => {
+export const toNonBlankStringOrUndefined = (
+  value: string | null | undefined,
+): NonBlankString | undefined => {
   // return undefined if the value is null, undefined or blank (empty or whitespace only)
   if (!isNonBlankString(value)) return undefined;
   return value;

+ 18 - 18
packages/core/src/interfaces/revision.ts

@@ -8,33 +8,33 @@ export const Origin = {
   Editor: 'editor',
 } as const;
 
-export type Origin = typeof Origin[keyof typeof Origin];
+export type Origin = (typeof Origin)[keyof typeof Origin];
 
 export const allOrigin = Object.values(Origin);
 
 export type IRevision = {
-  pageId: Ref<IPage>,
-  body: string,
-  author: Ref<IUser>,
-  format: string,
+  pageId: Ref<IPage>;
+  body: string;
+  author: Ref<IUser>;
+  format: string;
   hasDiffToPrev?: boolean;
-  origin?: Origin,
-  createdAt: Date,
-  updatedAt: Date,
-}
+  origin?: Origin;
+  createdAt: Date;
+  updatedAt: Date;
+};
 
 export type IRevisionHasId = IRevision & HasObjectId;
 
 export type IRevisionsForPagination = {
-  revisions: IRevisionHasId[], // revisions in one pagination
-  totalCounts: number // total counts
-}
+  revisions: IRevisionHasId[]; // revisions in one pagination
+  totalCounts: number; // total counts
+};
 export type HasRevisionShortbody = {
-  revisionShortBody?: string,
-}
+  revisionShortBody?: string;
+};
 
 export type SWRInfinitePageRevisionsResponse = {
-  revisions: IRevisionHasId[],
-  totalCount: number,
-  offset: number,
-}
+  revisions: IRevisionHasId[];
+  totalCount: number;
+  offset: number;
+};

+ 9 - 8
packages/core/src/interfaces/subscription.ts

@@ -7,15 +7,16 @@ export const SubscriptionStatusType = {
   UNSUBSCRIBE: 'UNSUBSCRIBE',
 } as const;
 export const AllSubscriptionStatusType = Object.values(SubscriptionStatusType);
-export type SubscriptionStatusType = typeof SubscriptionStatusType[keyof typeof SubscriptionStatusType];
+export type SubscriptionStatusType =
+  (typeof SubscriptionStatusType)[keyof typeof SubscriptionStatusType];
 
 export interface ISubscription {
-  user: Ref<IUser>
-  targetModel: string
-  target: Ref<IPage>
-  status: string
-  createdAt: Date
+  user: Ref<IUser>;
+  targetModel: string;
+  target: Ref<IPage>;
+  status: string;
+  createdAt: Date;
 
-  isSubscribing(): boolean
-  isUnsubscribing(): boolean
+  isSubscribing(): boolean;
+  isUnsubscribing(): boolean;
 }

+ 3 - 3
packages/core/src/interfaces/tag.ts

@@ -1,4 +1,4 @@
 export type ITag<ID = string> = {
-  _id: ID
-  name: string,
-}
+  _id: ID;
+  name: string;
+};

+ 33 - 33
packages/core/src/interfaces/user.ts

@@ -4,40 +4,40 @@ import type { HasObjectId } from './has-object-id';
 import type { Lang } from './lang';
 
 export type IUser = {
-  name: string,
-  username: string,
-  email: string,
-  password: string,
-  image?: string, // for backward conpatibility
-  imageAttachment?: Ref<IAttachment>,
-  imageUrlCached: string,
-  isGravatarEnabled: boolean,
-  admin: boolean,
-  readOnly: boolean,
-  apiToken?: string,
-  isEmailPublished: boolean,
-  isInvitationEmailSended: boolean,
-  lang: Lang,
-  slackMemberId?: string,
-  createdAt: Date,
-  lastLoginAt?: Date,
-  introduction: string,
-  status: IUserStatus,
-  isQuestionnaireEnabled: boolean,
-}
+  name: string;
+  username: string;
+  email: string;
+  password: string;
+  image?: string; // for backward conpatibility
+  imageAttachment?: Ref<IAttachment>;
+  imageUrlCached: string;
+  isGravatarEnabled: boolean;
+  admin: boolean;
+  readOnly: boolean;
+  apiToken?: string;
+  isEmailPublished: boolean;
+  isInvitationEmailSended: boolean;
+  lang: Lang;
+  slackMemberId?: string;
+  createdAt: Date;
+  lastLoginAt?: Date;
+  introduction: string;
+  status: IUserStatus;
+  isQuestionnaireEnabled: boolean;
+};
 
 export type IUserGroupRelation = {
-  relatedGroup: Ref<IUserGroup>,
-  relatedUser: Ref<IUser>,
-  createdAt: Date,
-}
+  relatedGroup: Ref<IUserGroup>;
+  relatedUser: Ref<IUser>;
+  createdAt: Date;
+};
 
 export type IUserGroup = {
   name: string;
   createdAt: Date;
   description: string;
   parent: Ref<IUserGroup> | null;
-}
+};
 
 export const USER_STATUS = {
   REGISTERED: 1,
@@ -46,16 +46,16 @@ export const USER_STATUS = {
   DELETED: 4,
   INVITED: 5,
 } as const;
-export type IUserStatus = typeof USER_STATUS[keyof typeof USER_STATUS]
+export type IUserStatus = (typeof USER_STATUS)[keyof typeof USER_STATUS];
 
 export type IUserHasId = IUser & HasObjectId;
 export type IUserGroupHasId = IUserGroup & HasObjectId;
 export type IUserGroupRelationHasId = IUserGroupRelation & HasObjectId;
 
 export type IAdminExternalAccount<P> = {
-  _id: string,
-  providerType: P,
-  accountId: string,
-  user: IUser,
-  createdAt: Date,
-}
+  _id: string;
+  providerType: P;
+  accountId: string;
+  user: IUser;
+  createdAt: Date;
+};

+ 7 - 7
packages/core/src/interfaces/vite.ts

@@ -1,10 +1,10 @@
 export type ViteManifestValue = {
-  file: string,
-  src: string,
-  isEntry?: boolean,
-  css?: string[],
-}
+  file: string;
+  src: string;
+  isEntry?: boolean;
+  css?: string[];
+};
 
 export type ViteManifest = {
-  [key: string]: ViteManifestValue,
-}
+  [key: string]: ViteManifestValue;
+};

+ 3 - 3
packages/core/src/remark-plugins/interfaces/option-parser.ts

@@ -1,4 +1,4 @@
 export type ParseRangeResult = {
-  start: number,
-  end: number,
-}
+  start: number;
+  end: number;
+};

+ 14 - 13
packages/core/src/remark-plugins/util/option-parser.spec.ts

@@ -1,26 +1,27 @@
 import { OptionParser } from './option-parser';
 
 describe('option-parser', () => {
-
   test.concurrent.each`
     arg
     ${'aaa'}
     ${'5++2'}
     ${'5:+2'}
-  `('.parseRange(\'$arg\') returns null', ({ arg }) => {
+  `(".parseRange('$arg') returns null", ({ arg }) => {
     expect(OptionParser.parseRange(arg)).toBeNull();
   });
 
   test.concurrent.each`
-    arg       | start | end
-    ${'1'}    | ${1} | ${1}
-    ${'2:1'}  | ${2} | ${1}
-    ${'2:'}   | ${2} | ${-1}
-    ${'10:-3'}   | ${10} | ${-3}
-    ${'5+2'}   | ${5} | ${7}
-    ${'5+'}   | ${5} | ${5}
-  `('.parseRange(\'$arg\') returns { start: $start, end : $end }', ({ arg, start, end }) => {
-    expect(OptionParser.parseRange(arg)).toEqual({ start, end });
-  });
-
+    arg        | start | end
+    ${'1'}     | ${1}  | ${1}
+    ${'2:1'}   | ${2}  | ${1}
+    ${'2:'}    | ${2}  | ${-1}
+    ${'10:-3'} | ${10} | ${-3}
+    ${'5+2'}   | ${5}  | ${7}
+    ${'5+'}    | ${5}  | ${5}
+  `(
+    ".parseRange('$arg') returns { start: $start, end : $end }",
+    ({ arg, start, end }) => {
+      expect(OptionParser.parseRange(arg)).toEqual({ start, end });
+    },
+  );
 });

+ 6 - 9
packages/core/src/remark-plugins/util/option-parser.ts

@@ -3,8 +3,7 @@ import type { ParseRangeResult } from '../interfaces/option-parser';
 /**
  * Options parser for custom tag
  */
-export class OptionParser {
-
+export const OptionParser = {
   /**
    * Parse range expression
    *
@@ -23,7 +22,7 @@ export class OptionParser {
    * @param {string} str
    * @returns {ParseRangeResult}
    */
-  static parseRange(str: string): ParseRangeResult | null {
+  parseRange(str: string): ParseRangeResult | null {
     if (str == null) {
       return null;
     }
@@ -35,7 +34,7 @@ export class OptionParser {
     }
 
     // determine start
-    let start;
+    let start: number;
     let end = -1;
 
     // has operator
@@ -46,8 +45,7 @@ export class OptionParser {
       // determine end
       if (operator === ':') {
         end = +match[4] || -1; // set last(-1) if undefined
-      }
-      else if (operator === '+') {
+      } else if (operator === '+') {
         end = +match[4] || 0; // plus zero if undefined
         end += start;
       }
@@ -59,6 +57,5 @@ export class OptionParser {
     }
 
     return { start, end };
-  }
-
-}
+  },
+};

+ 23 - 10
packages/core/src/swr/use-swr-static.ts

@@ -1,18 +1,31 @@
 import {
-  Key, SWRConfiguration, SWRResponse, useSWRConfig,
+  type Key,
+  type SWRConfiguration,
+  type SWRResponse,
+  useSWRConfig,
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
-
 export function useSWRStatic<Data, Error>(key: Key): SWRResponse<Data, Error>;
-export function useSWRStatic<Data, Error>(key: Key, data: Data | undefined): SWRResponse<Data, Error>;
-export function useSWRStatic<Data, Error>(key: Key, data: Data | undefined,
-  configuration: SWRConfiguration<Data, Error> | undefined): SWRResponse<Data, Error>;
+export function useSWRStatic<Data, Error>(
+  key: Key,
+  data: Data | undefined,
+): SWRResponse<Data, Error>;
+export function useSWRStatic<Data, Error>(
+  key: Key,
+  data: Data | undefined,
+  configuration: SWRConfiguration<Data, Error> | undefined,
+): SWRResponse<Data, Error>;
 
 export function useSWRStatic<Data, Error>(
-    ...args: readonly [Key]
+  ...args:
+    | readonly [Key]
     | readonly [Key, Data | undefined]
-    | readonly [Key, Data | undefined, SWRConfiguration<Data, Error> | undefined]
+    | readonly [
+        Key,
+        Data | undefined,
+        SWRConfiguration<Data, Error> | undefined,
+      ]
 ): SWRResponse<Data, Error> {
   const [key, data, configuration] = args;
 
@@ -23,9 +36,9 @@ export function useSWRStatic<Data, Error>(
   const { cache } = useSWRConfig();
   const swrResponse = useSWRImmutable(key, null, {
     ...configuration,
-    fallbackData: configuration?.fallbackData ?? (
-      key != null ? cache.get(key?.toString())?.data : undefined
-    ),
+    fallbackData:
+      configuration?.fallbackData ??
+      (key != null ? cache.get(key?.toString())?.data : undefined),
   });
 
   // update data

+ 6 - 1
packages/core/src/swr/with-utils.ts

@@ -1,7 +1,12 @@
 import type { SWRResponse } from 'swr';
 
+// biome-ignore lint/suspicious/noExplicitAny: ignore
 export type SWRResponseWithUtils<U, D = any, E = any> = SWRResponse<D, E> & U;
 
-export const withUtils = <U, D = any, E = any>(response: SWRResponse<D, E>, utils: U): SWRResponseWithUtils<U, D, E> => {
+// biome-ignore lint/suspicious/noExplicitAny: ignore
+export const withUtils = <U, D = any, E = any>(
+  response: SWRResponse<D, E>,
+  utils: U,
+): SWRResponseWithUtils<U, D, E> => {
   return Object.assign(response, utils);
 };

+ 2 - 6
packages/core/tsconfig.json

@@ -6,11 +6,7 @@
     "paths": {
       "~/*": ["./src/*"]
     },
-    "types": [
-      "vitest/globals"
-    ]
+    "types": ["vitest/globals"]
   },
-  "include": [
-    "src", "test"
-  ]
+  "include": ["src", "test"]
 }

+ 1 - 1
packages/core/vite.config.ts

@@ -1,4 +1,4 @@
-import path from 'path';
+import path from 'node:path';
 
 import glob from 'glob';
 import { nodeExternals } from 'rollup-plugin-node-externals';

+ 1 - 3
packages/core/vitest.config.ts

@@ -2,9 +2,7 @@ import tsconfigPaths from 'vite-tsconfig-paths';
 import { defineConfig } from 'vitest/config';
 
 export default defineConfig({
-  plugins: [
-    tsconfigPaths(),
-  ],
+  plugins: [tsconfigPaths()],
   test: {
     environment: 'node',
     clearMocks: true,

+ 1 - 0
packages/pdf-converter-client/.eslintignore

@@ -0,0 +1 @@
+*

+ 0 - 11
packages/pdf-converter-client/.eslintrc.cjs

@@ -1,11 +0,0 @@
-/**
- * @type {import('eslint').Linter.Config}
- */
-module.exports = {
-  extends: '../../.eslintrc.js',
-  ignorePatterns: [
-    'src/index.ts',
-    'dist/index.d.ts',
-    'dist/index.js',
-  ],
-};

+ 1 - 1
packages/pdf-converter-client/package.json

@@ -6,7 +6,7 @@
   "license": "MIT",
   "private": true,
   "scripts": {
-    "lint": "pnpm eslint **/*.{js,ts}",
+    "lint": "biome check",
     "gen:client-code": "orval",
     "dev": "pnpm gen:client-code && tsc -p tsconfig.json",
     "build": "pnpm gen:client-code && tsc -p tsconfig.json"

+ 1 - 1
packages/pluginkit/.eslintignore

@@ -1 +1 @@
-/dist/**
+*

+ 0 - 5
packages/pluginkit/.eslintrc.cjs

@@ -1,5 +0,0 @@
-module.exports = {
-  extends: [
-    'plugin:vitest/recommended',
-  ],
-};

+ 1 - 1
packages/pluginkit/package.json

@@ -15,7 +15,7 @@
     "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "pnpm run dev -w --emptyOutDir=false",
-    "lint:js": "eslint **/*.{js,ts}",
+    "lint:js": "biome check",
     "lint:typecheck": "vue-tsc --noEmit",
     "lint": "npm-run-all -p lint:*",
     "test": "vitest run --coverage"

+ 9 - 7
packages/pluginkit/src/model/growi-plugin-package-data.ts

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

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

@@ -3,17 +3,17 @@ import type { GrowiPluginType, GrowiThemeMetadata } from '@growi/core';
 import type { GrowiPluginDirective } from './growi-plugin-package-data';
 
 export type GrowiPluginValidationData = {
-  projectDirRoot: string,
-  growiPlugin: GrowiPluginDirective,
-  schemaVersion: number,
-  expectedPluginType?: GrowiPluginType,
-  actualPluginTypes?: GrowiPluginType[],
+  projectDirRoot: string;
+  growiPlugin: GrowiPluginDirective;
+  schemaVersion: number;
+  expectedPluginType?: GrowiPluginType;
+  actualPluginTypes?: GrowiPluginType[];
 };
 
 export type GrowiTemplatePluginValidationData = GrowiPluginValidationData & {
-  supportingLocales: string[],
-}
+  supportingLocales: string[];
+};
 
 export type GrowiThemePluginValidationData = GrowiPluginValidationData & {
-  themes: GrowiThemeMetadata[],
-}
+  themes: GrowiThemeMetadata[];
+};

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

@@ -2,14 +2,14 @@ import ExtensibleCustomError from 'extensible-custom-error';
 
 import type { GrowiPluginValidationData } from './growi-plugin-validation-data';
 
-
-export class GrowiPluginValidationError<E extends Partial<GrowiPluginValidationData> = Partial<GrowiPluginValidationData>> extends ExtensibleCustomError {
-
+export class GrowiPluginValidationError<
+  E extends
+    Partial<GrowiPluginValidationData> = Partial<GrowiPluginValidationData>,
+> extends ExtensibleCustomError {
   data?: E;
 
   constructor(message: string, data?: E) {
     super(message);
     this.data = data;
   }
-
 }

+ 6 - 2
packages/pluginkit/src/v4/client/utils/growi-facade/growi-react.spec.ts

@@ -8,14 +8,18 @@ describe('growiReact()', () => {
 
   afterEach(() => {
     process.env.NODE_ENV = originalNodeEnv;
-    delete (global as any).window.growiFacade;
+    // biome-ignore lint/suspicious/noExplicitAny: ignore
+    (global as any).window.growiFacade = undefined;
   });
 
   it('returns window.growiFacade.react in production mode', () => {
     // given
     process.env.NODE_ENV = 'production';
-    const mockProductionReact = { useEffect: () => {} } as unknown as typeof React;
+    const mockProductionReact = {
+      useEffect: () => {},
+    } as unknown as typeof React;
 
+    // biome-ignore lint/suspicious/noExplicitAny: ignore
     (global as any).window = {
       growiFacade: {
         react: mockProductionReact,

+ 1 - 2
packages/pluginkit/src/v4/client/utils/growi-facade/growi-react.ts

@@ -2,10 +2,9 @@ import type React from 'react';
 
 import type { GrowiFacade } from '@growi/core';
 
-
 declare global {
   interface Window {
-    growiFacade: GrowiFacade
+    growiFacade: GrowiFacade;
   }
 }
 

+ 18 - 16
packages/pluginkit/src/v4/interfaces/template.ts

@@ -1,25 +1,27 @@
 export type TemplateStatusBasis = {
-  id: string,
-  locale: string,
-  pluginId?: string,
-}
+  id: string;
+  locale: string;
+  pluginId?: string;
+};
 export type TemplateStatusValid = TemplateStatusBasis & {
-  isValid: true,
-  isDefault: boolean,
-  title: string,
-  desc?: string,
-}
+  isValid: true;
+  isDefault: boolean;
+  title: string;
+  desc?: string;
+};
 export type TemplateStatusInvalid = TemplateStatusBasis & {
-  isValid: false,
-  invalidReason: string,
-}
+  isValid: false;
+  invalidReason: string;
+};
 export type TemplateStatus = TemplateStatusValid | TemplateStatusInvalid;
 
-export function isTemplateStatusValid(status: TemplateStatus): status is TemplateStatusValid {
+export function isTemplateStatusValid(
+  status: TemplateStatus,
+): status is TemplateStatusValid {
   return status.isValid;
 }
 
 export type TemplateSummary = {
-  default: TemplateStatusValid,
-  [locale: string]: TemplateStatus,
-}
+  default: TemplateStatusValid;
+  [locale: string]: TemplateStatus;
+};

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

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

+ 5 - 3
packages/pluginkit/src/v4/server/utils/common/import-package-json.ts

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

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

@@ -2,7 +2,6 @@ import { GrowiPluginType } from '@growi/core';
 
 import examplePkg from '../../../../../test/fixtures/example-package/template1/package.json';
 
-
 import { validateGrowiDirective } from './validate-growi-plugin-directive';
 
 const mocks = vi.hoisted(() => {
@@ -16,8 +15,7 @@ vi.mock('./import-package-json', () => {
 });
 
 describe('validateGrowiDirective()', () => {
-
-  it('returns a data object', async() => {
+  it('returns a data object', async () => {
     // setup
     mocks.importPackageJsonMock.mockReturnValue(examplePkg);
 
@@ -28,28 +26,34 @@ describe('validateGrowiDirective()', () => {
     expect(data).not.toBeNull();
   });
 
-  it("with the 'expectedPluginType' argument returns a data object", async() => {
+  it("with the 'expectedPluginType' argument returns a data object", async () => {
     // setup
     mocks.importPackageJsonMock.mockReturnValue(examplePkg);
 
     // when
-    const data = validateGrowiDirective('package.json', GrowiPluginType.Template);
+    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') };
+      const caller = () => {
+        validateGrowiDirective('package.json');
+      };
 
       // then
-      expect(caller).toThrow("The package.json does not have 'growiPlugin' directive.");
+      expect(caller).toThrow(
+        "The package.json does not have 'growiPlugin' directive.",
+      );
     });
 
     it("when the 'schemaVersion' is NaN", () => {
@@ -61,10 +65,14 @@ describe('validateGrowiDirective()', () => {
       });
 
       // when
-      const caller = () => { validateGrowiDirective('package.json') };
+      const caller = () => {
+        validateGrowiDirective('package.json');
+      };
 
       // then
-      expect(caller).toThrow("The growiPlugin directive must have a valid 'schemaVersion' directive.");
+      expect(caller).toThrow(
+        "The growiPlugin directive must have a valid 'schemaVersion' directive.",
+      );
     });
 
     it("when the 'schemaVersion' is less than 4", () => {
@@ -76,10 +84,14 @@ describe('validateGrowiDirective()', () => {
       });
 
       // when
-      const caller = () => { validateGrowiDirective('package.json') };
+      const caller = () => {
+        validateGrowiDirective('package.json');
+      };
 
       // then
-      expect(caller).toThrow("The growiPlugin directive must have a valid 'schemaVersion' directive.");
+      expect(caller).toThrow(
+        "The growiPlugin directive must have a valid 'schemaVersion' directive.",
+      );
     });
 
     it("when the 'types' directive does not exist", () => {
@@ -91,10 +103,14 @@ describe('validateGrowiDirective()', () => {
       });
 
       // when
-      const caller = () => { validateGrowiDirective('package.json') };
+      const caller = () => {
+        validateGrowiDirective('package.json');
+      };
 
       // then
-      expect(caller).toThrow("The growiPlugin directive does not have 'types' directive.");
+      expect(caller).toThrow(
+        "The growiPlugin directive does not have 'types' directive.",
+      );
     });
 
     it("when the 'types' directive does not have expected plugin type", () => {
@@ -107,11 +123,14 @@ describe('validateGrowiDirective()', () => {
       });
 
       // when
-      const caller = () => { validateGrowiDirective('package.json', GrowiPluginType.Script) };
+      const caller = () => {
+        validateGrowiDirective('package.json', GrowiPluginType.Script);
+      };
 
       // then
-      expect(caller).toThrow("The growiPlugin directive does not have expected plugin type in 'types' directive.");
+      expect(caller).toThrow(
+        "The growiPlugin directive does not have expected plugin type in 'types' directive.",
+      );
     });
   });
-
 });

+ 29 - 8
packages/pluginkit/src/v4/server/utils/common/validate-growi-plugin-directive.ts

@@ -1,32 +1,50 @@
 import type { GrowiPluginType } from '@growi/core';
 
-import { type GrowiPluginValidationData, GrowiPluginValidationError } from '../../../../model';
+import {
+  type GrowiPluginValidationData,
+  GrowiPluginValidationError,
+} from '../../../../model';
 
 import { importPackageJson } from './import-package-json';
 
-
-export const validateGrowiDirective = (projectDirRoot: string, expectedPluginType?: GrowiPluginType): GrowiPluginValidationData => {
+export const validateGrowiDirective = (
+  projectDirRoot: string,
+  expectedPluginType?: GrowiPluginType,
+): GrowiPluginValidationData => {
   const pkg = importPackageJson(projectDirRoot);
 
   const { growiPlugin } = pkg;
 
-  const data: GrowiPluginValidationData = { projectDirRoot, schemaVersion: NaN, growiPlugin };
+  const data: GrowiPluginValidationData = {
+    projectDirRoot,
+    schemaVersion: Number.NaN,
+    growiPlugin,
+  };
 
   if (growiPlugin == null) {
-    throw new GrowiPluginValidationError("The package.json does not have 'growiPlugin' directive.", data);
+    throw new GrowiPluginValidationError(
+      "The package.json does not have 'growiPlugin' directive.",
+      data,
+    );
   }
 
   // schema version checking
   const schemaVersion = Number(growiPlugin.schemaVersion);
   data.schemaVersion = schemaVersion;
   if (Number.isNaN(schemaVersion) || schemaVersion < 4) {
-    throw new GrowiPluginValidationError("The growiPlugin directive must have a valid 'schemaVersion' directive.", data);
+    throw new GrowiPluginValidationError(
+      "The growiPlugin directive must have a valid 'schemaVersion' directive.",
+      data,
+    );
   }
 
   const types: GrowiPluginType[] = growiPlugin.types;
   data.actualPluginTypes = types;
   if (types == null) {
-    throw new GrowiPluginValidationError("The growiPlugin directive does not have 'types' directive.", data);
+    throw new GrowiPluginValidationError(
+      "The growiPlugin directive does not have 'types' directive.",
+      data,
+    );
   }
 
   // type checking
@@ -34,7 +52,10 @@ export const validateGrowiDirective = (projectDirRoot: string, expectedPluginTyp
     data.expectedPluginType = expectedPluginType;
 
     if (!types.includes(expectedPluginType)) {
-      throw new GrowiPluginValidationError("The growiPlugin directive does not have expected plugin type in 'types' directive.", data);
+      throw new GrowiPluginValidationError(
+        "The growiPlugin directive does not have expected plugin type in 'types' directive.",
+        data,
+      );
     }
   }
 

+ 7 - 4
packages/pluginkit/src/v4/server/utils/template/get-markdown.ts

@@ -1,10 +1,13 @@
-import fs from 'fs';
-import path from 'path';
+import fs from 'node:fs';
+import path from 'node:path';
 
 import { getStatus } from './get-status';
 
-
-export const getMarkdown = async(projectDirRoot: string, templateId: string, locale: string): Promise<string> => {
+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);

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

@@ -1,15 +1,13 @@
-import fs, { readFileSync } from 'fs';
-import path from 'path';
-import { promisify } from 'util';
-
+import fs, { readFileSync } from 'node:fs';
+import path from 'node:path';
+import { promisify } from 'node:util';
 
 const statAsync = promisify(fs.stat);
 
-
 type TemplateDirStatus = {
-  isTemplateExists: boolean,
-  meta?: { [key: string]: string },
-}
+  isTemplateExists: boolean;
+  meta?: { [key: string]: string };
+};
 
 export async function getStatus(tplDir: string): Promise<TemplateDirStatus> {
   const markdownPath = path.resolve(tplDir, 'template.md');
@@ -22,7 +20,9 @@ export async function getStatus(tplDir: string): Promise<TemplateDirStatus> {
 
   const result: TemplateDirStatus = {
     isTemplateExists,
-    meta: isMetaDataFileExists ? JSON.parse(readFileSync(metaDataPath, 'utf-8')) : undefined,
+    meta: isMetaDataFileExists
+      ? JSON.parse(readFileSync(metaDataPath, 'utf-8'))
+      : undefined,
   };
 
   return result;

+ 47 - 33
packages/pluginkit/src/v4/server/utils/template/scan.ts

@@ -1,20 +1,23 @@
-import fs from 'fs';
-import path from 'path';
+import fs from 'node:fs';
+import path from 'node:path';
 
 import type { GrowiTemplatePluginValidationData } from '../../../../model';
-import { isTemplateStatusValid, type TemplateStatus, type TemplateSummary } from '../../../interfaces';
+import {
+  type TemplateStatus,
+  type TemplateSummary,
+  isTemplateStatusValid,
+} 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,
-    },
+export const scanTemplate = async (
+  projectDirRoot: string,
+  templateId: string,
+  data: GrowiTemplatePluginValidationData,
+  opts?: {
+    pluginId?: string;
+  },
 ): Promise<TemplateStatus[]> => {
   const status: TemplateStatus[] = [];
 
@@ -26,13 +29,12 @@ export const scanTemplate = async(
 
     try {
       const stats = await getStatus(tplDir);
-      const {
-        isTemplateExists, meta,
-      } = stats;
+      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.");
+      if (meta?.title == null)
+        throw new Error("'meta.md does not contain the title.");
 
       const isDefault = !isDefaultPushed;
       status.push({
@@ -45,8 +47,7 @@ export const scanTemplate = async(
         desc: meta.desc,
       });
       isDefaultPushed = true;
-    }
-    catch (err) {
+    } catch (err) {
       status.push({
         pluginId: opts?.pluginId,
         id: templateId,
@@ -58,21 +59,23 @@ export const scanTemplate = async(
   }
 
   // eslint-disable-next-line no-console
-  console.debug(`Template directory (${projectDirRoot}) has scanned`, { status });
+  console.debug(`Template directory (${projectDirRoot}) has scanned`, {
+    status,
+  });
 
   return status;
 };
 
-export const scanAllTemplates = async(
-    projectDirRoot: string,
-    opts?: {
-      data?: GrowiTemplatePluginValidationData,
-      pluginId?: string,
-      returnsInvalidTemplates?: boolean,
-    },
+export const scanAllTemplates = async (
+  projectDirRoot: string,
+  opts?: {
+    data?: GrowiTemplatePluginValidationData;
+    pluginId?: string;
+    returnsInvalidTemplates?: boolean;
+  },
 ): Promise<TemplateSummary[]> => {
-
-  const data = opts?.data ?? validateTemplatePluginGrowiDirective(projectDirRoot);
+  const data =
+    opts?.data ?? validateTemplatePluginGrowiDirective(projectDirRoot);
 
   const summaries: TemplateSummary[] = [];
 
@@ -80,14 +83,23 @@ export const scanAllTemplates = async(
   const distDirFiles = fs.readdirSync(distDirPath);
 
   for await (const templateId of distDirFiles) {
-    const status = (await scanTemplate(projectDirRoot, templateId, data, { pluginId: opts?.pluginId }))
+    const status = (
+      await scanTemplate(projectDirRoot, templateId, data, {
+        pluginId: opts?.pluginId,
+      })
+    )
       // omit invalid templates if `returnsInvalidTemplates` is true
-      .filter(s => (opts?.returnsInvalidTemplates ? true : s.isValid));
+      .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)) {
+    const defaultTemplateStatus = status.find(
+      (s) => 'isDefault' in s && s.isDefault,
+    );
+
+    if (
+      defaultTemplateStatus == null ||
+      !isTemplateStatusValid(defaultTemplateStatus)
+    ) {
       continue;
     }
 
@@ -95,7 +107,9 @@ export const scanAllTemplates = async(
       // for the 'default' key
       default: defaultTemplateStatus,
       // for each locale keys
-      ...Object.fromEntries(status.map(templateStatus => [templateStatus.locale, templateStatus])),
+      ...Object.fromEntries(
+        status.map((templateStatus) => [templateStatus.locale, templateStatus]),
+      ),
     });
   }
 

+ 17 - 9
packages/pluginkit/src/v4/server/utils/template/validate-all-locales.ts

@@ -1,11 +1,15 @@
 import { scanAllTemplates } from './scan';
 import { validateTemplatePluginGrowiDirective } from './validate-growi-plugin-directive';
 
-
-export const validateAllTemplateLocales = async(projectDirRoot: string): Promise<boolean> => {
+export const validateAllTemplateLocales = async (
+  projectDirRoot: string,
+): Promise<boolean> => {
   const data = validateTemplatePluginGrowiDirective(projectDirRoot);
 
-  const results = await scanAllTemplates(projectDirRoot, { data, returnsInvalidTemplates: true });
+  const results = await scanAllTemplates(projectDirRoot, {
+    data,
+    returnsInvalidTemplates: true,
+  });
 
   if (Object.keys(results).length === 0) {
     throw new Error('This plugin does not have any templates');
@@ -15,19 +19,23 @@ export const validateAllTemplateLocales = async(projectDirRoot: string): Promise
   // 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 summary of results) {
+    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)) {
+    if (!validMap.every((bool) => bool)) {
       // eslint-disable-next-line no-console
-      console.warn(`[WARN] Template '${id}' has some locales that status is invalid`);
+      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)) {
+    if (!validMap.some((bool) => bool)) {
       return false;
     }
   }

+ 16 - 11
packages/pluginkit/src/v4/server/utils/template/validate-growi-plugin-directive.spec.ts

@@ -1,15 +1,16 @@
-import path from 'path';
+import path from 'node:path';
 
 import { GrowiPluginType } from '@growi/core';
 
 import { validateTemplatePluginGrowiDirective } from './validate-growi-plugin-directive';
 
-
 describe('validateTemplatePluginGrowiDirective()', () => {
-
-  it('returns a data object', async() => {
+  it('returns a data object', async () => {
     // setup
-    const projectDirRoot = path.resolve(__dirname, '../../../../../test/fixtures/example-package/template1');
+    const projectDirRoot = path.resolve(
+      __dirname,
+      '../../../../../test/fixtures/example-package/template1',
+    );
 
     // when
     const data = validateTemplatePluginGrowiDirective(projectDirRoot);
@@ -22,18 +23,22 @@ describe('validateTemplatePluginGrowiDirective()', () => {
   });
 
   describe('should throw an GrowiPluginValidationError', () => {
-
     it("when the pkg does not have 'growiPlugin.locale' directive", () => {
       // setup
-      const projectDirRoot = path.resolve(__dirname, '../../../../../test/fixtures/example-package/invalid-template1');
+      const projectDirRoot = path.resolve(
+        __dirname,
+        '../../../../../test/fixtures/example-package/invalid-template1',
+      );
 
       // when
-      const caller = () => { validateTemplatePluginGrowiDirective(projectDirRoot) };
+      const caller = () => {
+        validateTemplatePluginGrowiDirective(projectDirRoot);
+      };
 
       // then
-      expect(caller).toThrow("Template plugin must have 'supportingLocales' and that must have one or more locales");
+      expect(caller).toThrow(
+        "Template plugin must have 'supportingLocales' and that must have one or more locales",
+      );
     });
-
   });
-
 });

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

@@ -1,15 +1,19 @@
 import { GrowiPluginType } from '@growi/core';
 
-import type { GrowiPluginValidationData, GrowiTemplatePluginValidationData } from '../../../../model';
+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 => {
+export const validateTemplatePluginGrowiDirective = (
+  projectDirRoot: string,
+): GrowiTemplatePluginValidationData => {
   const data = validateGrowiDirective(projectDirRoot, GrowiPluginType.Template);
 
   const { growiPlugin } = data;
@@ -17,7 +21,9 @@ export const validateTemplatePluginGrowiDirective = (projectDirRoot: string): Gr
   // check supporting locales
   const supportingLocales: string[] | undefined = growiPlugin.locales;
   if (supportingLocales == null || supportingLocales.length === 0) {
-    throw new GrowiPluginValidationError<GrowiPluginValidationData & { supportingLocales?: string[] }>(
+    throw new GrowiPluginValidationError<
+      GrowiPluginValidationData & { supportingLocales?: string[] }
+    >(
       "Template plugin must have 'supportingLocales' and that must have one or more locales",
       {
         ...data,

+ 23 - 13
packages/pluginkit/src/v4/server/utils/theme/validate-growi-plugin-directive.spec.ts

@@ -1,15 +1,16 @@
-import path from 'path';
+import path from 'node:path';
 
 import { isGrowiThemeMetadata } from '@growi/core';
 
 import { validateThemePluginGrowiDirective } from './validate-growi-plugin-directive';
 
-
 describe('validateThemePluginGrowiDirective()', () => {
-
-  it('returns a data object', async() => {
+  it('returns a data object', async () => {
     // setup
-    const projectDirRoot = path.resolve(__dirname, '../../../../../test/fixtures/example-package/theme1');
+    const projectDirRoot = path.resolve(
+      __dirname,
+      '../../../../../test/fixtures/example-package/theme1',
+    );
 
     // when
     const data = validateThemePluginGrowiDirective(projectDirRoot);
@@ -22,29 +23,38 @@ describe('validateThemePluginGrowiDirective()', () => {
   });
 
   describe('should throw an GrowiPluginValidationError', () => {
-
     it("when the pkg does not have 'growiPlugin.themes' directive", () => {
       // setup
-      const projectDirRoot = path.resolve(__dirname, '../../../../../test/fixtures/example-package/invalid-theme1');
+      const projectDirRoot = path.resolve(
+        __dirname,
+        '../../../../../test/fixtures/example-package/invalid-theme1',
+      );
 
       // when
-      const caller = () => { validateThemePluginGrowiDirective(projectDirRoot) };
+      const caller = () => {
+        validateThemePluginGrowiDirective(projectDirRoot);
+      };
 
       // then
-      expect(caller).toThrow("Theme plugin must have 'themes' array and that must have one or more theme metadata");
+      expect(caller).toThrow(
+        "Theme plugin must have 'themes' array and that must have one or more theme metadata",
+      );
     });
 
     it("when the pkg does not have 'growiPlugin.locale' directive", () => {
       // setup
-      const projectDirRoot = path.resolve(__dirname, '../../../../../test/fixtures/example-package/invalid-theme2');
+      const projectDirRoot = path.resolve(
+        __dirname,
+        '../../../../../test/fixtures/example-package/invalid-theme2',
+      );
 
       // when
-      const caller = () => { validateThemePluginGrowiDirective(projectDirRoot) };
+      const caller = () => {
+        validateThemePluginGrowiDirective(projectDirRoot);
+      };
 
       // then
       expect(caller).toThrow(/^Some of theme metadata are invalid:/);
     });
-
   });
-
 });

+ 15 - 8
packages/pluginkit/src/v4/server/utils/theme/validate-growi-plugin-directive.ts

@@ -1,22 +1,30 @@
 import type { GrowiThemeMetadata } from '@growi/core';
 import { GrowiPluginType, isGrowiThemeMetadata } from '@growi/core';
 
-import type { GrowiPluginValidationData, GrowiThemePluginValidationData } from '../../../../model';
+import type {
+  GrowiPluginValidationData,
+  GrowiThemePluginValidationData,
+} from '../../../../model';
 import { GrowiPluginValidationError } from '../../../../model';
 import { validateGrowiDirective } from '../common';
 
-
 /**
  * An utility for theme plugin which wrap 'validateGrowiDirective' of './common' module
  * @param projectDirRoot
  */
-export const validateThemePluginGrowiDirective = (projectDirRoot: string): GrowiThemePluginValidationData => {
+export const validateThemePluginGrowiDirective = (
+  projectDirRoot: string,
+): GrowiThemePluginValidationData => {
   const data = validateGrowiDirective(projectDirRoot, GrowiPluginType.Theme);
 
   const { growiPlugin } = data;
 
   // check themes
-  if (growiPlugin.themes == null || !Array.isArray(growiPlugin.themes) || growiPlugin.themes.length === 0) {
+  if (
+    growiPlugin.themes == null ||
+    !Array.isArray(growiPlugin.themes) ||
+    growiPlugin.themes.length === 0
+  ) {
     throw new GrowiPluginValidationError<GrowiPluginValidationData>(
       "Theme plugin must have 'themes' array and that must have one or more theme metadata",
     );
@@ -24,14 +32,13 @@ export const validateThemePluginGrowiDirective = (projectDirRoot: string): Growi
 
   const validMetadatas: GrowiThemeMetadata[] = [];
   const invalidObjects: unknown[] = [];
-  growiPlugin.themes.forEach((theme: unknown) => {
+  for (const theme of growiPlugin.themes) {
     if (isGrowiThemeMetadata(theme)) {
       validMetadatas.push(theme);
-    }
-    else {
+    } else {
       invalidObjects.push(theme);
     }
-  });
+  }
 
   if (invalidObjects.length > 0) {
     throw new GrowiPluginValidationError<GrowiPluginValidationData>(

+ 13 - 5
packages/pluginkit/src/v4/utils/template.spec.ts

@@ -1,6 +1,6 @@
 import type { TemplateSummary } from '../interfaces';
 
-import { getLocalizedTemplate, extractSupportedLocales } from './template';
+import { extractSupportedLocales, getLocalizedTemplate } from './template';
 
 describe('getLocalizedTemplate', () => {
   it('should return undefined if templateSummary is undefined', () => {
@@ -17,7 +17,9 @@ describe('getLocalizedTemplate', () => {
         title: 'Default Title',
       },
     };
-    expect(getLocalizedTemplate(templateSummary)).toEqual(templateSummary.default);
+    expect(getLocalizedTemplate(templateSummary)).toEqual(
+      templateSummary.default,
+    );
   });
 
   it('should return the localized template if locale is provided and exists in templateSummary', () => {
@@ -37,7 +39,9 @@ describe('getLocalizedTemplate', () => {
         title: 'Japanese Title',
       },
     };
-    expect(getLocalizedTemplate(templateSummary, 'ja_JP')).toEqual(templateSummary.ja_JP);
+    expect(getLocalizedTemplate(templateSummary, 'ja_JP')).toEqual(
+      templateSummary.ja_JP,
+    );
   });
 
   it('should return the default template if locale is provided but does not exist in templateSummary', () => {
@@ -50,7 +54,9 @@ describe('getLocalizedTemplate', () => {
         title: 'Default Title',
       },
     };
-    expect(getLocalizedTemplate(templateSummary, 'fr')).toEqual(templateSummary.default);
+    expect(getLocalizedTemplate(templateSummary, 'fr')).toEqual(
+      templateSummary.default,
+    );
   });
 });
 
@@ -76,6 +82,8 @@ describe('extractSupportedLocales', () => {
         title: 'Japanese Title',
       },
     };
-    expect(extractSupportedLocales(templateSummary)).toEqual(new Set(['en_US', 'ja_JP']));
+    expect(extractSupportedLocales(templateSummary)).toEqual(
+      new Set(['en_US', 'ja_JP']),
+    );
   });
 });

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

@@ -1,6 +1,9 @@
-import type { TemplateSummary, TemplateStatus } from '../interfaces';
+import type { TemplateStatus, TemplateSummary } from '../interfaces';
 
-export const getLocalizedTemplate = (templateSummary: TemplateSummary | undefined, locale?: string): TemplateStatus | undefined => {
+export const getLocalizedTemplate = (
+  templateSummary: TemplateSummary | undefined,
+  locale?: string,
+): TemplateStatus | undefined => {
   if (templateSummary == null) {
     return undefined;
   }
@@ -10,10 +13,12 @@ export const getLocalizedTemplate = (templateSummary: TemplateSummary | undefine
     : templateSummary.default;
 };
 
-export const extractSupportedLocales = (templateSummary: TemplateSummary | undefined): Set<string> | undefined => {
+export const extractSupportedLocales = (
+  templateSummary: TemplateSummary | undefined,
+): Set<string> | undefined => {
   if (templateSummary == null) {
     return undefined;
   }
 
-  return new Set(Object.values(templateSummary).map(s => s.locale));
+  return new Set(Object.values(templateSummary).map((s) => s.locale));
 };

+ 2 - 6
packages/pluginkit/tsconfig.json

@@ -2,11 +2,7 @@
   "$schema": "http://json.schemastore.org/tsconfig",
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
-    "types": [
-      "vitest/globals"
-    ]
+    "types": ["vitest/globals"]
   },
-  "include": [
-    "src"
-  ]
+  "include": ["src"]
 }

+ 1 - 1
packages/pluginkit/vite.config.ts

@@ -1,4 +1,4 @@
-import path from 'path';
+import path from 'node:path';
 
 import glob from 'glob';
 import { nodeExternals } from 'rollup-plugin-node-externals';

+ 2 - 4
packages/pluginkit/vitest.config.ts

@@ -1,10 +1,8 @@
 import tsconfigPaths from 'vite-tsconfig-paths';
-import { defineConfig, coverageConfigDefaults } from 'vitest/config';
+import { coverageConfigDefaults, defineConfig } from 'vitest/config';
 
 export default defineConfig({
-  plugins: [
-    tsconfigPaths(),
-  ],
+  plugins: [tsconfigPaths()],
   test: {
     environment: 'node',
     clearMocks: true,

+ 1 - 1
packages/presentation/.eslintignore

@@ -1 +1 @@
-/dist/**
+*

+ 0 - 5
packages/presentation/.eslintrc.cjs

@@ -1,5 +0,0 @@
-module.exports = {
-  extends: [
-    'weseek/react',
-  ],
-};

+ 1 - 1
packages/presentation/package.json

@@ -33,7 +33,7 @@
     "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "pnpm run dev -w --emptyOutDir=false",
-    "lint:js": "eslint **/*.{js,jsx,ts,tsx}",
+    "lint:js": "biome check",
     "lint:styles": "stylelint --allow-empty-input \"src/**/*.scss\" \"src/**/*.css\"",
     "lint:typecheck": "vue-tsc --noEmit",
     "lint": "run-p lint:*"

+ 25 - 18
packages/presentation/src/client/components/GrowiSlides.tsx

@@ -4,27 +4,33 @@ import Head from 'next/head';
 import ReactMarkdown from 'react-markdown';
 
 import type { PresentationOptions } from '../consts';
-import { MARP_CONTAINER_CLASS_NAME, presentationMarpit, slideMarpit } from '../services/growi-marpit';
+import {
+  MARP_CONTAINER_CLASS_NAME,
+  presentationMarpit,
+  slideMarpit,
+} from '../services/growi-marpit';
 import * as extractSections from '../services/renderer/extract-sections';
 
-import { PresentationRichSlideSection, RichSlideSection } from './RichSlideSection';
-
+import {
+  PresentationRichSlideSection,
+  RichSlideSection,
+} from './RichSlideSection';
 
 type Props = {
-  options: PresentationOptions,
-  children?: string,
-  presentation?: boolean,
-}
+  options: PresentationOptions;
+  children?: string;
+  presentation?: boolean;
+};
 
 export const GrowiSlides = (props: Props): JSX.Element => {
-  const {
-    options, children, presentation,
-  } = props;
-  const {
-    rendererOptions, isDarkMode, disableSeparationByHeader,
-  } = options;
-
-  if (rendererOptions == null || rendererOptions.remarkPlugins == null || rendererOptions.components == null) {
+  const { options, children, presentation } = props;
+  const { rendererOptions, isDarkMode, disableSeparationByHeader } = options;
+
+  if (
+    rendererOptions == null ||
+    rendererOptions.remarkPlugins == null ||
+    rendererOptions.components == null
+  ) {
     return <></>;
   }
 
@@ -35,7 +41,9 @@ export const GrowiSlides = (props: Props): JSX.Element => {
       disableSeparationByHeader,
     },
   ]);
-  rendererOptions.components.section = presentation ? PresentationRichSlideSection : RichSlideSection;
+  rendererOptions.components.section = presentation
+    ? PresentationRichSlideSection
+    : RichSlideSection;
 
   const marpit = presentation ? presentationMarpit : slideMarpit;
   const { css } = marpit.render('');
@@ -46,10 +54,9 @@ export const GrowiSlides = (props: Props): JSX.Element => {
       </Head>
       <div className={`slides ${MARP_CONTAINER_CLASS_NAME}`}>
         <ReactMarkdown {...rendererOptions}>
-          { children ?? '## No Contents' }
+          {children ?? '## No Contents'}
         </ReactMarkdown>
       </div>
     </>
   );
-
 };

+ 4 - 4
packages/presentation/src/client/components/MarpSlides.tsx

@@ -5,9 +5,9 @@ import Head from 'next/head';
 import { presentationMarpit, slideMarpit } from '../services/growi-marpit';
 
 type Props = {
-  children?: string,
-  presentation?: boolean,
-}
+  children?: string;
+  presentation?: boolean;
+};
 
 export const MarpSlides = (props: Props): JSX.Element => {
   const { children, presentation } = props;
@@ -20,7 +20,7 @@ export const MarpSlides = (props: Props): JSX.Element => {
         <style>{css}</style>
       </Head>
       <div
-        // eslint-disable-next-line react/no-danger
+        // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
         dangerouslySetInnerHTML={{
           // DOMpurify.sanitize delete elements in <svg> so sanitize is not used here.
           __html: html,

+ 12 - 10
packages/presentation/src/client/components/Presentation.tsx

@@ -1,4 +1,4 @@
-import { useEffect, type JSX } from 'react';
+import { type JSX, useEffect } from 'react';
 
 import Reveal from 'reveal.js';
 
@@ -10,7 +10,6 @@ import styles from './Presentation.module.scss';
 
 const moduleClass = styles['grw-presentation'] ?? '';
 
-
 const baseRevealOptions: Reveal.Options = {
   // adjust size to the marp preset size
   width: 1280,
@@ -27,14 +26,16 @@ const baseRevealOptions: Reveal.Options = {
  */
 const removeAllHiddenElements = () => {
   const sections = document.querySelectorAll(`${moduleClass} section`);
-  sections.forEach(section => section.removeAttribute('hidden'));
+  for (const section of sections) {
+    section.removeAttribute('hidden');
+  }
 };
 
 export type PresentationProps = {
-  options: PresentationOptions,
-  marp?: boolean,
-  children?: string,
-}
+  options: PresentationOptions;
+  marp?: boolean;
+  children?: string;
+};
 
 export const Presentation = (props: PresentationProps): JSX.Element => {
   const { options, marp, children } = props;
@@ -45,8 +46,7 @@ export const Presentation = (props: PresentationProps): JSX.Element => {
       return;
     }
     const deck = new Reveal({ ...baseRevealOptions, ...revealOptions });
-    deck.initialize()
-      .then(() => deck.slide(0)); // navigate to the first slide
+    deck.initialize().then(() => deck.slide(0)); // navigate to the first slide
 
     deck.on('ready', removeAllHiddenElements);
     deck.on('slidechanged', removeAllHiddenElements);
@@ -59,7 +59,9 @@ export const Presentation = (props: PresentationProps): JSX.Element => {
 
   return (
     <div className={`${moduleClass} reveal`}>
-      <Slides options={options} hasMarpFlag={marp} presentation>{children}</Slides>
+      <Slides options={options} hasMarpFlag={marp} presentation>
+        {children}
+      </Slides>
     </div>
   );
 };

+ 35 - 34
packages/presentation/src/client/components/RichSlideSection.tsx

@@ -1,47 +1,48 @@
-import type { ReactNode, JSX } from 'react';
+import type { JSX, ReactNode } from 'react';
 import React from 'react';
 
 type RichSlideSectionProps = {
-  children?: ReactNode,
-  presentation?: boolean,
-}
-
-const OriginalRichSlideSection = React.memo((props: RichSlideSectionProps): JSX.Element => {
-  const { children, presentation } = props;
-
-  return (
-    <section className={presentation ? 'm-2' : 'shadow rounded m-2'}>
-      <svg data-marpit-svg="" viewBox="0 0 1280 720">
-        <foreignObject width="1280" height="720">
-          <section>
-            {children ?? <></>}
-          </section>
-        </foreignObject>
-      </svg>
-    </section>
-  );
-});
-
+  children?: ReactNode;
+  presentation?: boolean;
+};
 
-const RichSlideSectionNoMemorized = (props: RichSlideSectionProps): JSX.Element => {
+const OriginalRichSlideSection = React.memo(
+  (props: RichSlideSectionProps): JSX.Element => {
+    const { children, presentation } = props;
+
+    return (
+      <section className={presentation ? 'm-2' : 'shadow rounded m-2'}>
+        <svg data-marpit-svg="" viewBox="0 0 1280 720">
+          <title>Rich Slide Section</title>
+          <foreignObject width="1280" height="720">
+            <section>{children}</section>
+          </foreignObject>
+        </svg>
+      </section>
+    );
+  },
+);
+
+const RichSlideSectionNoMemorized = (
+  props: RichSlideSectionProps,
+): JSX.Element => {
   const { children } = props;
 
-  return (
-    <OriginalRichSlideSection>
-      {children}
-    </OriginalRichSlideSection>
-  );
+  return <OriginalRichSlideSection>{children}</OriginalRichSlideSection>;
 };
-export const RichSlideSection = React.memo(RichSlideSectionNoMemorized) as typeof RichSlideSectionNoMemorized;
-
+export const RichSlideSection = React.memo(
+  RichSlideSectionNoMemorized,
+) as typeof RichSlideSectionNoMemorized;
 
-const PresentationRichSlideSectionNoMemorized = (props: RichSlideSectionProps): JSX.Element => {
+const PresentationRichSlideSectionNoMemorized = (
+  props: RichSlideSectionProps,
+): JSX.Element => {
   const { children } = props;
 
   return (
-    <OriginalRichSlideSection presentation>
-      {children}
-    </OriginalRichSlideSection>
+    <OriginalRichSlideSection presentation>{children}</OriginalRichSlideSection>
   );
 };
-export const PresentationRichSlideSection = React.memo(PresentationRichSlideSectionNoMemorized) as typeof PresentationRichSlideSectionNoMemorized;
+export const PresentationRichSlideSection = React.memo(
+  PresentationRichSlideSectionNoMemorized,
+) as typeof PresentationRichSlideSectionNoMemorized;

+ 13 - 13
packages/presentation/src/client/components/Slides.tsx

@@ -8,24 +8,24 @@ import { MarpSlides } from './MarpSlides';
 import styles from './Slides.module.scss';
 
 export type SlidesProps = {
-  options: PresentationOptions,
-  children?: string,
-  hasMarpFlag?: boolean,
-  presentation?: boolean,
-}
+  options: PresentationOptions;
+  children?: string;
+  hasMarpFlag?: boolean;
+  presentation?: boolean;
+};
 
 export const Slides = (props: SlidesProps): JSX.Element => {
-  const {
-    options, children, hasMarpFlag, presentation,
-  } = props;
+  const { options, children, hasMarpFlag, presentation } = props;
 
   return (
     <div className={`${styles['slides-styles']}`}>
-      {
-        hasMarpFlag
-          ? <MarpSlides presentation={presentation}>{children}</MarpSlides>
-          : <GrowiSlides options={options} presentation={presentation}>{children}</GrowiSlides>
-      }
+      {hasMarpFlag ? (
+        <MarpSlides presentation={presentation}>{children}</MarpSlides>
+      ) : (
+        <GrowiSlides options={options} presentation={presentation}>
+          {children}
+        </GrowiSlides>
+      )}
     </div>
   );
 };

+ 5 - 5
packages/presentation/src/client/consts/index.ts

@@ -2,8 +2,8 @@ import type { Options as ReactMarkdownOptions } from 'react-markdown';
 import type { Options as RevealOptions } from 'reveal.js';
 
 export type PresentationOptions = {
-  rendererOptions: ReactMarkdownOptions,
-  revealOptions?: RevealOptions,
-  isDarkMode?: boolean,
-  disableSeparationByHeader?: boolean,
-}
+  rendererOptions: ReactMarkdownOptions;
+  revealOptions?: RevealOptions;
+  isDarkMode?: boolean;
+  disableSeparationByHeader?: boolean;
+};

+ 11 - 11
packages/presentation/src/client/services/growi-marpit.ts

@@ -8,12 +8,12 @@ export const MARP_CONTAINER_CLASS_NAME = 'marpit';
 // https://github.com/marp-team/marp-vscode/blob/d9af184ed12b65bb28c0f328e250955d548ac1d1/src/plugins/line-number.ts
 const sourceMapIgnoredTypesForElements = ['inline', 'marpit_slide_open'];
 const lineNumber = (md) => {
-
-  const { marpit_slide_containers_open: marpitSlideContainersOpen } = md.renderer.rules;
+  const { marpit_slide_containers_open: marpitSlideContainersOpen } =
+    md.renderer.rules;
 
   // Enable line sync by per slides
   md.renderer.rules.marpit_slide_containers_open = (tks, i, opts, env, slf) => {
-    const slide = tks.slice(i + 1).find(t => t.type === 'marpit_slide_open');
+    const slide = tks.slice(i + 1).find((t) => t.type === 'marpit_slide_open');
 
     if (slide?.map?.length) {
       tks[i].attrJoin('class', 'has-data-line');
@@ -27,8 +27,8 @@ const lineNumber = (md) => {
   md.core.ruler.push('marp_growi_source_map_attr', (state) => {
     for (const token of state.tokens) {
       if (
-        token.map?.length
-        && !sourceMapIgnoredTypesForElements.includes(token.type)
+        token.map?.length &&
+        !sourceMapIgnoredTypesForElements.includes(token.type)
       ) {
         token.attrJoin('class', 'has-data-line');
         token.attrSet('data-line', token.map[0]);
@@ -48,15 +48,15 @@ const marpitOption: MarpOptions = {
 };
 
 const slideMarpitOption = marpitOption;
-slideMarpitOption.slideContainer = (
-  [new Element('section', { class: 'shadow rounded m-2' })]
-);
+slideMarpitOption.slideContainer = [
+  new Element('section', { class: 'shadow rounded m-2' }),
+];
 
 export const slideMarpit = new Marp(slideMarpitOption).use(lineNumber);
 
 const presentationMarpitOption = marpitOption;
-presentationMarpitOption.slideContainer = (
-  [new Element('section', { class: 'm-2' })]
-);
+presentationMarpitOption.slideContainer = [
+  new Element('section', { class: 'm-2' }),
+];
 
 export const presentationMarpit = new Marp(presentationMarpitOption);

+ 26 - 29
packages/presentation/src/client/services/renderer/extract-sections.ts

@@ -1,20 +1,21 @@
 import type { Schema as SanitizeOption } from 'hast-util-sanitize';
 import type { Plugin } from 'unified';
-import type { Parent, Node } from 'unist';
+import type { Node, Parent } from 'unist';
 import { findAfter } from 'unist-util-find-after';
 import { visit } from 'unist-util-visit';
 
-
-function wrapWithSection(parentNode: Parent, startElem: Node, endElem?: Node | null, isDarkMode?: boolean): void {
+function wrapWithSection(
+  parentNode: Parent,
+  startElem: Node,
+  endElem?: Node | null,
+  isDarkMode?: boolean,
+): void {
   const siblings = parentNode.children;
 
   const startIndex = siblings.indexOf(startElem);
   const endIndex = endElem != null ? siblings.indexOf(endElem) : undefined;
 
-  const between = siblings.slice(
-    startIndex,
-    endIndex,
-  );
+  const between = siblings.slice(startIndex, endIndex);
 
   const section = {
     type: 'section',
@@ -36,11 +37,13 @@ function removeElement(parentNode: Parent, elem: Node): void {
 }
 
 export type ExtractSectionsPluginParams = {
-  isDarkMode?: boolean,
-  disableSeparationByHeader?: boolean,
-}
+  isDarkMode?: boolean;
+  disableSeparationByHeader?: boolean;
+};
 
-export const remarkPlugin: Plugin<[ExtractSectionsPluginParams]> = (options) => {
+export const remarkPlugin: Plugin<[ExtractSectionsPluginParams]> = (
+  options,
+) => {
   const { isDarkMode, disableSeparationByHeader } = options;
 
   const startCondition = (node: Node) => {
@@ -58,30 +61,24 @@ export const remarkPlugin: Plugin<[ExtractSectionsPluginParams]> = (options) =>
 
   return (tree) => {
     // wrap with <section>
-    visit(
-      tree,
-      startCondition,
-      (node, index, parent: Parent) => {
-        if (parent == null || parent.type !== 'root' || node.type === 'yaml') {
-          return;
-        }
+    visit(tree, startCondition, (node, index, parent: Parent) => {
+      if (parent == null || parent.type !== 'root' || node.type === 'yaml') {
+        return;
+      }
 
-        const startElem = node;
-        const endElem = findAfter(parent, startElem, endCondition);
+      const startElem = node;
+      const endElem = findAfter(parent, startElem, endCondition);
 
-        wrapWithSection(parent, startElem, endElem, isDarkMode);
+      wrapWithSection(parent, startElem, endElem, isDarkMode);
 
-        // remove <hr>
-        if (endElem != null && endElem.type === 'thematicBreak') {
-          removeElement(parent, endElem);
-        }
-      },
-    );
+      // remove <hr>
+      if (endElem != null && endElem.type === 'thematicBreak') {
+        removeElement(parent, endElem);
+      }
+    });
   };
-
 };
 
-
 export const sanitizeOption: SanitizeOption = {
   tagNames: ['section'],
 };

+ 29 - 28
packages/presentation/src/services/use-slides-by-frontmatter.ts

@@ -4,79 +4,81 @@ import type { Parent, Root } from 'mdast';
 import type { Processor } from 'unified';
 
 type ParseResult = {
-  marp: boolean | undefined,
-  slide: boolean | undefined,
-}
+  marp: boolean | undefined;
+  slide: boolean | undefined;
+};
 
 const parseSlideFrontmatter = (frontmatter: string): ParseResult => {
-
-  let marp;
-  let slide;
+  let marp: boolean | undefined;
+  let slide: boolean | undefined;
 
   const lines = frontmatter.split('\n');
-  lines.forEach((line) => {
-    const [key, value] = line.split(':').map(part => part.trim());
+
+  for (const line of lines) {
+    const [key, value] = line.split(':').map((part) => part.trim());
     if (key === 'marp' && value === 'true') {
       marp = true;
     }
     if (key === 'slide' && value === 'true') {
       slide = true;
     }
-  });
+  }
 
   return { marp, slide };
 };
 
-
 type ProcessorOpts = {
-  onParsed?: (result: ParseResult) => void,
-  onSkipped?: () => void,
+  onParsed?: (result: ParseResult) => void;
+  onSkipped?: () => void;
 };
 
-const generateFrontmatterProcessor = async(opts?: ProcessorOpts) => {
-
+const generateFrontmatterProcessor = async (opts?: ProcessorOpts) => {
   const remarkFrontmatter = (await import('remark-frontmatter')).default;
   const remarkParse = (await import('remark-parse')).default;
   const remarkStringify = (await import('remark-stringify')).default;
   const unified = (await import('unified')).unified;
 
-  return (unified()
+  return unified()
     .use(remarkParse)
     .use(remarkStringify)
     .use(remarkFrontmatter, ['yaml'])
-    .use(() => ((obj: Parent) => {
+    .use(() => (obj: Parent) => {
       if (obj.children[0]?.type === 'yaml') {
         const result = parseSlideFrontmatter(obj.children[0]?.value);
         opts?.onParsed?.(result);
-      }
-      else {
+      } else {
         opts?.onSkipped?.();
       }
-    })));
+    });
 };
 
 export type UseSlide = {
-  marp?: boolean,
-}
+  marp?: boolean;
+};
 
 /**
  * Frontmatter parser for slide
  * @param markdown Markdwon document
  * @returns An UseSlide instance. If the markdown does not contain neither "marp" or "slide" attribute in frontmatter, it returns undefined.
  */
-export const useSlidesByFrontmatter = (markdown?: string, isEnabledMarp?: boolean): UseSlide | undefined => {
-
-  const [processor, setProcessor] = useState<Processor<Root, undefined, undefined, Root, string>|undefined>();
-  const [parseResult, setParseResult] = useState<UseSlide|undefined>();
+export const useSlidesByFrontmatter = (
+  markdown?: string,
+  isEnabledMarp?: boolean,
+): UseSlide | undefined => {
+  const [processor, setProcessor] = useState<
+    Processor<Root, undefined, undefined, Root, string> | undefined
+  >();
+  const [parseResult, setParseResult] = useState<UseSlide | undefined>();
 
   useEffect(() => {
     if (processor != null) {
       return;
     }
 
-    (async() => {
+    (async () => {
       const p = await generateFrontmatterProcessor({
-        onParsed: result => setParseResult(result.marp || result.slide ? result : undefined),
+        onParsed: (result) =>
+          setParseResult(result.marp || result.slide ? result : undefined),
         onSkipped: () => setParseResult(undefined),
       });
       setProcessor(p);
@@ -94,5 +96,4 @@ export const useSlidesByFrontmatter = (markdown?: string, isEnabledMarp?: boolea
   return parseResult != null
     ? { marp: isEnabledMarp && parseResult?.marp }
     : undefined;
-
 };

+ 2 - 5
packages/presentation/tsconfig.json

@@ -5,13 +5,10 @@
     "jsx": "react-jsx",
 
     "baseUrl": ".",
-    "paths": {
-    },
+    "paths": {},
 
     /* TODO: remove below flags for strict checking */
     "noImplicitAny": false
   },
-  "include": [
-    "src"
-  ]
+  "include": ["src"]
 }

+ 1 - 1
packages/presentation/vite.config.ts

@@ -1,4 +1,4 @@
-import path from 'path';
+import path from 'node:path';
 
 import react from '@vitejs/plugin-react';
 import glob from 'glob';

+ 1 - 1
packages/remark-attachment-refs/.eslintignore

@@ -1 +1 @@
-/dist/**
+*

+ 0 - 5
packages/remark-attachment-refs/.eslintrc.cjs

@@ -1,5 +0,0 @@
-module.exports = {
-  extends: [
-    'weseek/react',
-  ],
-};

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

@@ -37,7 +37,7 @@
     "watch": "run-p watch:*",
     "watch:client": "pnpm run dev:client -w --emptyOutDir=false",
     "watch:server": "pnpm run dev:server -w --emptyOutDir=false",
-    "lint:js": "eslint **/*.{js,jsx,ts,tsx}",
+    "lint:js": "biome check",
     "lint:styles": "stylelint \"src/**/*.scss\" \"src/**/*.css\"",
     "lint:typecheck": "vue-tsc --noEmit",
     "lint": "run-p lint:*",

+ 33 - 19
packages/remark-attachment-refs/src/client/components/AttachmentList.tsx

@@ -1,4 +1,4 @@
-import { useCallback, type JSX } from 'react';
+import { type JSX, useCallback } from 'react';
 
 import type { IAttachmentHasId } from '@growi/core';
 import { Attachment, LoadingSpinner } from '@growi/ui/dist/components';
@@ -6,16 +6,15 @@ import { Attachment, LoadingSpinner } from '@growi/ui/dist/components';
 import { ExtractedAttachments } from './ExtractedAttachments';
 import type { RefsContext } from './util/refs-context';
 
-
 import styles from './AttachmentList.module.scss';
 
 const AttachmentLink = Attachment;
 
 type Props = {
-  refsContext: RefsContext
-  isLoading: boolean
-  error?: Error
-  attachments: IAttachmentHasId[]
+  refsContext: RefsContext;
+  isLoading: boolean;
+  error?: Error;
+  attachments: IAttachmentHasId[];
 };
 
 export const AttachmentList = ({
@@ -28,12 +27,15 @@ export const AttachmentList = ({
     return (
       <div className="text-muted">
         <small>
-          <span className="material-symbols-outlined fs-5 me-1" aria-hidden="true">info</span>
-          {
-            refsContext.options?.prefix != null
-              ? `${refsContext.options.prefix} and descendant pages have no attachments`
-              : `${refsContext.pagePath} has no attachments`
-          }
+          <span
+            className="material-symbols-outlined fs-5 me-1"
+            aria-hidden="true"
+          >
+            info
+          </span>
+          {refsContext.options?.prefix != null
+            ? `${refsContext.options.prefix} and descendant pages have no attachments`
+            : `${refsContext.pagePath} has no attachments`}
         </small>
       </div>
     );
@@ -44,7 +46,9 @@ export const AttachmentList = ({
       return (
         <div className="text-muted">
           <LoadingSpinner className="me-1" />
-          <span className="attachment-refs-blink">{refsContext.toString()}</span>
+          <span className="attachment-refs-blink">
+            {refsContext.toString()}
+          </span>
         </div>
       );
     }
@@ -62,13 +66,23 @@ export const AttachmentList = ({
       return renderNoAttachmentsMessage();
     }
 
-    return (refsContext.isExtractImage)
-      ? <ExtractedAttachments attachments={attachments} refsContext={refsContext} />
-      : attachments.map((attachment) => {
-        return <AttachmentLink key={attachment._id} attachment={attachment} inUse={false} />;
-      });
+    return refsContext.isExtractImage ? (
+      <ExtractedAttachments
+        attachments={attachments}
+        refsContext={refsContext}
+      />
+    ) : (
+      attachments.map((attachment) => {
+        return (
+          <AttachmentLink
+            key={attachment._id}
+            attachment={attachment}
+            inUse={false}
+          />
+        );
+      })
+    );
   }, [isLoading, error, attachments, refsContext, renderNoAttachmentsMessage]);
 
   return <div className={styles['attachment-refs']}>{renderContents()}</div>;
-
 };

+ 187 - 176
packages/remark-attachment-refs/src/client/components/ExtractedAttachments.tsx

@@ -7,8 +7,8 @@ import type { Property } from 'csstype';
 import type { RefsContext } from './util/refs-context';
 
 type Props = {
-  attachments: IAttachmentHasId[],
-  refsContext: RefsContext,
+  attachments: IAttachmentHasId[];
+  refsContext: RefsContext;
 };
 
 /**
@@ -16,184 +16,195 @@ type Props = {
  *  2. when 'fileFormat' is not image, render Attachment as an Attachment component
  */
 // TODO https://redmine.weseek.co.jp/issues/121095: implement image carousel modal without using react-images
-export const ExtractedAttachments = React.memo(({
-  attachments,
-  refsContext,
-}: Props): JSX.Element => {
-
-  // const [showCarousel, setShowCarousel] = useState(false);
-  // const [currentIndex, setCurrentIndex] = useState<number | null>(null);
-
-  // const imageClickedHandler = useCallback((index: number) => {
-  //   setShowCarousel(true);
-  //   setCurrentIndex(index);
-  // }, []);
-
-  const getAttachmentsFilteredByFormat = useCallback(() => {
-    return attachments
-      .filter(attachment => attachment.fileFormat.startsWith('image/'));
-  }, [attachments]);
-
-  const getClassesAndStylesForNonGrid = useCallback(() => {
-    const { options } = refsContext;
-
-    const width = options?.width;
-    const height = options?.height;
-    const maxWidth = options?.maxWidth;
-    const maxHeight = options?.maxHeight;
-    const display = options?.display || 'block';
-
-    const containerStyles = {
-      width, height, maxWidth, maxHeight, display,
-    };
-
-    const imageClasses = [];
-    const imageStyles = {
-      width, height, maxWidth, maxHeight,
-    };
-
-    return {
-      containerStyles,
-      imageClasses,
-      imageStyles,
-    };
-  }, [refsContext]);
-
-  const getClassesAndStylesForGrid = useCallback(() => {
-    const { options } = refsContext;
+export const ExtractedAttachments = React.memo(
+  ({ attachments, refsContext }: Props): JSX.Element => {
+    // const [showCarousel, setShowCarousel] = useState(false);
+    // const [currentIndex, setCurrentIndex] = useState<number | null>(null);
+
+    // const imageClickedHandler = useCallback((index: number) => {
+    //   setShowCarousel(true);
+    //   setCurrentIndex(index);
+    // }, []);
+
+    const getAttachmentsFilteredByFormat = useCallback(() => {
+      return attachments.filter((attachment) =>
+        attachment.fileFormat.startsWith('image/'),
+      );
+    }, [attachments]);
+
+    const getClassesAndStylesForNonGrid = useCallback(() => {
+      const { options } = refsContext;
+
+      const width = options?.width;
+      const height = options?.height;
+      const maxWidth = options?.maxWidth;
+      const maxHeight = options?.maxHeight;
+      const display = options?.display || 'block';
+
+      const containerStyles = {
+        width,
+        height,
+        maxWidth,
+        maxHeight,
+        display,
+      };
+
+      const imageClasses = [];
+      const imageStyles = {
+        width,
+        height,
+        maxWidth,
+        maxHeight,
+      };
+
+      return {
+        containerStyles,
+        imageClasses,
+        imageStyles,
+      };
+    }, [refsContext]);
+
+    const getClassesAndStylesForGrid = useCallback(() => {
+      const { options } = refsContext;
+
+      const maxWidth = options?.maxWidth;
+      const maxHeight = options?.maxHeight;
+
+      const containerStyles = {
+        width: refsContext.getOptGridWidth(),
+        height: refsContext.getOptGridHeight(),
+        maxWidth,
+        maxHeight,
+      };
+
+      const imageClasses = ['w-100', 'h-100'];
+      const imageStyles = {
+        objectFit: 'cover' as Property.ObjectFit,
+        maxWidth,
+        maxHeight,
+      };
+
+      return {
+        containerStyles,
+        imageClasses,
+        imageStyles,
+      };
+    }, [refsContext]);
+
+    /**
+     * wrapper method for getClassesAndStylesForGrid/getClassesAndStylesForNonGrid
+     */
+    const getClassesAndStyles = useCallback(() => {
+      const { options } = refsContext;
+
+      return options?.grid != null
+        ? getClassesAndStylesForGrid()
+        : getClassesAndStylesForNonGrid();
+    }, [
+      getClassesAndStylesForGrid,
+      getClassesAndStylesForNonGrid,
+      refsContext,
+    ]);
+
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    const renderExtractedImage = useCallback(
+      (attachment: IAttachmentHasId, index: number) => {
+        const { options } = refsContext;
+
+        // determine alt
+        let alt = refsContext.isSingle ? options?.alt : undefined; // use only when single mode
+        alt = alt || attachment.originalName; //                     use 'originalName' if options.alt is not specified
+
+        // get styles
+        const { containerStyles, imageClasses, imageStyles } =
+          getClassesAndStyles();
+
+        // carousel settings
+        // let onClick;
+        // if (options?.noCarousel == null) {
+        //   // pointer cursor
+        //   Object.assign(containerStyles, { cursor: 'pointer' });
+        //   // set click handler
+        //   onClick = () => {
+        //     imageClickedHandler(index);
+        //   };
+        // }
+
+        return (
+          <div key={attachment._id} style={containerStyles}>
+            <img
+              src={attachment.filePathProxied}
+              alt={alt}
+              className={imageClasses.join(' ')}
+              style={imageStyles}
+            />
+          </div>
+        );
+      },
+      [getClassesAndStyles, refsContext],
+    );
 
-    const maxWidth = options?.maxWidth;
-    const maxHeight = options?.maxHeight;
-
-    const containerStyles = {
-      width: refsContext.getOptGridWidth(),
-      height: refsContext.getOptGridHeight(),
-      maxWidth,
-      maxHeight,
-    };
-
-    const imageClasses = ['w-100', 'h-100'];
-    const imageStyles = {
-      objectFit: 'cover' as Property.ObjectFit,
-      maxWidth,
-      maxHeight,
-    };
-
-    return {
-      containerStyles,
-      imageClasses,
-      imageStyles,
-    };
-  }, [refsContext]);
-
-  /**
-   * wrapper method for getClassesAndStylesForGrid/getClassesAndStylesForNonGrid
-   */
-  const getClassesAndStyles = useCallback(() => {
-    const { options } = refsContext;
+    // const renderCarousel = useCallback(() => {
+    //   const { options } = refsContext;
+    //   const withCarousel = options?.noCarousel == null;
+
+    //   const images = getAttachmentsFilteredByFormat()
+    //     .map((attachment) => {
+    //       return { src: attachment.filePathProxied };
+    //     });
+
+    //   // overwrite react-images modal styles
+    //   const zIndex = 1030; // > grw-navbar
+    //   const modalStyles = {
+    //     blanket: (styleObj) => {
+    //       return Object.assign(styleObj, { zIndex });
+    //     },
+    //     positioner: (styleObj) => {
+    //       return Object.assign(styleObj, { zIndex });
+    //     },
+    //   };
 
-    return (options?.grid != null)
-      ? getClassesAndStylesForGrid()
-      : getClassesAndStylesForNonGrid();
-  }, [getClassesAndStylesForGrid, getClassesAndStylesForNonGrid, refsContext]);
+    //   return (
+    //     <ModalGateway>
+    //       { withCarousel && showCarousel && (
+    //         <Modal styles={modalStyles} onClose={() => { setShowCarousel(false) }}>
+    //           <Carousel views={images} currentIndex={currentIndex} />
+    //         </Modal>
+    //       ) }
+    //     </ModalGateway>
+    //   );
+    // }, [refsContext]);
 
-  // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  const renderExtractedImage = useCallback((attachment: IAttachmentHasId, index: number) => {
     const { options } = refsContext;
-
-    // determine alt
-    let alt = refsContext.isSingle ? options?.alt : undefined; // use only when single mode
-    alt = alt || attachment.originalName; //                     use 'originalName' if options.alt is not specified
-
-    // get styles
-    const {
-      containerStyles, imageClasses, imageStyles,
-    } = getClassesAndStyles();
-
-    // carousel settings
-    // let onClick;
-    // if (options?.noCarousel == null) {
-    //   // pointer cursor
-    //   Object.assign(containerStyles, { cursor: 'pointer' });
-    //   // set click handler
-    //   onClick = () => {
-    //     imageClickedHandler(index);
-    //   };
-    // }
+    const grid = options?.grid;
+    const gridGap = options?.gridGap;
+
+    const styles = {};
+
+    // Grid mode
+    if (grid != null) {
+      const gridTemplateColumns = refsContext.isOptGridColumnEnabled()
+        ? `repeat(${refsContext.getOptGridColumnsNum()}, 1fr)`
+        : `repeat(auto-fill, ${refsContext.getOptGridWidth()})`;
+
+      Object.assign(styles, {
+        display: 'grid',
+        gridTemplateColumns,
+        gridAutoRows: '1fr',
+        gridGap,
+      });
+    }
+
+    const contents = getAttachmentsFilteredByFormat().map((attachment, index) =>
+      renderExtractedImage(attachment, index),
+    );
 
     return (
-      <div
-        key={attachment._id}
-        style={containerStyles}
-      >
-        <img src={attachment.filePathProxied} alt={alt} className={imageClasses.join(' ')} style={imageStyles} />
-      </div>
+      <React.Fragment>
+        <div style={styles}>{contents}</div>
+
+        {/* { renderCarousel() } */}
+      </React.Fragment>
     );
-  }, [getClassesAndStyles, refsContext]);
-
-  // const renderCarousel = useCallback(() => {
-  //   const { options } = refsContext;
-  //   const withCarousel = options?.noCarousel == null;
-
-  //   const images = getAttachmentsFilteredByFormat()
-  //     .map((attachment) => {
-  //       return { src: attachment.filePathProxied };
-  //     });
-
-  //   // overwrite react-images modal styles
-  //   const zIndex = 1030; // > grw-navbar
-  //   const modalStyles = {
-  //     blanket: (styleObj) => {
-  //       return Object.assign(styleObj, { zIndex });
-  //     },
-  //     positioner: (styleObj) => {
-  //       return Object.assign(styleObj, { zIndex });
-  //     },
-  //   };
-
-  //   return (
-  //     <ModalGateway>
-  //       { withCarousel && showCarousel && (
-  //         <Modal styles={modalStyles} onClose={() => { setShowCarousel(false) }}>
-  //           <Carousel views={images} currentIndex={currentIndex} />
-  //         </Modal>
-  //       ) }
-  //     </ModalGateway>
-  //   );
-  // }, [refsContext]);
-
-  const { options } = refsContext;
-  const grid = options?.grid;
-  const gridGap = options?.gridGap;
-
-  const styles = {};
-
-  // Grid mode
-  if (grid != null) {
-
-    const gridTemplateColumns = (refsContext.isOptGridColumnEnabled())
-      ? `repeat(${refsContext.getOptGridColumnsNum()}, 1fr)`
-      : `repeat(auto-fill, ${refsContext.getOptGridWidth()})`;
-
-    Object.assign(styles, {
-      display: 'grid',
-      gridTemplateColumns,
-      gridAutoRows: '1fr',
-      gridGap,
-    });
-
-  }
-
-  const contents = getAttachmentsFilteredByFormat()
-    .map((attachment, index) => renderExtractedImage(attachment, index));
-
-  return (
-    <React.Fragment>
-      <div style={styles}>
-        {contents}
-      </div>
-
-      {/* { renderCarousel() } */}
-    </React.Fragment>
-  );
-});
+  },
+);

+ 9 - 5
packages/remark-attachment-refs/src/client/components/Gallery.tsx

@@ -12,10 +12,14 @@ export const Gallery = React.memo((props: Props): JSX.Element => {
   return <RefsImgSubstance grid={grid} gridGap={gridGap} {...props} />;
 });
 
-export const GalleryImmutable = React.memo((props: Omit<Props, 'isImmutable'>): JSX.Element => {
-  const grid = props.grid || gridDefault;
-  const gridGap = props.gridGap || gridGapDefault;
-  return <RefsImgSubstance grid={grid} gridGap={gridGap} {...props} isImmutable />;
-});
+export const GalleryImmutable = React.memo(
+  (props: Omit<Props, 'isImmutable'>): JSX.Element => {
+    const grid = props.grid || gridDefault;
+    const gridGap = props.gridGap || gridGapDefault;
+    return (
+      <RefsImgSubstance grid={grid} gridGap={gridGap} {...props} isImmutable />
+    );
+  },
+);
 
 Gallery.displayName = 'Gallery';

+ 31 - 28
packages/remark-attachment-refs/src/client/components/Ref.tsx

@@ -5,41 +5,44 @@ import { useSWRxRef } from '../stores/refs';
 import { AttachmentList } from './AttachmentList';
 import { RefsContext } from './util/refs-context';
 
-
 type Props = {
-  fileNameOrId: string,
-  pagePath: string,
-  isImmutable?: boolean,
+  fileNameOrId: string;
+  pagePath: string;
+  isImmutable?: boolean;
 };
 
-const RefSubstance = React.memo(({
-  fileNameOrId,
-  pagePath,
-  isImmutable,
-}: Props): JSX.Element => {
-  const refsContext = useMemo(() => {
-    return new RefsContext('ref', pagePath, { fileNameOrId });
-  }, [fileNameOrId, pagePath]);
-
-  const { data, error, isLoading } = useSWRxRef(pagePath, fileNameOrId, isImmutable);
-  const attachments = data != null ? [data] : [];
-
-  return (
-    <AttachmentList
-      refsContext={refsContext}
-      isLoading={isLoading}
-      error={error}
-      attachments={attachments}
-    />
-  );
-});
+const RefSubstance = React.memo(
+  ({ fileNameOrId, pagePath, isImmutable }: Props): JSX.Element => {
+    const refsContext = useMemo(() => {
+      return new RefsContext('ref', pagePath, { fileNameOrId });
+    }, [fileNameOrId, pagePath]);
+
+    const { data, error, isLoading } = useSWRxRef(
+      pagePath,
+      fileNameOrId,
+      isImmutable,
+    );
+    const attachments = data != null ? [data] : [];
+
+    return (
+      <AttachmentList
+        refsContext={refsContext}
+        isLoading={isLoading}
+        error={error}
+        attachments={attachments}
+      />
+    );
+  },
+);
 
 export const Ref = React.memo((props: Props): JSX.Element => {
   return <RefSubstance {...props} />;
 });
 
-export const RefImmutable = React.memo((props: Omit<Props, 'isImmutable'>): JSX.Element => {
-  return <RefSubstance {...props} isImmutable />;
-});
+export const RefImmutable = React.memo(
+  (props: Omit<Props, 'isImmutable'>): JSX.Element => {
+    return <RefSubstance {...props} isImmutable />;
+  },
+);
 
 Ref.displayName = 'Ref';

+ 54 - 42
packages/remark-attachment-refs/src/client/components/RefImg.tsx

@@ -5,55 +5,67 @@ import { useSWRxRef } from '../stores/refs';
 import { AttachmentList } from './AttachmentList';
 import { RefsContext } from './util/refs-context';
 
-
 type Props = {
-  fileNameOrId: string
-  pagePath: string
-  width?: string
-  height?: string
-  maxWidth?: string
-  maxHeight?: string
-  alt?: string
-
-  isImmutable?: boolean
+  fileNameOrId: string;
+  pagePath: string;
+  width?: string;
+  height?: string;
+  maxWidth?: string;
+  maxHeight?: string;
+  alt?: string;
+
+  isImmutable?: boolean;
 };
 
-const RefImgSubstance = React.memo(({
-  fileNameOrId,
-  pagePath,
-  width,
-  height,
-  maxWidth,
-  maxHeight,
-  alt,
-  isImmutable,
-}: Props): JSX.Element => {
-  const refsContext = useMemo(() => {
-    const options = {
-      fileNameOrId, width, height, maxWidth, maxHeight, alt,
-    };
-    return new RefsContext('refimg', pagePath, options);
-  }, [fileNameOrId, pagePath, width, height, maxWidth, maxHeight, alt]);
-
-  const { data, error, isLoading } = useSWRxRef(pagePath, fileNameOrId, isImmutable);
-  const attachments = data != null ? [data] : [];
-
-  return (
-    <AttachmentList
-      refsContext={refsContext}
-      isLoading={isLoading}
-      error={error}
-      attachments={attachments}
-    />
-  );
-});
+const RefImgSubstance = React.memo(
+  ({
+    fileNameOrId,
+    pagePath,
+    width,
+    height,
+    maxWidth,
+    maxHeight,
+    alt,
+    isImmutable,
+  }: Props): JSX.Element => {
+    const refsContext = useMemo(() => {
+      const options = {
+        fileNameOrId,
+        width,
+        height,
+        maxWidth,
+        maxHeight,
+        alt,
+      };
+      return new RefsContext('refimg', pagePath, options);
+    }, [fileNameOrId, pagePath, width, height, maxWidth, maxHeight, alt]);
+
+    const { data, error, isLoading } = useSWRxRef(
+      pagePath,
+      fileNameOrId,
+      isImmutable,
+    );
+    const attachments = data != null ? [data] : [];
+
+    return (
+      <AttachmentList
+        refsContext={refsContext}
+        isLoading={isLoading}
+        error={error}
+        attachments={attachments}
+      />
+    );
+  },
+);
 
 export const RefImg = React.memo((props: Props): JSX.Element => {
   return <RefImgSubstance {...props} />;
 });
 
-export const RefImgImmutable = React.memo((props: Omit<Props, 'isImmutable'>): JSX.Element => {
-  return <RefImgSubstance {...props} isImmutable />;
-});
+export const RefImgImmutable = React.memo(
+  (props: Omit<Props, 'isImmutable'>): JSX.Element => {
+    return <RefImgSubstance {...props} isImmutable />;
+  },
+);
 
 RefImg.displayName = 'RefImg';

Некоторые файлы не были показаны из-за большого количества измененных файлов