Ver código fonte

Merge branch 'master' into fix/unhandled-popover-error

Yuki Takei 2 dias atrás
pai
commit
c8fecb30b1
31 arquivos alterados com 275 adições e 236 exclusões
  1. 66 0
      .claude/rules/lsp.md
  2. 1 5
      .claude/settings.json
  3. 5 0
      .devcontainer/app/postCreateCommand.sh
  4. 5 0
      .devcontainer/pdf-converter/postCreateCommand.sh
  5. 0 23
      .vscode/mcp.json
  6. 2 2
      apps/app/src/client/components/Admin/G2GDataTransferExportForm.tsx
  7. 5 8
      apps/app/src/client/components/LoginForm/LoginForm.tsx
  8. 2 2
      apps/app/src/client/components/PageRenameModal/PageRenameModal.tsx
  9. 39 14
      apps/app/src/client/util/watch-rendering-and-rescroll.spec.tsx
  10. 3 1
      apps/app/src/client/util/watch-rendering-and-rescroll.ts
  11. 1 1
      apps/app/src/components/utils/use-lazy-loader.ts
  12. 8 7
      apps/app/src/features/openai/server/services/client-delegator/get-client.ts
  13. 1 4
      apps/app/src/pages/admin/_shared/layout.tsx
  14. 1 3
      apps/app/src/server/routes/apiv3/page-listing.ts
  15. 3 9
      apps/app/src/server/routes/apiv3/share-links.js
  16. 16 13
      apps/app/src/server/service/file-uploader/multipart-uploader.spec.ts
  17. 10 9
      apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/get-client.ts
  18. 9 10
      apps/app/src/server/util/is-simple-request.spec.ts
  19. 1 0
      apps/app/src/server/util/safe-path-utils.ts
  20. 2 3
      apps/app/src/services/renderer/recommended-whitelist.ts
  21. 3 3
      apps/app/src/states/ui/sidebar/sidebar.ts
  22. 6 14
      biome.json
  23. 1 1
      package.json
  24. 1 8
      packages/core/src/models/serializers/user-serializer.ts
  25. 9 9
      packages/core/src/utils/objectid-utils.spec.ts
  26. 6 7
      packages/core/src/utils/page-path-utils/generate-children-regexp.spec.ts
  27. 29 41
      packages/core/src/utils/page-path-utils/index.spec.ts
  28. 1 0
      packages/remark-growi-directive/src/index.js
  29. 1 1
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-attributes.js
  30. 1 1
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-label.js
  31. 37 37
      pnpm-lock.yaml

+ 66 - 0
.claude/rules/lsp.md

@@ -0,0 +1,66 @@
+# LSP Usage
+
+The `LSP` tool provides TypeScript-aware code intelligence. Prefer it over `grep`/`find` for symbol-level queries.
+
+## Tool availability (read before concluding LSP is unavailable)
+
+The way `LSP` is exposed differs between the main session and sub-agents. Check which context you are in before concluding it is unavailable.
+
+**Main session (this file is in your system prompt):**
+`LSP` is registered as a **deferred tool** — its schema is not loaded at session start, so it will NOT appear in the initial top-level tool list. It instead shows up by name inside the session-start `<system-reminder>` listing deferred tools.
+
+Do not conclude LSP is unavailable just because it isn't in the initial tool list. To use it:
+
+1. Confirm `LSP` appears in the deferred-tool list in the session-start system-reminder.
+2. Load its schema with `ToolSearch` using `query: "select:LSP"`.
+3. After that, call `LSP` like any other tool.
+
+Only if `LSP` is missing from the deferred-tool list AND `ToolSearch` with `select:LSP` returns no match should you treat LSP as disabled and fall back to `grep`.
+
+**Sub-agents (Explore, general-purpose, etc.):**
+`LSP` is provided directly in the initial tool list — no `ToolSearch` step needed. `ToolSearch` itself is not available in sub-agents. Just call `LSP` as a normal tool.
+
+Note: `.claude/rules/` files are NOT injected into sub-agent system prompts. A sub-agent will not know the guidance in this file unless the parent includes it in the `Agent` prompt. When delegating symbol-level research (definition lookup, caller search, type inspection) to a sub-agent, restate the key rules inline — at minimum: "prefer LSP over grep for TypeScript symbol queries; use `incomingCalls` for callers, `goToDefinition` for definitions".
+
+## Auto-start behavior
+
+The `typescript-language-server` starts automatically when the LSP tool is first invoked — no manual startup or health check is needed. If the server isn't installed, the tool returns an error; in that case fall back to `grep`.
+
+In the devcontainer, `typescript-language-server` is pre-installed globally via `postCreateCommand.sh`. It auto-detects and uses the workspace's `node_modules/typescript` at runtime.
+
+## When to use LSP (not grep)
+
+| Task | Preferred LSP operation |
+|------|------------------------|
+| Find where a function/class/type is defined | `goToDefinition` |
+| Find all call sites **including imports** | `findReferences` (see caveat below) |
+| Find which functions call a given function | `incomingCalls` ← prefer this over `findReferences` for callers |
+| Check a variable's type or JSDoc | `hover` |
+| List all exports in a file | `documentSymbol` |
+| Find what implements an interface | `goToImplementation` |
+| Trace what a function calls | `outgoingCalls` |
+
+## Decision rule
+
+- **Use LSP** when the query is about a *symbol* (function, class, type, variable) — LSP understands TypeScript semantics and won't false-match string occurrences or comments.
+- **Use grep** when searching for string literals, comments, config values, or when LSP returns no results (e.g., generated code, `.js` files without types).
+
+## `findReferences` — lazy-loading caveat
+
+TypeScript LSP loads files **on demand**. On a cold server (first query after devcontainer start), calling `findReferences` from the *definition file* may return only the definition itself because consumer files haven't been loaded yet.
+
+**Mitigation strategies (in order of preference):**
+
+1. **Prefer `incomingCalls`** over `findReferences` when you want callers. It correctly resolves cross-file call sites even on a cold server.
+2. If you need `findReferences` with full results, call it from a **known call site** (not the definition). After any file in the consumer chain is queried, the server loads it and subsequent `findReferences` calls return complete results.
+3. As a last resort, run a `hover` on an import statement in the consumer file first to warm up that file, then retry `findReferences` from the definition.
+
+## Required: line + character
+
+LSP operations require `line` and `character` (both 1-based). Read the file first to identify the exact position of the symbol, then call LSP.
+
+```
+# Example: symbol starts at col 14 on line 85
+export const useCollaborativeEditorMode = (
+             ^--- character 14
+```

+ 1 - 5
.claude/settings.json

@@ -23,9 +23,7 @@
       "Bash(gh pr diff *)",
       "Bash(ls *)",
       "WebFetch(domain:github.com)",
-      "mcp__context7__*",
       "mcp__plugin_context7_*",
-      "mcp__github__*",
       "WebSearch",
       "WebFetch"
     ]
@@ -57,8 +55,6 @@
   },
   "enabledPlugins": {
     "context7@claude-plugins-official": true,
-    "github@claude-plugins-official": true,
-    "typescript-lsp@claude-plugins-official": true,
-    "playwright@claude-plugins-official": true
+    "typescript-lsp@claude-plugins-official": true
   }
 }

+ 5 - 0
.devcontainer/app/postCreateCommand.sh

@@ -25,6 +25,11 @@ pnpm config set store-dir /workspace/.pnpm-store
 # Install turbo
 pnpm install turbo --global
 
+# Install typescript-language-server for Claude Code LSP plugin
+# Use `npm -g` (not `pnpm --global`) so the binary lands in nvm's node bin, which is on the default PATH.
+# pnpm's global bin requires PNPM_HOME from ~/.bashrc, which the Claude Code extension's shell doesn't source.
+npm install -g typescript-language-server typescript
+
 # Install dependencies
 turbo run bootstrap
 

+ 5 - 0
.devcontainer/pdf-converter/postCreateCommand.sh

@@ -20,6 +20,11 @@ pnpm i -g pnpm
 # Install turbo
 pnpm install turbo --global
 
+# Install typescript-language-server for Claude Code LSP plugin
+# Use `npm -g` (not `pnpm --global`) so the binary lands in nvm's node bin, which is on the default PATH.
+# pnpm's global bin requires PNPM_HOME from ~/.bashrc, which the Claude Code extension's shell doesn't source.
+npm install -g typescript-language-server typescript
+
 # Install dependencies
 turbo run bootstrap
 

+ 0 - 23
.vscode/mcp.json

@@ -1,23 +0,0 @@
-{
-  "servers": {
-    "context7": {
-      "type": "http",
-      "url": "https://mcp.context7.com/mcp"
-    },
-    "serena": {
-      "type": "stdio",
-      "command": "uvx",
-      "args": [
-        "--from",
-        "git+https://github.com/oraios/serena",
-        "serena",
-        "start-mcp-server",
-        "--context",
-        "ide",
-        "--project",
-        ".",
-        "--enable-web-dashboard=false"
-      ]
-    }
-  }
-}

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

@@ -110,8 +110,8 @@ const WarnForGroups = ({ errors }: WarnForGroupsProps): JSX.Element => {
   return (
     <div className="alert alert-warning">
       <ul>
-        {errors.map((error, index) => {
-          return <li key={`${error.message}-${index}`}>{error.message}</li>;
+        {errors.map((error) => {
+          return <li key={error.message}>{error.message}</li>;
         })}
       </ul>
     </div>

+ 5 - 8
apps/app/src/client/components/LoginForm/LoginForm.tsx

@@ -147,10 +147,10 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       if (errors == null || errors.length === 0) return <></>;
       return (
         <div className="alert alert-danger">
-          {errors.map((err, index) => {
+          {errors.map((err) => {
             return (
               <small
-                key={`${err.code}-${index}`}
+                key={err.code}
                 // biome-ignore lint/security/noDangerouslySetInnerHtml: rendered HTML from translations
                 dangerouslySetInnerHTML={{
                   __html: tWithOpt(err.message, err.args),
@@ -171,10 +171,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       return (
         <ul className="alert alert-danger">
           {errors.map((err, index) => (
-            <small
-              key={`${err.message}-${index}`}
-              className={index > 0 ? 'mt-1' : ''}
-            >
+            <small key={err.message} className={index > 0 ? 'mt-1' : ''}>
               {tWithOpt(err.message, err.args)}
             </small>
           ))}
@@ -394,8 +391,8 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
         {registerErrors != null && registerErrors.length > 0 && (
           <p className="alert alert-danger">
-            {registerErrors.map((err, index) => (
-              <span key={`${err.message}-${index}`}>
+            {registerErrors.map((err) => (
+              <span key={err.message}>
                 {tWithOpt(err.message, err.args)}
                 <br />
               </span>

+ 2 - 2
apps/app/src/client/components/PageRenameModal/PageRenameModal.tsx

@@ -57,7 +57,7 @@ const PageRenameModalSubstance: React.FC = () => {
 
   const [errs, setErrs] = useState(null);
 
-  const [subordinatedPages, setSubordinatedPages] = useState([]);
+  const [_subordinatedPages, setSubordinatedPages] = useState([]);
   const [existingPaths, setExistingPaths] = useState<string[]>([]);
   const [isRenameRecursively, setIsRenameRecursively] = useState(true);
   const [isRenameRedirect, setIsRenameRedirect] = useState(false);
@@ -201,7 +201,7 @@ const PageRenameModalSubstance: React.FC = () => {
     };
 
     return debounce(1000, checkIsPagePathRenameable);
-  }, [isUsersHomepage]);
+  }, []);
 
   useEffect(() => {
     if (isOpened && page != null && pageNameInput !== page.data.path) {

+ 39 - 14
apps/app/src/client/util/watch-rendering-and-rescroll.spec.tsx

@@ -1,8 +1,24 @@
+import { setImmediate as realSetImmediate } from 'node:timers/promises';
 import { GROWI_IS_CONTENT_RENDERING_ATTR } from '@growi/core/dist/consts';
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 
 import { watchRenderingAndReScroll } from './watch-rendering-and-rescroll';
 
+// happy-dom captures the real setTimeout at module load, before vitest's
+// fake timers are installed. Its MutationObserver callbacks therefore fire
+// on the REAL event loop, not on the fake timer clock. Yielding to the real
+// event loop (via node:timers/promises) flushes them.
+// Yield twice because happy-dom batches zero-delay timeouts and dispatch
+// across two real-timer ticks (the batcher + the listener's own timer).
+// NOTE: happy-dom v15 stores the MO listener callback in a `WeakRef`
+// (see MutationObserverListener.cjs), so GC between `observe()` and the
+// first mutation can silently drop delivery. Tests that assert MO-driven
+// behavior should rely on retry to absorb that rare collection window.
+const flushMutationObservers = async () => {
+  await realSetImmediate();
+  await realSetImmediate();
+};
+
 describe('watchRenderingAndReScroll', () => {
   let container: HTMLDivElement;
   let scrollToTarget: ReturnType<typeof vi.fn>;
@@ -56,7 +72,7 @@ describe('watchRenderingAndReScroll', () => {
     // Trigger a DOM mutation mid-timer
     const child = document.createElement('span');
     container.appendChild(child);
-    await vi.advanceTimersByTimeAsync(0);
+    await flushMutationObservers();
 
     // The timer should NOT have been reset — 2 more seconds should fire it
     vi.advanceTimersByTime(2000);
@@ -65,7 +81,10 @@ describe('watchRenderingAndReScroll', () => {
     cleanup();
   });
 
-  it('should detect rendering elements added after initial check via observer', async () => {
+  // Retry absorbs rare happy-dom MO WeakRef GC drops (see file-top note).
+  it('should detect rendering elements added after initial check via observer', {
+    retry: 3,
+  }, async () => {
     const cleanup = watchRenderingAndReScroll(container, scrollToTarget);
 
     vi.advanceTimersByTime(3000);
@@ -76,10 +95,9 @@ describe('watchRenderingAndReScroll', () => {
     renderingEl.setAttribute(GROWI_IS_CONTENT_RENDERING_ATTR, 'true');
     container.appendChild(renderingEl);
 
-    // Flush microtasks so MutationObserver callback fires
-    await vi.advanceTimersByTimeAsync(0);
+    // Flush MO so it schedules the poll timer
+    await flushMutationObservers();
 
-    // Timer should be scheduled — fires after 5s
     await vi.advanceTimersByTimeAsync(5000);
     expect(scrollToTarget).toHaveBeenCalledTimes(1);
 
@@ -154,7 +172,10 @@ describe('watchRenderingAndReScroll', () => {
     expect(scrollToTarget).not.toHaveBeenCalled();
   });
 
-  it('should not schedule further re-scrolls after rendering elements complete', async () => {
+  // Retry absorbs rare happy-dom MO WeakRef GC drops (see file-top note).
+  it('should perform a final re-scroll when rendering completes after the first poll', {
+    retry: 3,
+  }, async () => {
     const renderingEl = document.createElement('div');
     renderingEl.setAttribute(GROWI_IS_CONTENT_RENDERING_ATTR, 'true');
     container.appendChild(renderingEl);
@@ -165,13 +186,16 @@ describe('watchRenderingAndReScroll', () => {
     vi.advanceTimersByTime(5000);
     expect(scrollToTarget).toHaveBeenCalledTimes(1);
 
-    // Rendering completes — attribute toggled to false
+    // Rendering completes — attribute toggled to false. MO observes the
+    // transition and triggers a final re-scroll to compensate for the
+    // trailing layout shift.
     renderingEl.setAttribute(GROWI_IS_CONTENT_RENDERING_ATTR, 'false');
-    await vi.advanceTimersByTimeAsync(0);
+    await flushMutationObservers();
+    expect(scrollToTarget).toHaveBeenCalledTimes(2);
 
-    // No further re-scrolls should be scheduled
+    // No further scrolls afterward — the MO cleared the next poll timer.
     vi.advanceTimersByTime(10000);
-    expect(scrollToTarget).toHaveBeenCalledTimes(1);
+    expect(scrollToTarget).toHaveBeenCalledTimes(2);
 
     cleanup();
   });
@@ -183,12 +207,13 @@ describe('watchRenderingAndReScroll', () => {
 
     const cleanup = watchRenderingAndReScroll(container, scrollToTarget);
 
-    // Rendering completes before the first poll timer fires
+    // Rendering completes before the first poll timer fires (no async flush,
+    // so the MO does not deliver before the timer).
     renderingEl.setAttribute(GROWI_IS_CONTENT_RENDERING_ATTR, 'false');
 
-    // Poll timer fires at 5s — detects no rendering elements.
-    // wasRendering is reset in the timer callback BEFORE scrollToTarget so that
-    // the subsequent checkAndSchedule call does not trigger a redundant extra scroll.
+    // wasRendering is reset in the timer callback BEFORE scrollToTarget so
+    // the subsequent checkAndSchedule call does not trigger a redundant
+    // extra scroll.
     vi.advanceTimersByTime(5000);
     expect(scrollToTarget).toHaveBeenCalledTimes(1);
 

+ 3 - 1
apps/app/src/client/util/watch-rendering-and-rescroll.ts

@@ -8,7 +8,9 @@ export const WATCH_TIMEOUT_MS = 10000;
 
 /**
  * Watch for elements with in-progress rendering status in the container.
- * Periodically calls scrollToTarget while rendering elements remain.
+ * Periodically calls scrollToTarget while rendering elements remain, and
+ * performs a final re-scroll when the last rendering element completes
+ * to compensate for the trailing layout shift (Requirements 3.1–3.3).
  * Returns a cleanup function that stops observation and clears timers.
  */
 export const watchRenderingAndReScroll = (

+ 1 - 1
apps/app/src/components/utils/use-lazy-loader.ts

@@ -63,7 +63,7 @@ export const useLazyLoader = <T extends Record<string, unknown>>(
     null,
   );
 
-  const memoizedImportFn = useCallback(importFn, []);
+  const memoizedImportFn = useCallback(importFn, [importFn]);
 
   useEffect(() => {
     if (isActive && !Component) {

+ 8 - 7
apps/app/src/features/openai/server/services/client-delegator/get-client.ts

@@ -8,13 +8,14 @@ type GetDelegatorOptions = {
 };
 
 type IsAny<T> = 'dummy' extends T & 'dummy' ? true : false;
-type Delegator<Opts extends GetDelegatorOptions> = IsAny<Opts> extends true
-  ? IOpenaiClientDelegator
-  : Opts extends { openaiServiceType: 'openai' }
-    ? OpenaiClientDelegator
-    : Opts extends { openaiServiceType: 'azure-openai' }
-      ? AzureOpenaiClientDelegator
-      : IOpenaiClientDelegator;
+type Delegator<Opts extends GetDelegatorOptions> =
+  IsAny<Opts> extends true
+    ? IOpenaiClientDelegator
+    : Opts extends { openaiServiceType: 'openai' }
+      ? OpenaiClientDelegator
+      : Opts extends { openaiServiceType: 'azure-openai' }
+        ? AzureOpenaiClientDelegator
+        : IOpenaiClientDelegator;
 
 // biome-ignore lint/suspicious/noImplicitAnyLet: ignore
 let instance;

+ 1 - 4
apps/app/src/pages/admin/_shared/layout.tsx

@@ -28,10 +28,7 @@ export function createAdminPageLayout<P extends AdminCommonProps>(
           : options.title;
       const title = useCustomTitle(rawTitle);
 
-      const factories = useMemo(
-        () => options.containerFactories ?? [],
-        [options.containerFactories],
-      );
+      const factories = useMemo(() => options.containerFactories ?? [], []);
       const containers = useUnstatedContainers(factories);
 
       return (

+ 1 - 3
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -45,9 +45,7 @@ const validator = {
   ),
   pageIdsOrPathRequired: [
     // type check independent of existence check
-    query('pageIds')
-      .isArray()
-      .optional(),
+    query('pageIds').isArray().optional(),
     query('path').isString().optional(),
     // existence check
     oneOf(

+ 3 - 9
apps/app/src/server/routes/apiv3/share-links.js

@@ -111,9 +111,7 @@ module.exports = (crowi) => {
 
   validator.getShareLinks = [
     // validate the page id is MongoId
-    query('relatedPage')
-      .isMongoId()
-      .withMessage('Page Id is required'),
+    query('relatedPage').isMongoId().withMessage('Page Id is required'),
   ];
 
   /**
@@ -178,9 +176,7 @@ module.exports = (crowi) => {
 
   validator.shareLinkStatus = [
     // validate the page id is MongoId
-    body('relatedPage')
-      .isMongoId()
-      .withMessage('Page Id is required'),
+    body('relatedPage').isMongoId().withMessage('Page Id is required'),
     // validate expireation date is not empty, is not before today and is date.
     body('expiredAt')
       .if((value) => value != null)
@@ -268,9 +264,7 @@ module.exports = (crowi) => {
 
   validator.deleteShareLinks = [
     // validate the page id is MongoId
-    query('relatedPage')
-      .isMongoId()
-      .withMessage('Page Id is required'),
+    query('relatedPage').isMongoId().withMessage('Page Id is required'),
   ];
 
   /**

+ 16 - 13
apps/app/src/server/service/file-uploader/multipart-uploader.spec.ts

@@ -112,20 +112,23 @@ describe('MultipartUploader', () => {
         },
       ];
 
-      describe.each(cases)(
-        'When current status is $current and desired status is $desired',
-        ({ current, desired, errorMessage }) => {
-          beforeEach(() => {
-            uploader.setCurrentStatus(current);
-          });
+      describe.each(
+        cases,
+      )('When current status is $current and desired status is $desired', ({
+        current,
+        desired,
+        errorMessage,
+      }) => {
+        beforeEach(() => {
+          uploader.setCurrentStatus(current);
+        });
 
-          it(`should throw expected error: "${errorMessage}"`, () => {
-            expect(() => uploader.testValidateUploadStatus(desired)).toThrow(
-              errorMessage,
-            );
-          });
-        },
-      );
+        it(`should throw expected error: "${errorMessage}"`, () => {
+          expect(() => uploader.testValidateUploadStatus(desired)).toThrow(
+            errorMessage,
+          );
+        });
+      });
     });
   });
 });

+ 10 - 9
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/get-client.ts

@@ -25,15 +25,16 @@ type GetDelegatorOptions =
     };
 
 type IsAny<T> = 'dummy' extends T & 'dummy' ? true : false;
-type Delegator<Opts extends GetDelegatorOptions> = IsAny<Opts> extends true
-  ? ElasticsearchClientDelegator
-  : Opts extends { version: 7 }
-    ? ES7ClientDelegator
-    : Opts extends { version: 8 }
-      ? ES8ClientDelegator
-      : Opts extends { version: 9 }
-        ? ES9ClientDelegator
-        : ElasticsearchClientDelegator;
+type Delegator<Opts extends GetDelegatorOptions> =
+  IsAny<Opts> extends true
+    ? ElasticsearchClientDelegator
+    : Opts extends { version: 7 }
+      ? ES7ClientDelegator
+      : Opts extends { version: 8 }
+        ? ES8ClientDelegator
+        : Opts extends { version: 9 }
+          ? ES9ClientDelegator
+          : ElasticsearchClientDelegator;
 
 let instance: ElasticsearchClientDelegator | null = null;
 export const getClient = async <Opts extends GetDelegatorOptions>(

+ 9 - 10
apps/app/src/server/util/is-simple-request.spec.ts

@@ -88,16 +88,15 @@ describe('isSimpleRequest', () => {
         'X-Requested-With',
         'X-CSRF-Token',
       ];
-      it.each(unsafeHeaders)(
-        'returns false for unsafe header: %s',
-        (headerName) => {
-          const reqMock = mock<Request>({
-            method: 'POST',
-            headers: { [headerName]: 'test-value' },
-          });
-          expect(isSimpleRequest(reqMock)).toBe(false);
-        },
-      );
+      it.each(
+        unsafeHeaders,
+      )('returns false for unsafe header: %s', (headerName) => {
+        const reqMock = mock<Request>({
+          method: 'POST',
+          headers: { [headerName]: 'test-value' },
+        });
+        expect(isSimpleRequest(reqMock)).toBe(false);
+      });
       // combination
       it('returns false when safe and unsafe headers are mixed', () => {
         const reqMock = mock<Request>();

+ 1 - 0
apps/app/src/server/util/safe-path-utils.ts

@@ -1,5 +1,6 @@
 import { AllLang } from '@growi/core';
 import path from 'pathe';
+
 export { AllLang as SUPPORTED_LOCALES };
 
 /**

+ 2 - 3
apps/app/src/services/renderer/recommended-whitelist.ts

@@ -3,9 +3,8 @@ import deepmerge from 'ts-deepmerge';
 
 type Attributes = typeof defaultSchema.attributes;
 
-type ExtractPropertyDefinition<T> = T extends Record<string, (infer U)[]>
-  ? U
-  : never;
+type ExtractPropertyDefinition<T> =
+  T extends Record<string, (infer U)[]> ? U : never;
 
 type PropertyDefinition = ExtractPropertyDefinition<NonNullable<Attributes>>;
 

+ 3 - 3
apps/app/src/states/ui/sidebar/sidebar.ts

@@ -66,10 +66,10 @@ export const useCurrentProductNavWidth = () => {
 
 // Export base atoms for SSR hydration
 export {
-  preferCollapsedModeAtom,
-  isCollapsedContentsOpenedAtom,
-  currentSidebarContentsAtom,
   currentProductNavWidthAtom,
+  currentSidebarContentsAtom,
+  isCollapsedContentsOpenedAtom,
+  preferCollapsedModeAtom,
 };
 
 const sidebarModeAtom = atom((get) => {

+ 6 - 14
biome.json

@@ -1,30 +1,22 @@
 {
   "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
+  "vcs": {
+    "enabled": true,
+    "clientKind": "git",
+    "useIgnoreFile": true
+  },
   "files": {
     "includes": [
       "**",
-      "!**/.pnpm-store",
       "!**/.terraform",
-      "!**/coverage",
-      "!**/dist",
-      "!**/.next",
-      "!**/node_modules",
-      "!**/vite.*.ts.timestamp-*",
       "!**/*.grit",
       "!**/turbo.json",
       "!**/.devcontainer",
       "!**/.stylelintrc.json",
       "!**/package.json",
-      "!**/*.vendor-styles.prebuilt.*",
-      "!.turbo",
       "!.vscode",
       "!.claude",
       "!tsconfig.base.json",
-      "!apps/app/src/styles/prebuilt",
-      "!apps/app/next-env.d.ts",
-      "!apps/app/tmp",
-      "!apps/pdf-converter/specs",
-      "!apps/slackbot-proxy/src/public/bootstrap",
       "!packages/pdf-converter-client/src/index.ts",
       "!packages/pdf-converter-client/specs"
     ]
@@ -85,7 +77,7 @@
       "correctness": {
         "useUniqueElementIds": "warn"
       },
-      "nursery": {
+      "complexity": {
         "useMaxParams": "warn"
       }
     }

+ 1 - 1
package.json

@@ -41,7 +41,7 @@
   },
   "// comments for devDependencies": {},
   "devDependencies": {
-    "@biomejs/biome": "^2.2.6",
+    "@biomejs/biome": "^2.4.12",
     "@changesets/changelog-github": "^0.5.0",
     "@changesets/cli": "^2.27.3",
     "@faker-js/faker": "^9.0.1",

+ 1 - 8
packages/core/src/models/serializers/user-serializer.ts

@@ -13,14 +13,7 @@ export const omitInsecureAttributes = <U extends IUser>(
 ): IUserSerializedSecurely<U> => {
   const leanDoc = user instanceof Document ? user.toObject<U>() : user;
 
-  const {
-    // biome-ignore lint/correctness/noUnusedVariables: ignore
-    password,
-    // biome-ignore lint/correctness/noUnusedVariables: ignore
-    apiToken,
-    email,
-    ...rest
-  } = leanDoc;
+  const { password, apiToken, email, ...rest } = leanDoc;
 
   const secureUser: IUserSerializedSecurely<U> = rest;
 

+ 9 - 9
packages/core/src/utils/objectid-utils.spec.ts

@@ -4,16 +4,16 @@ import { isValidObjectId } from './objectid-utils';
 
 describe('isValidObjectId', () => {
   describe.concurrent.each`
-    arg                                           | expected
-    ${undefined}                                  | ${false}
-    ${null}                                       | ${false}
-    ${'geeks'}                                    | ${false}
-    ${'toptoptoptop'}                             | ${false}
-    ${'geeksfogeeks'}                             | ${false}
-    ${'594ced02ed345b2b049222c5'}                 | ${true}
-    ${new ObjectId('594ced02ed345b2b049222c5')}   | ${true}
+    arg                                         | expected
+    ${undefined}                                | ${false}
+    ${null}                                     | ${false}
+    ${'geeks'}                                  | ${false}
+    ${'toptoptoptop'}                           | ${false}
+    ${'geeksfogeeks'}                           | ${false}
+    ${'594ced02ed345b2b049222c5'}               | ${true}
+    ${new ObjectId('594ced02ed345b2b049222c5')} | ${true}
   `('should return $expected', ({ arg, expected }) => {
-    test(`when the argument is '${arg}'`, async () => {
+    test(`when the argument is '${arg}'`, () => {
       // when:
       const result = isValidObjectId(arg);
 

+ 6 - 7
packages/core/src/utils/page-path-utils/generate-children-regexp.spec.ts

@@ -60,12 +60,11 @@ describe('generateChildrenRegExp', () => {
       expect(validPath).toMatch(result);
     });
 
-    test.each(invalidPaths)(
-      'should not match invalid path: %s',
-      (invalidPath) => {
-        const result = generateChildrenRegExp(path);
-        expect(invalidPath).not.toMatch(result);
-      },
-    );
+    test.each(
+      invalidPaths,
+    )('should not match invalid path: %s', (invalidPath) => {
+      const result = generateChildrenRegExp(path);
+      expect(invalidPath).not.toMatch(result);
+    });
   });
 });

+ 29 - 41
packages/core/src/utils/page-path-utils/index.spec.ts

@@ -56,17 +56,14 @@ describe.concurrent('convertToNewAffiliationPath test', () => {
     expect(result === 'parent/child').toBe(false);
   });
 
-  test.concurrent(
-    'Parent and Child path names are switched unexpectedly',
-    () => {
-      const result = convertToNewAffiliationPath(
-        'parent/',
-        'parent4/',
-        'parent/child',
-      );
-      expect(result === 'child/parent4').toBe(false);
-    },
-  );
+  test.concurrent('Parent and Child path names are switched unexpectedly', () => {
+    const result = convertToNewAffiliationPath(
+      'parent/',
+      'parent4/',
+      'parent/child',
+    );
+    expect(result === 'child/parent4').toBe(false);
+  });
 });
 
 describe.concurrent('isCreatablePage test', () => {
@@ -149,41 +146,32 @@ describe.concurrent('isCreatablePage test', () => {
       );
     });
 
-    test.concurrent(
-      'Should omit when some paths are at duplicated area',
-      () => {
-        const paths = ['/A', '/A/A', '/A/B/A', '/B', '/B/A', '/AA'];
-        const expectedPaths = ['/A', '/B', '/AA'];
+    test.concurrent('Should omit when some paths are at duplicated area', () => {
+      const paths = ['/A', '/A/A', '/A/B/A', '/B', '/B/A', '/AA'];
+      const expectedPaths = ['/A', '/B', '/AA'];
 
-        expect(omitDuplicateAreaPathFromPaths(paths)).toStrictEqual(
-          expectedPaths,
-        );
-      },
-    );
+      expect(omitDuplicateAreaPathFromPaths(paths)).toStrictEqual(
+        expectedPaths,
+      );
+    });
 
-    test.concurrent(
-      'Should omit when some long paths are at duplicated area',
-      () => {
-        const paths = ['/A/B/C', '/A/B/C/D', '/A/B/C/D/E'];
-        const expectedPaths = ['/A/B/C'];
+    test.concurrent('Should omit when some long paths are at duplicated area', () => {
+      const paths = ['/A/B/C', '/A/B/C/D', '/A/B/C/D/E'];
+      const expectedPaths = ['/A/B/C'];
 
-        expect(omitDuplicateAreaPathFromPaths(paths)).toStrictEqual(
-          expectedPaths,
-        );
-      },
-    );
+      expect(omitDuplicateAreaPathFromPaths(paths)).toStrictEqual(
+        expectedPaths,
+      );
+    });
 
-    test.concurrent(
-      'Should omit when some long paths are at duplicated area [case insensitivity]',
-      () => {
-        const paths = ['/a/B/C', '/A/b/C/D', '/A/B/c/D/E'];
-        const expectedPaths = ['/a/B/C'];
+    test.concurrent('Should omit when some long paths are at duplicated area [case insensitivity]', () => {
+      const paths = ['/a/B/C', '/A/b/C/D', '/A/B/c/D/E'];
+      const expectedPaths = ['/a/B/C'];
 
-        expect(omitDuplicateAreaPathFromPaths(paths)).toStrictEqual(
-          expectedPaths,
-        );
-      },
-    );
+      expect(omitDuplicateAreaPathFromPaths(paths)).toStrictEqual(
+        expectedPaths,
+      );
+    });
   });
 
   describe.concurrent('Test getUsernameByPath', () => {

+ 1 - 0
packages/remark-growi-directive/src/index.js

@@ -8,4 +8,5 @@ export {
   TextGrowiPluginDirectiveData,
 } from './mdast-util-growi-directive';
 
+// biome-ignore lint/style/noDefaultExport: remark plugins are conventionally consumed as default imports
 export default remarkGrowiDirectivePlugin;

+ 1 - 1
packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-attributes.js

@@ -34,7 +34,7 @@ import {
  * @param {string} attributeValueData
  * @param {boolean} [disallowEol=false]
  */
-// biome-ignore lint/nursery/useMaxParams: This module is transplanted from micromark and we want to keep the signature same.
+// biome-ignore lint/complexity/useMaxParams: This module is transplanted from micromark and we want to keep the signature same.
 export function factoryAttributes(
   effects,
   ok,

+ 1 - 1
packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-label.js

@@ -22,7 +22,7 @@ import { ok as assert } from 'uvu/assert';
  * @param {string} stringType
  * @param {boolean} [disallowEol=false]
  */
-// biome-ignore lint/nursery/useMaxParams: This module is transplanted from micromark and we want to keep the signature same.
+// biome-ignore lint/complexity/useMaxParams: This module is transplanted from micromark and we want to keep the signature same.
 export function factoryLabel(
   effects,
   ok,

+ 37 - 37
pnpm-lock.yaml

@@ -22,8 +22,8 @@ importers:
   .:
     devDependencies:
       '@biomejs/biome':
-        specifier: ^2.2.6
-        version: 2.2.6
+        specifier: ^2.4.12
+        version: 2.4.12
       '@changesets/changelog-github':
         specifier: ^0.5.0
         version: 0.5.0(encoding@0.1.13)
@@ -2392,59 +2392,59 @@ packages:
     resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
     engines: {node: '>=18'}
 
-  '@biomejs/biome@2.2.6':
-    resolution: {integrity: sha512-yKTCNGhek0rL5OEW1jbLeZX8LHaM8yk7+3JRGv08my+gkpmtb5dDE+54r2ZjZx0ediFEn1pYBOJSmOdDP9xtFw==}
+  '@biomejs/biome@2.4.12':
+    resolution: {integrity: sha512-Rro7adQl3NLq/zJCIL98eElXKI8eEiBtoeu5TbXF/U3qbjuSc7Jb5rjUbeHHcquDWeSf3HnGP7XI5qGrlRk/pA==}
     engines: {node: '>=14.21.3'}
     hasBin: true
 
-  '@biomejs/cli-darwin-arm64@2.2.6':
-    resolution: {integrity: sha512-UZPmn3M45CjTYulgcrFJFZv7YmK3pTxTJDrFYlNElT2FNnkkX4fsxjExTSMeWKQYoZjvekpH5cvrYZZlWu3yfA==}
+  '@biomejs/cli-darwin-arm64@2.4.12':
+    resolution: {integrity: sha512-BnMU4Pc3ciEVteVpZ0BK33MLr7X57F5w1dwDLDn+/iy/yTrA4Q/N2yftidFtsA4vrDh0FMXDpacNV/Tl3fbmng==}
     engines: {node: '>=14.21.3'}
     cpu: [arm64]
     os: [darwin]
 
-  '@biomejs/cli-darwin-x64@2.2.6':
-    resolution: {integrity: sha512-HOUIquhHVgh/jvxyClpwlpl/oeMqntlteL89YqjuFDiZ091P0vhHccwz+8muu3nTyHWM5FQslt+4Jdcd67+xWQ==}
+  '@biomejs/cli-darwin-x64@2.4.12':
+    resolution: {integrity: sha512-x9uJ0bI1rJsWICp3VH8w/5PnAVD3A7SqzDpbrfoUQX1QyWrK5jSU4fRLo/wSgGeplCivbxBRKmt5Xq4/nWvq8A==}
     engines: {node: '>=14.21.3'}
     cpu: [x64]
     os: [darwin]
 
-  '@biomejs/cli-linux-arm64-musl@2.2.6':
-    resolution: {integrity: sha512-TjCenQq3N6g1C+5UT3jE1bIiJb5MWQvulpUngTIpFsL4StVAUXucWD0SL9MCW89Tm6awWfeXBbZBAhJwjyFbRQ==}
+  '@biomejs/cli-linux-arm64-musl@2.4.12':
+    resolution: {integrity: sha512-FhfpkAAlKL6kwvcVap0Hgp4AhZmtd3YImg0kK1jd7C/aSoh4SfsB2f++yG1rU0lr8Y5MCFJrcSkmssiL9Xnnig==}
     engines: {node: '>=14.21.3'}
     cpu: [arm64]
     os: [linux]
     libc: [musl]
 
-  '@biomejs/cli-linux-arm64@2.2.6':
-    resolution: {integrity: sha512-BpGtuMJGN+o8pQjvYsUKZ+4JEErxdSmcRD/JG3mXoWc6zrcA7OkuyGFN1mDggO0Q1n7qXxo/PcupHk8gzijt5g==}
+  '@biomejs/cli-linux-arm64@2.4.12':
+    resolution: {integrity: sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw==}
     engines: {node: '>=14.21.3'}
     cpu: [arm64]
     os: [linux]
     libc: [glibc]
 
-  '@biomejs/cli-linux-x64-musl@2.2.6':
-    resolution: {integrity: sha512-1ZcBux8zVM3JhWN2ZCPaYf0+ogxXG316uaoXJdgoPZcdK/rmRcRY7PqHdAos2ExzvjIdvhQp72UcveI98hgOog==}
+  '@biomejs/cli-linux-x64-musl@2.4.12':
+    resolution: {integrity: sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew==}
     engines: {node: '>=14.21.3'}
     cpu: [x64]
     os: [linux]
     libc: [musl]
 
-  '@biomejs/cli-linux-x64@2.2.6':
-    resolution: {integrity: sha512-1HaM/dpI/1Z68zp8ZdT6EiBq+/O/z97a2AiHMl+VAdv5/ELckFt9EvRb8hDHpk8hUMoz03gXkC7VPXOVtU7faA==}
+  '@biomejs/cli-linux-x64@2.4.12':
+    resolution: {integrity: sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw==}
     engines: {node: '>=14.21.3'}
     cpu: [x64]
     os: [linux]
     libc: [glibc]
 
-  '@biomejs/cli-win32-arm64@2.2.6':
-    resolution: {integrity: sha512-h3A88G8PGM1ryTeZyLlSdfC/gz3e95EJw9BZmA6Po412DRqwqPBa2Y9U+4ZSGUAXCsnSQE00jLV8Pyrh0d+jQw==}
+  '@biomejs/cli-win32-arm64@2.4.12':
+    resolution: {integrity: sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig==}
     engines: {node: '>=14.21.3'}
     cpu: [arm64]
     os: [win32]
 
-  '@biomejs/cli-win32-x64@2.2.6':
-    resolution: {integrity: sha512-yx0CqeOhPjYQ5ZXgPfu8QYkgBhVJyvWe36as7jRuPrKPO5ylVDfwVtPQ+K/mooNTADW0IhxOZm3aPu16dP8yNQ==}
+  '@biomejs/cli-win32-x64@2.4.12':
+    resolution: {integrity: sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA==}
     engines: {node: '>=14.21.3'}
     cpu: [x64]
     os: [win32]
@@ -15419,39 +15419,39 @@ snapshots:
 
   '@bcoe/v8-coverage@1.0.2': {}
 
-  '@biomejs/biome@2.2.6':
+  '@biomejs/biome@2.4.12':
     optionalDependencies:
-      '@biomejs/cli-darwin-arm64': 2.2.6
-      '@biomejs/cli-darwin-x64': 2.2.6
-      '@biomejs/cli-linux-arm64': 2.2.6
-      '@biomejs/cli-linux-arm64-musl': 2.2.6
-      '@biomejs/cli-linux-x64': 2.2.6
-      '@biomejs/cli-linux-x64-musl': 2.2.6
-      '@biomejs/cli-win32-arm64': 2.2.6
-      '@biomejs/cli-win32-x64': 2.2.6
+      '@biomejs/cli-darwin-arm64': 2.4.12
+      '@biomejs/cli-darwin-x64': 2.4.12
+      '@biomejs/cli-linux-arm64': 2.4.12
+      '@biomejs/cli-linux-arm64-musl': 2.4.12
+      '@biomejs/cli-linux-x64': 2.4.12
+      '@biomejs/cli-linux-x64-musl': 2.4.12
+      '@biomejs/cli-win32-arm64': 2.4.12
+      '@biomejs/cli-win32-x64': 2.4.12
 
-  '@biomejs/cli-darwin-arm64@2.2.6':
+  '@biomejs/cli-darwin-arm64@2.4.12':
     optional: true
 
-  '@biomejs/cli-darwin-x64@2.2.6':
+  '@biomejs/cli-darwin-x64@2.4.12':
     optional: true
 
-  '@biomejs/cli-linux-arm64-musl@2.2.6':
+  '@biomejs/cli-linux-arm64-musl@2.4.12':
     optional: true
 
-  '@biomejs/cli-linux-arm64@2.2.6':
+  '@biomejs/cli-linux-arm64@2.4.12':
     optional: true
 
-  '@biomejs/cli-linux-x64-musl@2.2.6':
+  '@biomejs/cli-linux-x64-musl@2.4.12':
     optional: true
 
-  '@biomejs/cli-linux-x64@2.2.6':
+  '@biomejs/cli-linux-x64@2.4.12':
     optional: true
 
-  '@biomejs/cli-win32-arm64@2.2.6':
+  '@biomejs/cli-win32-arm64@2.4.12':
     optional: true
 
-  '@biomejs/cli-win32-x64@2.2.6':
+  '@biomejs/cli-win32-x64@2.4.12':
     optional: true
 
   '@braintree/sanitize-url@7.1.0': {}