diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8dd9d1c70..c506c36f6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,8 +7,8 @@ "ghcr.io/devcontainers/features/node:1": { "version": "22.11.0" }, - "ghcr.io/devcontainers-extra/features/corepack:1": { - "version": "0.31.0" + "ghcr.io/devcontainers-extra/features/pnpm:2": { + "version": "10.6.1" } }, "forwardPorts": [3000], diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh index e02a533c1..216292b08 100755 --- a/.devcontainer/init.sh +++ b/.devcontainer/init.sh @@ -7,8 +7,6 @@ sudo apt-get update sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb git config --global --add safe.directory /workspace git submodule update --init -corepack install -corepack enable pnpm config set store-dir /home/node/.local/share/pnpm/store pnpm install --frozen-lockfile cp .devcontainer/devcontainer.yml .config/default.yml diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml index fdd128be3..1c4bee209 100644 --- a/.github/workflows/api-misskey-js.yml +++ b/.github/workflows/api-misskey-js.yml @@ -9,10 +9,6 @@ on: paths: - packages/misskey-js/** - .github/workflows/api-misskey-js.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: report: @@ -22,7 +18,8 @@ jobs: - name: Checkout uses: actions/checkout@v4.2.2 - - run: corepack enable + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Setup Node.js uses: actions/setup-node@v4.2.0 diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml index 2da964746..3244a3915 100644 --- a/.github/workflows/get-api-diff.yml +++ b/.github/workflows/get-api-diff.yml @@ -9,10 +9,6 @@ on: paths: - packages/backend/** - .github/workflows/get-api-diff.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: get-from-misskey: runs-on: ubuntu-latest @@ -34,14 +30,13 @@ jobs: with: ref: ${{ matrix.ref }} submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b1d52e8b3..361bd697e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -28,10 +28,6 @@ on: - packages/misskey-reversi/** - packages/shared/eslint.config.js - .github/workflows/lint.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: pnpm_install: runs-on: ubuntu-latest @@ -40,12 +36,12 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile lint: @@ -71,12 +67,12 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Restore eslint cache uses: actions/cache@v4.2.2 @@ -101,12 +97,12 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - run: pnpm --filter misskey-js run build if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'sw' }} diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml index 2daeaa3bd..4c0de376d 100644 --- a/.github/workflows/locale.yml +++ b/.github/workflows/locale.yml @@ -9,10 +9,6 @@ on: paths: - locales/** - .github/workflows/locale.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: locale_verify: runs-on: ubuntu-latest @@ -22,11 +18,11 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - run: cd locales && node verify.js diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml index 8e4ad4368..aa32f2cb3 100644 --- a/.github/workflows/on-release-created.yml +++ b/.github/workflows/on-release-created.yml @@ -6,9 +6,6 @@ on: workflow_dispatch: -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: publish-misskey-js: name: Publish misskey-js @@ -26,8 +23,8 @@ jobs: - uses: actions/checkout@v4.2.2 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: @@ -36,7 +33,6 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Publish package run: | - corepack enable pnpm i --frozen-lockfile pnpm build pnpm --filter misskey-js publish --access public --no-git-checks --provenance diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index 9e5a79faa..9fdbeab91 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -13,9 +13,6 @@ on: # This is a waste of chromatic build quota, so we don't run storybook CI on pull requests targets master. - master -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: build: # chromatic is not likely to be available for fork repositories, so we disable for fork repositories. @@ -43,14 +40,13 @@ jobs: run: | echo "base=$(git rev-list --parents -n1 HEAD | cut -d" " -f2)" >> $GITHUB_OUTPUT git checkout $(git rev-list --parents -n1 HEAD | cut -d" " -f3) - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js 20.x uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 2b8092cf4..69652621c 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -18,10 +18,6 @@ on: - packages/misskey-js/** - .github/workflows/test-backend.yml - .github/misskey/test.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: unit: name: Unit tests (backend) @@ -48,8 +44,8 @@ jobs: - uses: actions/checkout@v4.2.2 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Install FFmpeg run: | for i in {1..3}; do @@ -70,7 +66,6 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml @@ -111,14 +106,13 @@ jobs: - uses: actions/checkout@v4.2.2 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml diff --git a/.github/workflows/test-federation.yml b/.github/workflows/test-federation.yml index 0b71325de..93588b54b 100644 --- a/.github/workflows/test-federation.yml +++ b/.github/workflows/test-federation.yml @@ -15,9 +15,6 @@ on: - packages/misskey-js/** - .github/workflows/test-federation.yml -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: test: name: Federation test @@ -29,8 +26,8 @@ jobs: - uses: actions/checkout@v4 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Install FFmpeg run: | for i in {1..3}; do @@ -53,7 +50,6 @@ jobs: cache: 'pnpm' - name: Build Misskey run: | - corepack enable && corepack prepare pnpm i --frozen-lockfile pnpm build - name: Setup diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index e489ebf07..14a754c19 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -22,10 +22,6 @@ on: - packages/backend/** - .github/workflows/test-frontend.yml - .github/misskey/test.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: vitest: name: Unit tests (frontend) @@ -39,14 +35,13 @@ jobs: - uses: actions/checkout@v4.2.2 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml @@ -95,14 +90,13 @@ jobs: # if: ${{ matrix.browser == 'firefox' }} #- uses: browser-actions/setup-firefox@latest # if: ${{ matrix.browser == 'firefox' }} - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Copy Configure run: cp .github/misskey/test.yml .config diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml index 05f757acc..29b6c6172 100644 --- a/.github/workflows/test-misskey-js.yml +++ b/.github/workflows/test-misskey-js.yml @@ -14,10 +14,6 @@ on: paths: - packages/misskey-js/** - .github/workflows/test-misskey-js.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: test: name: Unit tests (misskey.js) @@ -33,7 +29,8 @@ jobs: - name: Checkout uses: actions/checkout@v4.2.2 - - run: corepack enable + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index 56e42213f..205eae239 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -9,7 +9,6 @@ on: env: NODE_ENV: production - COREPACK_DEFAULT_TO_LATEST: 0 jobs: production: @@ -24,14 +23,13 @@ jobs: - uses: actions/checkout@v4.2.2 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml index a8b240298..f84efa482 100644 --- a/.github/workflows/validate-api-json.yml +++ b/.github/workflows/validate-api-json.yml @@ -12,10 +12,6 @@ on: paths: - packages/backend/** - .github/workflows/validate-api-json.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: validate-api-json: runs-on: ubuntu-latest @@ -28,8 +24,8 @@ jobs: - uses: actions/checkout@v4.2.2 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: @@ -37,7 +33,6 @@ jobs: cache: 'pnpm' - name: Install Redocly CLI run: npm i -g @redocly/cli - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml diff --git a/.npmrc b/.npmrc index c42da845b..daebfd521 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,3 @@ engine-strict = true +save-exact = true +shell-emulator = true diff --git a/CHANGELOG.md b/CHANGELOG.md index 197de5aec..e5df6de4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## 2025.3.1 + +### General +- pnpmをv10に更新 +- Corepackを削除 + +### Client +- Feat: 設定の検索を追加(実験的) +- Enhance: 設定項目の再配置 + +### Server +- Fix: DBマイグレーション際にシステムアカウントのユーザーID判定が正しくない問題を修正 +- Fix: user.featured列が状況によってJSON文字列になっていたのを修正 + + ## 2025.3.0 ### General diff --git a/Dockerfile b/Dockerfile index 3bc204439..9d5596f1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,8 +6,6 @@ ARG NODE_VERSION=22.11.0-bookworm FROM --platform=$BUILDPLATFORM node:${NODE_VERSION} AS native-builder -ENV COREPACK_DEFAULT_TO_LATEST=0 - RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean \ @@ -16,8 +14,6 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ && apt-get install -yqq --no-install-recommends \ build-essential -RUN corepack enable - WORKDIR /misskey COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] @@ -33,6 +29,8 @@ COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bu ARG NODE_ENV=production +RUN node -e "console.log(JSON.parse(require('node:fs').readFileSync('./package.json')).packageManager)" | xargs npm install -g + RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --frozen-lockfile --aggregate-output @@ -46,14 +44,10 @@ RUN rm -rf .git/ FROM --platform=$TARGETPLATFORM node:${NODE_VERSION} AS target-builder -ENV COREPACK_DEFAULT_TO_LATEST=0 - RUN apt-get update \ && apt-get install -yqq --no-install-recommends \ build-essential -RUN corepack enable - WORKDIR /misskey COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] @@ -65,6 +59,8 @@ COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bu ARG NODE_ENV=production +RUN node -e "console.log(JSON.parse(require('node:fs').readFileSync('./package.json')).packageManager)" | xargs npm install -g + RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --frozen-lockfile --aggregate-output @@ -72,13 +68,11 @@ FROM --platform=$TARGETPLATFORM node:${NODE_VERSION}-slim AS runner ARG UID="991" ARG GID="991" -ENV COREPACK_DEFAULT_TO_LATEST=0 RUN apt-get update \ && apt-get install -y --no-install-recommends \ ffmpeg tini curl libjemalloc-dev libjemalloc2 \ && ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so \ - && corepack enable \ && groupadd -g "${GID}" misskey \ && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \ && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \ @@ -86,13 +80,13 @@ RUN apt-get update \ && apt-get clean \ && rm -rf /var/lib/apt/lists +# add package.json to add pnpm +COPY ./package.json ./package.json +RUN node -e "console.log(JSON.parse(require('node:fs').readFileSync('./package.json')).packageManager)" | xargs npm install -g + USER misskey WORKDIR /misskey -# add package.json to add pnpm -COPY --chown=misskey:misskey ./package.json ./package.json -RUN corepack install - COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 8b52450e9..377d16b14 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -111,7 +111,7 @@ followRequests: "Peticions de seguiment" unfollow: "Deixar de seguir" followRequestPending: "Sol·licituds de seguiment pendents" enterEmoji: "Introduir un emoji" -renote: "Impulsos" +renote: "Impulsar" unrenote: "Anul·la l'impuls" renoted: "S'ha impulsat" renotedToX: "Impulsat per {name}." @@ -1114,7 +1114,7 @@ forceShowAds: "Mostra els anuncis sempre " addMemo: "Afegir recordatori" editMemo: "Editar recordatori" reactionsList: "Reaccions" -renotesList: "Impulsos" +renotesList: "Llistat d'impulsos " notificationDisplay: "Notificacions" leftTop: "Dalt a l'esquerra " rightTop: "Dalt a la dreta " @@ -1190,7 +1190,7 @@ pastAnnouncements: "Informes passats" youHaveUnreadAnnouncements: "Tens informes per llegir." useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey." replies: "Respostes" -renotes: "Impulsos" +renotes: "Impulsar" loadReplies: "Mostrar les respostes" loadConversation: "Mostrar la conversació " pinnedList: "Llista fixada" @@ -2452,7 +2452,7 @@ _notification: follow: "Segueix-me" mention: "Menció" reply: "Respostes" - renote: "Renotar" + renote: "Impulsar" quote: "Citar" reaction: "Reaccions" pollEnded: "Enquesta terminada" @@ -2467,7 +2467,7 @@ _notification: _actions: followBack: "També et segueix" reply: "Respondre" - renote: "Renotar" + renote: "Impulsos" _deck: alwaysShowMainColumn: "Mostrar sempre la columna principal" columnAlign: "Alinea les columnes" diff --git a/locales/en-US.yml b/locales/en-US.yml index 04ddd2966..f4c332369 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1311,6 +1311,8 @@ federationSpecified: "This server is operated in a whitelist federation. Interac federationDisabled: "Federation is disabled on this server. You cannot interact with users on other servers." confirmOnReact: "Confirm when reacting" reactAreYouSure: "Would you like to add a \"{emoji}\" reaction?" +markAsSensitiveConfirm: "Do you want to set this media as sensitive?" +unmarkAsSensitiveConfirm: "Do you want to remove the sensitive designation for this media?" _accountSettings: requireSigninToViewContents: "Require sign-in to view contents" requireSigninToViewContentsDescription1: "Require login to view all notes and other content you have created. This will have the effect of preventing crawlers from collecting your information." @@ -2594,6 +2596,7 @@ _moderationLogTypes: deletePage: "Page deleted" deleteFlash: "Play deleted" deleteGalleryPost: "Gallery post deleted" + updateProxyAccountDescription: "Update the description of the proxy account" _fileViewer: title: "File details" type: "File type" @@ -2649,7 +2652,7 @@ _dataSaver: description: "Prevents images/videos from being loaded automatically. Hidden images/videos will be loaded when tapped." _avatar: title: "Avatar image" - description: "Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic." + description: "Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic." _urlPreview: title: "URL preview thumbnails" description: "URL preview thumbnail images will no longer be loaded." @@ -2857,4 +2860,8 @@ _bootErrors: _search: searchScopeAll: "All" searchScopeLocal: "Local" + searchScopeServer: "Specific server" searchScopeUser: "Specific user" + pleaseEnterServerHost: "Enter the server host" + pleaseSelectUser: "Select user" + serverHostPlaceholder: "Example: misskey.example.com" diff --git a/locales/index.d.ts b/locales/index.d.ts index 947b57779..6810d204c 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4971,7 +4971,7 @@ export interface Locale extends ILocale { */ "disableStreamingTimeline": string; /** - * 通知をグルーピングして表示する + * 通知をグルーピング */ "useGroupedNotifications": string; /** @@ -5270,6 +5270,14 @@ export interface Locale extends ILocale { * このメディアのセンシティブ指定を解除しますか? */ "unmarkAsSensitiveConfirm": string; + /** + * 環境設定 + */ + "preferences": string; + /** + * アクセシビリティ + */ + "accessibility": string; "_accountSettings": { /** * コンテンツの表示にログインを必須にする diff --git a/locales/it-IT.yml b/locales/it-IT.yml index c3a33139b..524892703 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1313,6 +1313,8 @@ confirmOnReact: "Confermare le reazioni" reactAreYouSure: "Vuoi davvero reagire con {emoji} ?" markAsSensitiveConfirm: "Vuoi davvero indicare questo contenuto multimediale come esplicito?" unmarkAsSensitiveConfirm: "Vuoi davvero indicare come non esplicito il contenuto multimediale?" +preferences: "Preferenze" +accessibility: "Accessibilità" _accountSettings: requireSigninToViewContents: "Per vedere il contenuto, è necessaria l'iscrizione" requireSigninToViewContentsDescription1: "Richiedere l'iscrizione per visualizzare tutte le Note e gli altri contenuti che hai creato. Probabilmente l'effetto è impedire la raccolta di informazioni da parte dei bot crawler." @@ -1475,7 +1477,7 @@ _serverSettings: _accountMigration: moveFrom: "Migra un altro profilo dentro a questo" moveFromSub: "Crea un alias verso un altro profilo remoto" - moveFromLabel: "Profilo da cui migrare #{n}" + moveFromLabel: "Profilo da cui migrare n. {n}" moveFromDescription: "Se desideri spostare i Follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@vecchia.istanza.it" moveTo: "Migrare questo profilo verso un un altro" moveToLabel: "Profilo verso cui migrare" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index fbe4d9889..7a5d2f795 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1238,7 +1238,7 @@ releaseToRefresh: "離してリロード" refreshing: "リロード中" pullDownToRefresh: "引っ張ってリロード" disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする" -useGroupedNotifications: "通知をグルーピングして表示する" +useGroupedNotifications: "通知をグルーピング" signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。" cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。" doReaction: "リアクションする" @@ -1313,6 +1313,8 @@ confirmOnReact: "リアクションする際に確認する" reactAreYouSure: "\" {emoji} \" をリアクションしますか?" markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?" unmarkAsSensitiveConfirm: "このメディアのセンシティブ指定を解除しますか?" +preferences: "環境設定" +accessibility: "アクセシビリティ" _accountSettings: requireSigninToViewContents: "コンテンツの表示にログインを必須にする" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index f0df9deee..f5a978606 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1313,6 +1313,8 @@ confirmOnReact: "发送回应前需要确认" reactAreYouSure: "要用「{emoji}」进行回应吗?" markAsSensitiveConfirm: "要将此媒体标记为敏感吗?" unmarkAsSensitiveConfirm: "要将此媒体解除敏感标记吗?" +preferences: "设置" +accessibility: "辅助功能" _accountSettings: requireSigninToViewContents: "需要登录才能显示内容" requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 7c7e29054..ea4ef49e7 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1313,6 +1313,8 @@ confirmOnReact: "反應時確認" reactAreYouSure: "用「 {emoji} 」反應嗎?" markAsSensitiveConfirm: "要將這個媒體設定為敏感嗎?" unmarkAsSensitiveConfirm: "要解除這個媒體的敏感設定嗎?" +preferences: "環境設定" +accessibility: "輔助工具" _accountSettings: requireSigninToViewContents: "須登入以顯示內容" requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。" diff --git a/package.json b/package.json index a3a5924af..267dd8ada 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "misskey", - "version": "2025.3.0", + "version": "2025.3.1", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/misskey-dev/misskey.git" }, - "packageManager": "pnpm@9.15.4", + "packageManager": "pnpm@10.6.1", "workspaces": [ "packages/frontend-shared", "packages/frontend", @@ -47,35 +47,44 @@ "cleanall": "pnpm clean-all" }, "resolutions": { - "chokidar": "3.6.0", + "chokidar": "4.0.3", "lodash": "4.17.21" }, "dependencies": { "cssnano": "7.0.6", - "execa": "8.0.1", + "execa": "9.5.2", "fast-glob": "3.3.3", - "ignore-walk": "6.0.5", + "ignore-walk": "7.0.0", "js-yaml": "4.1.0", - "postcss": "8.5.2", - "tar": "6.2.1", + "postcss": "8.5.3", + "tar": "7.4.3", "terser": "5.39.0", - "typescript": "5.7.3", + "typescript": "5.8.2", "esbuild": "0.25.0", "glob": "11.0.1" }, "devDependencies": { "@misskey-dev/eslint-plugin": "2.1.0", - "@types/node": "22.13.4", - "@typescript-eslint/eslint-plugin": "8.24.0", - "@typescript-eslint/parser": "8.24.0", + "@types/node": "22.13.9", + "@typescript-eslint/eslint-plugin": "8.26.0", + "@typescript-eslint/parser": "8.26.0", "cross-env": "7.0.3", - "cypress": "14.0.3", - "eslint": "9.20.1", - "globals": "15.15.0", + "cypress": "14.1.0", + "eslint": "9.21.0", + "globals": "16.0.0", "ncp": "2.0.0", + "pnpm": "10.6.1", "start-server-and-test": "2.0.10" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.22.0" + }, + "pnpm": { + "overrides": { + "@aiscript-dev/aiscript-languageserver": "-" + }, + "patchedDependencies": { + "re2": "scripts/dependency-patches/re2.patch" + } } } diff --git a/packages/backend/migration/1741279404074-system-accounts-fixup.js b/packages/backend/migration/1741279404074-system-accounts-fixup.js new file mode 100644 index 000000000..31cab7f5a --- /dev/null +++ b/packages/backend/migration/1741279404074-system-accounts-fixup.js @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SystemAccounts1741279404074 { + name = 'SystemAccounts1741279404074' + + async up(queryRunner) { + const instanceActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'instance.actor' AND "host" IS NULL AND "id" NOT IN (SELECT "userId" FROM "system_account" WHERE "type" = 'actor')`); + if (instanceActor.length > 0) { + console.warn('instance.actor was incorrect, updating...'); + await queryRunner.query(`UPDATE "system_account" SET "id" = '${instanceActor[0].id}', "userId" = '${instanceActor[0].id}' WHERE "type" = 'actor'`); + } + + const relayActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'relay.actor' AND "host" IS NULL AND "id" NOT IN (SELECT "userId" FROM "system_account" WHERE "type" = 'relay')`); + if (relayActor.length > 0) { + console.warn('relay.actor was incorrect, updating...'); + await queryRunner.query(`UPDATE "system_account" SET "id" = '${relayActor[0].id}', "userId" = '${relayActor[0].id}' WHERE "type" = 'relay'`); + } + } + + async down(queryRunner) { + // fixup migration, no down migration + } +} diff --git a/packages/backend/migration/1741424411879-user-featured-fixup.js b/packages/backend/migration/1741424411879-user-featured-fixup.js new file mode 100644 index 000000000..5643a328f --- /dev/null +++ b/packages/backend/migration/1741424411879-user-featured-fixup.js @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class UserFeaturedFixup1741424411879 { + name = 'UserFeaturedFixup1741424411879' + + async up(queryRunner) { + await queryRunner.query(`CREATE OR REPLACE FUNCTION pg_temp.extract_ap_id(text) RETURNS text AS $$ + SELECT + CASE + WHEN $1 ~ '^https?://' THEN $1 + WHEN $1 LIKE '{%' THEN COALESCE(jsonb_extract_path_text($1::jsonb, 'id'), null) + ELSE null + END; + $$ LANGUAGE sql IMMUTABLE;`); + + // "host" is NOT NULL is not needed but just in case add it to prevent overwriting irreplaceable data + await queryRunner.query(`UPDATE "user" SET "featured" = pg_temp.extract_ap_id("featured") WHERE "host" IS NOT NULL`); + } + + async down(queryRunner) { + // fixup migration, no down migration + } +} diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index fc68eb483..a295e8192 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -268,7 +268,6 @@ export class FileInfoService { private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator { const watcher = new FSWatcher({ cwd, - disableGlobbing: true, }); let finished = false; command.once('end', () => { diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 879f1922c..e52078ed0 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -560,7 +560,7 @@ export class ApPersonService implements OnModuleInit { inbox: person.inbox, sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null, followersUri: person.followers ? getApId(person.followers) : undefined, - featured: person.featured, + featured: person.featured ? getApId(person.featured) : undefined, emojis: emojiNames, name: truncate(person.name, nameLength), tags, diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 8c4b13a40..20e985aaf 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -751,7 +751,7 @@ export class ActivityPubServerService { }); // follow - fastify.get<{ Params: { followRequestId: string ; } }>('/follows/:followRequestId', async (request, reply) => { + fastify.get<{ Params: { followRequestId: string; } }>('/follows/:followRequestId', async (request, reply) => { // This may be used before the follow is completed, so we do not // check if the following exists and only check if the follow request exists. diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index bf0a01169..772c37094 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -497,7 +497,7 @@ export class FileServerService { @bindThis private async downloadAndDetectTypeFromUrl(url: string): Promise< - { state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } + { state: 'remote'; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } > { const [path, cleanup] = await createTemp(); try { diff --git a/packages/backend/test-federation/compose.tpl.yml b/packages/backend/test-federation/compose.tpl.yml index 8b270e58f..25770063d 100644 --- a/packages/backend/test-federation/compose.tpl.yml +++ b/packages/backend/test-federation/compose.tpl.yml @@ -17,7 +17,6 @@ services: - ./.config/docker.env environment: - NODE_ENV=production - - COREPACK_DEFAULT_TO_LATEST=0 volumes: - type: bind source: ../../../built @@ -75,6 +74,10 @@ services: source: ../../../pnpm-workspace.yaml target: /misskey/pnpm-workspace.yaml read_only: true + - type: bind + source: ../../../scripts/dependency-patches + target: /misskey/scripts/dependency-patches + read_only: true - type: bind source: ./certificates/rootCA.crt target: /usr/local/share/ca-certificates/rootCA.crt @@ -82,7 +85,7 @@ services: working_dir: /misskey command: > bash -c " - corepack enable && corepack prepare + npm install -g pnpm pnpm -F backend migrate pnpm -F backend start " diff --git a/packages/backend/test-federation/compose.yml b/packages/backend/test-federation/compose.yml index ed39109aa..dfa51b940 100644 --- a/packages/backend/test-federation/compose.yml +++ b/packages/backend/test-federation/compose.yml @@ -9,7 +9,7 @@ services: service: misskey command: > bash -c " - corepack enable && corepack prepare + npm install -g pnpm pnpm -F backend i pnpm -F misskey-js i pnpm -F misskey-reversi i @@ -29,7 +29,6 @@ services: environment: - NODE_ENV=development - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt - - COREPACK_DEFAULT_TO_LATEST=0 volumes: - type: bind source: ../package.json @@ -71,6 +70,10 @@ services: source: ../../../pnpm-workspace.yaml target: /misskey/pnpm-workspace.yaml read_only: true + - type: bind + source: ../../../scripts/dependency-patches + target: /misskey/scripts/dependency-patches + read_only: true - type: bind source: ./certificates/rootCA.crt target: /usr/local/share/ca-certificates/rootCA.crt @@ -78,7 +81,7 @@ services: working_dir: /misskey entrypoint: > bash -c ' - corepack enable && corepack prepare + npm install -g pnpm pnpm -F misskey-js i --frozen-lockfile pnpm -F backend i --frozen-lockfile exec "$0" "$@" @@ -90,8 +93,6 @@ services: depends_on: redis.test: condition: service_healthy - environment: - - COREPACK_DEFAULT_TO_LATEST=0 volumes: - type: bind source: ../package.json @@ -117,10 +118,14 @@ services: source: ../../../pnpm-workspace.yaml target: /misskey/pnpm-workspace.yaml read_only: true + - type: bind + source: ../../../scripts/dependency-patches + target: /misskey/scripts/dependency-patches + read_only: true working_dir: /misskey command: > bash -c " - corepack enable && corepack prepare + npm install -g pnpm pnpm -F backend i --frozen-lockfile pnpm exec tsc -p ./packages/backend/test-federation node ./packages/backend/test-federation/built/daemon.js diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index 1ee4fc2c2..21247e32a 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -34,7 +34,7 @@ "typescript": "5.8.2", "uuid": "11.1.0", "json5": "2.2.3", - "vite": "6.2.0", + "vite": "6.2.1", "vue": "3.5.13" }, "devDependencies": { @@ -48,14 +48,14 @@ "@types/ws": "8.18.0", "@typescript-eslint/eslint-plugin": "8.26.0", "@typescript-eslint/parser": "8.26.0", - "@vitest/coverage-v8": "3.0.7", + "@vitest/coverage-v8": "3.0.8", "@vue/runtime-core": "3.5.13", - "acorn": "8.14.0", + "acorn": "8.14.1", "cross-env": "7.0.3", "eslint-plugin-import": "2.31.0", - "eslint-plugin-vue": "9.33.0", + "eslint-plugin-vue": "10.0.0", "fast-glob": "3.3.3", - "happy-dom": "17.2.2", + "happy-dom": "17.3.0", "intersection-observer": "0.12.2", "micromatch": "4.0.8", "msw": "2.7.3", @@ -64,7 +64,7 @@ "start-server-and-test": "2.0.10", "vite-plugin-turbosnap": "1.0.3", "vue-component-type-helpers": "2.2.8", - "vue-eslint-parser": "9.4.3", + "vue-eslint-parser": "10.1.1", "vue-tsc": "2.2.8" } } diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index ad9a0bafb..7a05771ea 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -25,10 +25,10 @@ "@typescript-eslint/eslint-plugin": "8.26.0", "@typescript-eslint/parser": "8.26.0", "esbuild": "0.25.0", - "eslint-plugin-vue": "9.33.0", + "eslint-plugin-vue": "10.0.0", "nodemon": "3.1.9", "typescript": "5.8.2", - "vue-eslint-parser": "9.4.3" + "vue-eslint-parser": "10.1.1" }, "files": [ "js-built" diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts index 9f318cf44..c1119c252 100644 --- a/packages/frontend/.storybook/main.ts +++ b/packages/frontend/.storybook/main.ts @@ -39,6 +39,10 @@ const config = { if (~replacePluginForIsChromatic) { config.plugins?.splice(replacePluginForIsChromatic, 1); } + + //pluginsからcreateSearchIndexを削除、複数あるかもしれないので全て削除 + config.plugins = config.plugins?.filter((plugin: Plugin) => plugin && plugin.name !== 'createSearchIndex') ?? []; + return mergeConfig(config, { plugins: [ { diff --git a/packages/frontend/lib/vite-plugin-create-search-index.ts b/packages/frontend/lib/vite-plugin-create-search-index.ts new file mode 100644 index 000000000..509eb804c --- /dev/null +++ b/packages/frontend/lib/vite-plugin-create-search-index.ts @@ -0,0 +1,1496 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { parse as vueSfcParse } from 'vue/compiler-sfc'; +import type { LogOptions, Plugin } from 'vite'; +import fs from 'node:fs'; +import { glob } from 'glob'; +import JSON5 from 'json5'; +import MagicString from 'magic-string'; +import path from 'node:path' +import { hash, toBase62 } from '../vite.config'; +import { createLogger } from 'vite'; + +interface VueAstNode { + type: number; + tag?: string; + loc?: { + start: { offset: number, line: number, column: number }, + end: { offset: number, line: number, column: number }, + source?: string + }; + props?: Array<{ + name: string; + type: number; + value?: { content?: string }; + arg?: { content?: string }; + exp?: { content?: string; loc?: any }; + }>; + children?: VueAstNode[]; + content?: any; + __markerId?: string; + __children?: string[]; +} + +export type AnalysisResult = { + filePath: string; + usage: SearchIndexItem[]; +} + +export type SearchIndexItem = { + id: string; + path?: string; + label: string; + keywords: string | string[]; + icon?: string; + inlining?: string[]; + children?: SearchIndexItem[]; +}; + +export type Options = { + targetFilePaths: string[], + exportFilePath: string, + verbose?: boolean, +}; + +// 関連するノードタイプの定数化 +const NODE_TYPES = { + ELEMENT: 1, + EXPRESSION: 2, + TEXT: 3, + INTERPOLATION: 5, // Mustache +}; + +// マーカー関係を表す型 +interface MarkerRelation { + parentId?: string; + markerId: string; + node: VueAstNode; +} + +// ロガー +let logger = { + info: (msg: string, options?: LogOptions) => { }, + warn: (msg: string, options?: LogOptions) => { }, + error: (msg: string, options?: LogOptions) => { }, +}; +let loggerInitialized = false; + +function initLogger(options: Options) { + if (loggerInitialized) return; + loggerInitialized = true; + const viteLogger = createLogger(options.verbose ? 'info' : 'warn'); + + logger.info = (msg, options) => { + msg = `[create-search-index] ${msg}`; + viteLogger.info(msg, options); + } + + logger.warn = (msg, options) => { + msg = `[create-search-index] ${msg}`; + viteLogger.warn(msg, options); + } + + logger.error = (msg, options) => { + msg = `[create-search-index] ${msg}`; + viteLogger.error(msg, options); + } +} + +/** + * 解析結果をTypeScriptファイルとして出力する + */ +function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisResult[]): void { + logger.info(`Processing ${analysisResults.length} files for output`); + + // 新しいツリー構造を構築 + const allMarkers = new Map(); + + // 1. すべてのマーカーを一旦フラットに収集 + for (const file of analysisResults) { + logger.info(`Processing file: ${file.filePath} with ${file.usage.length} markers`); + + for (const marker of file.usage) { + if (marker.id) { + // キーワードとchildren処理を共通化 + const processedMarker = { + ...marker, + keywords: processMarkerProperty(marker.keywords, 'keywords'), + children: processMarkerProperty(marker.children || [], 'children') + }; + + allMarkers.set(marker.id, processedMarker); + } + } + } + + logger.info(`Collected total ${allMarkers.size} unique markers`); + + // 2. 子マーカーIDの収集 + const childIds = collectChildIds(allMarkers); + logger.info(`Found ${childIds.size} child markers`); + + // 3. ルートマーカーの特定(他の誰かの子でないマーカー) + const rootMarkers = identifyRootMarkers(allMarkers, childIds); + logger.info(`Found ${rootMarkers.length} root markers`); + + // 4. 子マーカーの参照を解決 + const resolvedRootMarkers = resolveChildReferences(rootMarkers, allMarkers); + + // 5. デバッグ情報を生成 + const { totalMarkers, totalChildren } = countMarkers(resolvedRootMarkers); + logger.info(`Total markers in tree: ${totalMarkers} (${resolvedRootMarkers.length} roots + ${totalChildren} nested children)`); + + // 6. 結果をTS形式で出力 + writeOutputFile(outputPath, resolvedRootMarkers); +} + +/** + * マーカーのプロパティ(keywordsやchildren)を処理する + */ +function processMarkerProperty(propValue: any, propType: 'keywords' | 'children'): any { + // 文字列の配列表現を解析 + if (typeof propValue === 'string' && propValue.startsWith('[') && propValue.endsWith(']')) { + try { + // JSON5解析を試みる + return JSON5.parse(propValue.replace(/'/g, '"')); + } catch (e) { + // 解析に失敗した場合 + logger.warn(`Could not parse ${propType}: ${propValue}, using ${propType === 'children' ? 'empty array' : 'as is'}`); + return propType === 'children' ? [] : propValue; + } + } + + return propValue; +} + +/** + * 全マーカーから子IDを収集する + */ +function collectChildIds(allMarkers: Map): Set { + const childIds = new Set(); + + allMarkers.forEach((marker, id) => { + // 通常のchildren処理 + const children = marker.children; + if (Array.isArray(children)) { + children.forEach(childId => { + if (typeof childId === 'string') { + if (!allMarkers.has(childId)) { + logger.warn(`Warning: Child marker ID ${childId} referenced but not found`); + } else { + childIds.add(childId); + } + } + }); + } + + // inlining処理を追加 + if (marker.inlining) { + let inliningIds: string[] = []; + + // 文字列の場合は配列に変換 + if (typeof marker.inlining === 'string') { + try { + const inliningStr = (marker.inlining as string).trim(); + if (inliningStr.startsWith('[') && inliningStr.endsWith(']')) { + inliningIds = JSON5.parse(inliningStr.replace(/'/g, '"')); + logger.info(`Parsed inlining string to array: ${inliningStr} -> ${JSON.stringify(inliningIds)}`); + } else { + inliningIds = [inliningStr]; + } + } catch (e) { + logger.error(`Failed to parse inlining string: ${marker.inlining}`, e); + } + } + // 既に配列の場合 + else if (Array.isArray(marker.inlining)) { + inliningIds = marker.inlining; + } + + // inliningで指定されたIDを子セットに追加 + for (const inlineId of inliningIds) { + if (typeof inlineId === 'string') { + if (!allMarkers.has(inlineId)) { + logger.warn(`Warning: Inlining marker ID ${inlineId} referenced but not found`); + } else { + // inliningで参照されているマーカーも子として扱う + childIds.add(inlineId); + logger.info(`Added inlined marker ${inlineId} as child in collectChildIds`); + } + } + } + } + }); + + return childIds; +} + +/** + * ルートマーカー(他の子でないマーカー)を特定する + */ +function identifyRootMarkers( + allMarkers: Map, + childIds: Set +): SearchIndexItem[] { + const rootMarkers: SearchIndexItem[] = []; + + allMarkers.forEach((marker, id) => { + if (!childIds.has(id)) { + rootMarkers.push(marker); + logger.info(`Added root marker to output: ${id} with label ${marker.label}`); + } + }); + + return rootMarkers; +} + +/** + * 子マーカーの参照をIDから実際のオブジェクトに解決する + */ +function resolveChildReferences( + rootMarkers: SearchIndexItem[], + allMarkers: Map +): SearchIndexItem[] { + function resolveChildrenForMarker(marker: SearchIndexItem): SearchIndexItem { + // マーカーのディープコピーを作成 + const resolvedMarker = { ...marker }; + // 明示的に子マーカー配列を作成 + const resolvedChildren: SearchIndexItem[] = []; + + // 通常のchildren処理 + if (Array.isArray(marker.children)) { + for (const childId of marker.children) { + if (typeof childId === 'string') { + const childMarker = allMarkers.get(childId); + if (childMarker) { + // 子マーカーの子も再帰的に解決 + const resolvedChild = resolveChildrenForMarker(childMarker); + resolvedChildren.push(resolvedChild); + logger.info(`Resolved regular child ${childId} for parent ${marker.id}`); + } + } + } + } + + // inlining属性の処理 + let inliningIds: string[] = []; + + // 文字列の場合は配列に変換。例: "['2fa']" -> ['2fa'] + if (typeof marker.inlining === 'string') { + try { + // 文字列形式の配列を実際の配列に変換 + const inliningStr = (marker.inlining as string).trim(); + if (inliningStr.startsWith('[') && inliningStr.endsWith(']')) { + inliningIds = JSON5.parse(inliningStr.replace(/'/g, '"')); + logger.info(`Converted string inlining to array: ${inliningStr} -> ${JSON.stringify(inliningIds)}`); + } else { + // 単一値の場合は配列に + inliningIds = [inliningStr]; + logger.info(`Converted single string inlining to array: ${inliningStr}`); + } + } catch (e) { + logger.error(`Failed to parse inlining string: ${marker.inlining}`, e); + } + } + // 既に配列の場合はそのまま使用 + else if (Array.isArray(marker.inlining)) { + inliningIds = marker.inlining; + } + + // インライン指定されたマーカーを子として追加 + for (const inlineId of inliningIds) { + if (typeof inlineId === 'string') { + const inlineMarker = allMarkers.get(inlineId); + if (inlineMarker) { + // インライン指定されたマーカーを再帰的に解決 + const resolvedInline = resolveChildrenForMarker(inlineMarker); + delete resolvedInline.path + resolvedChildren.push(resolvedInline); + logger.info(`Added inlined marker ${inlineId} as child to ${marker.id}`); + } else { + logger.warn(`Inlining target not found: ${inlineId} referenced by ${marker.id}`); + } + } + } + + // 解決した子が存在する場合のみchildrenプロパティを設定 + if (resolvedChildren.length > 0) { + resolvedMarker.children = resolvedChildren; + } else { + delete resolvedMarker.children; + } + + return resolvedMarker; + } + + // すべてのルートマーカーの子を解決 + return rootMarkers.map(marker => resolveChildrenForMarker(marker)); +} + +/** + * マーカー数を数える(デバッグ用) + */ +function countMarkers(markers: SearchIndexItem[]): { totalMarkers: number, totalChildren: number } { + let totalMarkers = markers.length; + let totalChildren = 0; + + function countNested(items: SearchIndexItem[]): void { + for (const marker of items) { + if (marker.children && Array.isArray(marker.children)) { + totalChildren += marker.children.length; + totalMarkers += marker.children.length; + countNested(marker.children as SearchIndexItem[]); + } + } + } + + countNested(markers); + return { totalMarkers, totalChildren }; +} + +/** + * 最終的なTypeScriptファイルを出力 + */ +function writeOutputFile(outputPath: string, resolvedRootMarkers: SearchIndexItem[]): void { + try { + const tsOutput = generateTypeScriptCode(resolvedRootMarkers); + fs.writeFileSync(outputPath, tsOutput, 'utf-8'); + // 強制的に出力させるためにViteロガーを使わない + console.log(`Successfully wrote search index to ${outputPath} with ${resolvedRootMarkers.length} root entries`); + } catch (error) { + logger.error('[create-search-index]: error writing output: ', error); + } +} + +/** + * TypeScriptコード生成 + */ +function generateTypeScriptCode(resolvedRootMarkers: SearchIndexItem[]): string { + return ` +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// This file was automatically generated by create-search-index. +// Do not edit this file. + +import { i18n } from '@/i18n.js'; + +export type SearchIndexItem = { + id: string; + path?: string; + label: string; + keywords: string[]; + icon?: string; + children?: SearchIndexItem[]; +}; + +export const searchIndexes: SearchIndexItem[] = ${customStringify(resolvedRootMarkers)} as const; + +export type SearchIndex = typeof searchIndexes; +`; +} + +/** + * オブジェクトを特殊な形式の文字列に変換する + * i18n参照を保持しつつ適切な形式に変換 + */ +function customStringify(obj: any, depth = 0): string { + const INDENT_STR = '\t'; + + // 配列の処理 + if (Array.isArray(obj)) { + if (obj.length === 0) return '[]'; + const indent = INDENT_STR.repeat(depth); + const childIndent = INDENT_STR.repeat(depth + 1); + + // 配列要素の処理 + const items = obj.map(item => { + // オブジェクト要素 + if (typeof item === 'object' && item !== null) { + return `${childIndent}${customStringify(item, depth + 1)}`; + } + + // i18n参照を含む文字列要素 + if (typeof item === 'string' && item.includes('i18n.ts.')) { + return `${childIndent}${item}`; // クォートなしでそのまま出力 + } + + // その他の要素 + return `${childIndent}${JSON5.stringify(item)}`; + }).join(',\n'); + + return `[\n${items},\n${indent}]`; + } + + // null または非オブジェクト + if (obj === null || typeof obj !== 'object') { + return JSON5.stringify(obj); + } + + // オブジェクトの処理 + const indent = INDENT_STR.repeat(depth); + const childIndent = INDENT_STR.repeat(depth + 1); + + const entries = Object.entries(obj) + // 不要なプロパティを除去 + .filter(([key, value]) => { + if (value === undefined) return false; + if (key === 'children' && Array.isArray(value) && value.length === 0) return false; + if (key === 'inlining') return false; + return true; + }) + // 各プロパティを変換 + .map(([key, value]) => { + // 子要素配列の特殊処理 + if (key === 'children' && Array.isArray(value) && value.length > 0) { + return `${childIndent}${key}: ${customStringify(value, depth + 1)}`; + } + + // ラベルやその他プロパティを処理 + return `${childIndent}${key}: ${formatSpecialProperty(key, value)}`; + }); + + if (entries.length === 0) return '{}'; + return `{\n${entries.join(',\n')},\n${indent}}`; +} + +/** + * 特殊プロパティの書式設定 + */ +function formatSpecialProperty(key: string, value: any): string { + // 値がundefinedの場合は空文字列を返す + if (value === undefined) { + return '""'; + } + + // childrenが配列の場合は特別に処理 + if (key === 'children' && Array.isArray(value)) { + return customStringify(value); + } + + // keywordsが配列の場合、特別に処理 + if (key === 'keywords' && Array.isArray(value)) { + return `[${formatArrayForOutput(value)}]`; + } + + // 文字列値の場合の特別処理 + if (typeof value === 'string') { + // i18n.ts 参照を含む場合 - クォートなしでそのまま出力 + if (isI18nReference(value)) { + logger.info(`Preserving i18n reference in output: ${value}`); + return value; + } + + // keywords が配列リテラルの形式の場合 + if (key === 'keywords' && value.startsWith('[') && value.endsWith(']')) { + return value; + } + } + + // 上記以外は通常の JSON5 文字列として返す + return JSON5.stringify(value); +} + +/** + * 配列式の文字列表現を生成 + */ +function formatArrayForOutput(items: any[]): string { + return items.map(item => { + // i18n.ts. 参照の文字列はそのままJavaScript式として出力 + if (typeof item === 'string' && isI18nReference(item)) { + logger.info(`Preserving i18n reference in array: ${item}`); + return item; // クォートなしでそのまま + } + + // その他の値はJSON5形式で文字列化 + return JSON5.stringify(item); + }).join(', '); +} + +/** + * 要素ノードからテキスト内容を抽出する + * 各抽出方法を分離して可読性を向上 + */ +function extractElementText(node: VueAstNode): string | null { + if (!node) return null; + + logger.info(`Extracting text from node type=${node.type}, tag=${node.tag || 'unknown'}`); + + // 1. 直接コンテンツの抽出を試行 + const directContent = extractDirectContent(node); + if (directContent) return directContent; + + // 子要素がない場合は終了 + if (!node.children || !Array.isArray(node.children)) { + return null; + } + + // 2. インターポレーションノードを検索 + const interpolationContent = extractInterpolationContent(node.children); + if (interpolationContent) return interpolationContent; + + // 3. 式ノードを検索 + const expressionContent = extractExpressionContent(node.children); + if (expressionContent) return expressionContent; + + // 4. テキストノードを検索 + const textContent = extractTextContent(node.children); + if (textContent) return textContent; + + // 5. 再帰的に子ノードを探索 + return extractNestedContent(node.children); +} +/** + * ノードから直接コンテンツを抽出 + */ +function extractDirectContent(node: VueAstNode): string | null { + if (!node.content) return null; + + const content = typeof node.content === 'string' + ? node.content.trim() + : (node.content.content ? node.content.content.trim() : null); + + if (!content) return null; + + logger.info(`Direct node content found: ${content}`); + + // Mustache構文のチェック + const mustachePattern = /^\s*{{\s*(.*?)\s*}}\s*$/; + const mustacheMatch = content.match(mustachePattern); + + if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) { + const extractedContent = mustacheMatch[1].trim(); + logger.info(`Extracted i18n reference from mustache: ${extractedContent}`); + return extractedContent; + } + + // 直接i18n参照を含む場合 + if (isI18nReference(content)) { + logger.info(`Direct i18n reference found: ${content}`); + return content; + } + + // その他のコンテンツ + return content; +} + +/** + * インターポレーションノード(Mustache)からコンテンツを抽出 + */ +function extractInterpolationContent(children: VueAstNode[]): string | null { + for (const child of children) { + if (child.type === NODE_TYPES.INTERPOLATION) { + logger.info(`Found interpolation node (Mustache): ${JSON.stringify(child.content).substring(0, 100)}...`); + + if (child.content && child.content.type === 4 && child.content.content) { + const content = child.content.content.trim(); + logger.info(`Interpolation content: ${content}`); + + if (isI18nReference(content)) { + return content; + } + } else if (child.content && typeof child.content === 'object') { + // オブジェクト形式のcontentを探索 + logger.info(`Complex interpolation node: ${JSON.stringify(child.content).substring(0, 100)}...`); + + if (child.content.content) { + const content = child.content.content.trim(); + + if (isI18nReference(content)) { + logger.info(`Found i18n reference in complex interpolation: ${content}`); + return content; + } + } + } + } + } + + return null; +} + +/** + * 式ノードからコンテンツを抽出 + */ +function extractExpressionContent(children: VueAstNode[]): string | null { + // i18n.ts. 参照パターンを持つものを優先 + for (const child of children) { + if (child.type === NODE_TYPES.EXPRESSION && child.content) { + const expr = child.content.trim(); + + if (isI18nReference(expr)) { + logger.info(`Found i18n reference in expression node: ${expr}`); + return expr; + } + } + } + + // その他の式 + for (const child of children) { + if (child.type === NODE_TYPES.EXPRESSION && child.content) { + const expr = child.content.trim(); + logger.info(`Found expression: ${expr}`); + return expr; + } + } + + return null; +} + +/** + * テキストノードからコンテンツを抽出 + */ +function extractTextContent(children: VueAstNode[]): string | null { + for (const child of children) { + if (child.type === NODE_TYPES.TEXT && child.content) { + const text = child.content.trim(); + + if (text) { + logger.info(`Found text node: ${text}`); + + // Mustache構文のチェック + const mustachePattern = /^\s*{{\s*(.*?)\s*}}\s*$/; + const mustacheMatch = text.match(mustachePattern); + + if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) { + logger.info(`Extracted i18n ref from text mustache: ${mustacheMatch[1]}`); + return mustacheMatch[1].trim(); + } + + return text; + } + } + } + + return null; +} + +/** + * 子ノードを再帰的に探索してコンテンツを抽出 + */ +function extractNestedContent(children: VueAstNode[]): string | null { + for (const child of children) { + if (child.children && Array.isArray(child.children) && child.children.length > 0) { + const nestedContent = extractElementText(child); + + if (nestedContent) { + logger.info(`Found nested content: ${nestedContent}`); + return nestedContent; + } + } else if (child.type === NODE_TYPES.ELEMENT) { + // childrenがなくても内部を調査 + const nestedContent = extractElementText(child); + + if (nestedContent) { + logger.info(`Found content in childless element: ${nestedContent}`); + return nestedContent; + } + } + } + + return null; +} + + +/** + * SearchLabelとSearchKeywordを探して抽出する関数 + */ +function extractLabelsAndKeywords(nodes: VueAstNode[]): { label: string | null, keywords: any[] } { + let label: string | null = null; + const keywords: any[] = []; + + logger.info(`Extracting labels and keywords from ${nodes.length} nodes`); + + // 再帰的にSearchLabelとSearchKeywordを探索(ネストされたSearchMarkerは処理しない) + function findComponents(nodes: VueAstNode[]) { + for (const node of nodes) { + if (node.type === NODE_TYPES.ELEMENT) { + logger.info(`Checking element: ${node.tag}`); + + // SearchMarkerの場合は、その子要素は別スコープなのでスキップ + if (node.tag === 'SearchMarker') { + logger.info(`Found nested SearchMarker - skipping its content to maintain scope isolation`); + continue; // このSearchMarkerの中身は処理しない (スコープ分離) + } + + // SearchLabelの処理 + if (node.tag === 'SearchLabel') { + logger.info(`Found SearchLabel node, structure: ${JSON.stringify(node).substring(0, 200)}...`); + + // まず完全なノード内容の抽出を試みる + const content = extractElementText(node); + if (content) { + label = content; + logger.info(`SearchLabel content extracted: ${content}`); + } else { + logger.info(`SearchLabel found but extraction failed, trying direct children inspection`); + + // バックアップ: 子直接確認 - type=5のMustacheインターポレーションを重点的に確認 + if (node.children && Array.isArray(node.children)) { + for (const child of node.children) { + // Mustacheインターポレーション + if (child.type === NODE_TYPES.INTERPOLATION && child.content) { + // content内の式を取り出す + const expression = child.content.content || + (child.content.type === 4 ? child.content.content : null) || + JSON.stringify(child.content); + + logger.info(`Interpolation expression: ${expression}`); + if (typeof expression === 'string' && isI18nReference(expression)) { + label = expression.trim(); + logger.info(`Found i18n in interpolation: ${label}`); + break; + } + } + // 式ノード + else if (child.type === NODE_TYPES.EXPRESSION && child.content && isI18nReference(child.content)) { + label = child.content.trim(); + logger.info(`Found i18n in expression: ${label}`); + break; + } + // テキストノードでもMustache構文を探す + else if (child.type === NODE_TYPES.TEXT && child.content) { + const mustacheMatch = child.content.trim().match(/^\s*{{\s*(.*?)\s*}}\s*$/); + if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) { + label = mustacheMatch[1].trim(); + logger.info(`Found i18n in text mustache: ${label}`); + break; + } + } + } + } + } + } + // SearchKeywordの処理 + else if (node.tag === 'SearchKeyword') { + logger.info(`Found SearchKeyword node`); + + // まず完全なノード内容の抽出を試みる + const content = extractElementText(node); + if (content) { + keywords.push(content); + logger.info(`SearchKeyword content extracted: ${content}`); + } else { + logger.info(`SearchKeyword found but extraction failed, trying direct children inspection`); + + // バックアップ: 子直接確認 - type=5のMustacheインターポレーションを重点的に確認 + if (node.children && Array.isArray(node.children)) { + for (const child of node.children) { + // Mustacheインターポレーション + if (child.type === NODE_TYPES.INTERPOLATION && child.content) { + // content内の式を取り出す + const expression = child.content.content || + (child.content.type === 4 ? child.content.content : null) || + JSON.stringify(child.content); + + logger.info(`Keyword interpolation: ${expression}`); + if (typeof expression === 'string' && isI18nReference(expression)) { + const keyword = expression.trim(); + keywords.push(keyword); + logger.info(`Found i18n keyword in interpolation: ${keyword}`); + break; + } + } + // 式ノード + else if (child.type === NODE_TYPES.EXPRESSION && child.content && isI18nReference(child.content)) { + const keyword = child.content.trim(); + keywords.push(keyword); + logger.info(`Found i18n keyword in expression: ${keyword}`); + break; + } + // テキストノードでもMustache構文を探す + else if (child.type === NODE_TYPES.TEXT && child.content) { + const mustacheMatch = child.content.trim().match(/^\s*{{\s*(.*?)\s*}}\s*$/); + if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) { + const keyword = mustacheMatch[1].trim(); + keywords.push(keyword); + logger.info(`Found i18n keyword in text mustache: ${keyword}`); + break; + } + } + } + } + } + } + + // 子要素を再帰的に調査(ただしSearchMarkerは除外) + if (node.children && Array.isArray(node.children)) { + findComponents(node.children); + } + } + } + } + + findComponents(nodes); + + // デバッグ情報 + logger.info(`Extraction completed: label=${label}, keywords=[${keywords.join(', ')}]`); + return { label, keywords }; +} + + +function extractUsageInfoFromTemplateAst( + templateAst: any, + id: string, +): SearchIndexItem[] { + const allMarkers: SearchIndexItem[] = []; + const markerMap = new Map(); + const childrenIds = new Set(); + const normalizedId = id.replace(/\\/g, '/'); + + if (!templateAst) return allMarkers; + + // マーカーの基本情報を収集 + function collectMarkers(node: VueAstNode, parentId: string | null = null) { + if (node.type === 1 && node.tag === 'SearchMarker') { + // マーカーID取得 + const markerIdProp = node.props?.find((p: any) => p.name === 'markerId'); + const markerId = markerIdProp?.value?.content || + node.__markerId; + + // SearchMarkerにマーカーIDがない場合はエラー + if (markerId == null) { + logger.error(`Marker ID not found for node: ${JSON.stringify(node)}`); + throw new Error(`Marker ID not found in file ${id}`); + } + + // マーカー基本情報 + const markerInfo: SearchIndexItem = { + id: markerId, + children: [], + label: '', // デフォルト値 + keywords: [], + }; + + // 静的プロパティを取得 + if (node.props && Array.isArray(node.props)) { + for (const prop of node.props) { + if (prop.type === 6 && prop.name && prop.name !== 'markerId') { + if (prop.name === 'path') markerInfo.path = prop.value?.content || ''; + else if (prop.name === 'icon') markerInfo.icon = prop.value?.content || ''; + else if (prop.name === 'label') markerInfo.label = prop.value?.content || ''; + } + } + } + + // バインドプロパティを取得 + const bindings = extractNodeBindings(node); + if (bindings.path) markerInfo.path = bindings.path; + if (bindings.icon) markerInfo.icon = bindings.icon; + if (bindings.label) markerInfo.label = bindings.label; + if (bindings.children) markerInfo.children = bindings.children; + if (bindings.inlining) { + markerInfo.inlining = bindings.inlining; + logger.info(`Added inlining ${JSON.stringify(bindings.inlining)} to marker ${markerId}`); + } + if (bindings.keywords) { + if (Array.isArray(bindings.keywords)) { + markerInfo.keywords = bindings.keywords; + } else { + markerInfo.keywords = bindings.keywords || []; + } + } + + //pathがない場合はファイルパスを設定 + if (markerInfo.path == null && parentId == null) { + markerInfo.path = normalizedId.match(/.*(\/(admin|settings)\/[^\/]+)\.vue$/)?.[1]; + } + + // SearchLabelとSearchKeywordを抽出 (AST全体を探索) + if (node.children && Array.isArray(node.children)) { + logger.info(`Processing marker ${markerId} for labels and keywords`); + const extracted = extractLabelsAndKeywords(node.children); + + // SearchLabelからのラベル取得は最優先で適用 + if (extracted.label) { + markerInfo.label = extracted.label; + logger.info(`Using extracted label for ${markerId}: ${extracted.label}`); + } else if (markerInfo.label) { + logger.info(`Using existing label for ${markerId}: ${markerInfo.label}`); + } else { + markerInfo.label = 'Unnamed marker'; + logger.info(`No label found for ${markerId}, using default`); + } + + // SearchKeywordからのキーワード取得を追加 + if (extracted.keywords.length > 0) { + const existingKeywords = Array.isArray(markerInfo.keywords) ? + [...markerInfo.keywords] : + (markerInfo.keywords ? [markerInfo.keywords] : []); + + // i18n参照のキーワードは最優先で追加 + const combinedKeywords = [...existingKeywords]; + for (const kw of extracted.keywords) { + combinedKeywords.push(kw); + logger.info(`Added extracted keyword to ${markerId}: ${kw}`); + } + + markerInfo.keywords = combinedKeywords; + } + } + + // マーカーを登録 + markerMap.set(markerId, markerInfo); + allMarkers.push(markerInfo); + + // 親子関係を記録 + if (parentId) { + const parent = markerMap.get(parentId); + if (parent) { + childrenIds.add(markerId); + } + } + + // 子ノードを処理 + if (node.children && Array.isArray(node.children)) { + node.children.forEach((child: VueAstNode) => { + collectMarkers(child, markerId); + }); + } + + return markerId; + } + // SearchMarkerでない場合は再帰的に子ノードを処理 + else if (node.children && Array.isArray(node.children)) { + node.children.forEach((child: VueAstNode) => { + collectMarkers(child, parentId); + }); + } + + return null; + } + + // AST解析開始 + collectMarkers(templateAst); + return allMarkers; +} + +// バインドプロパティの処理を修正する関数 +function extractNodeBindings(node: VueAstNode): Record { + const bindings: Record = {}; + + if (!node.props || !Array.isArray(node.props)) return bindings; + + // バインド式を収集 + for (const prop of node.props) { + if (prop.type === 7 && prop.name === 'bind' && prop.arg?.content) { + const propName = prop.arg.content; + const propContent = prop.exp?.content || ''; + + logger.info(`Processing bind prop ${propName}: ${propContent}`); + + // inliningプロパティの処理を追加 + if (propName === 'inlining') { + try { + const content = propContent.trim(); + + // 配列式の場合 + if (content.startsWith('[') && content.endsWith(']')) { + // 配列要素を解析 + const elements = parseArrayExpression(content); + if (elements.length > 0) { + bindings.inlining = elements; + logger.info(`Parsed inlining array: ${JSON5.stringify(elements)}`); + } else { + bindings.inlining = []; + } + } + // 文字列の場合は配列に変換 + else if (content) { + bindings.inlining = [content]; // 単一の値を配列に + logger.info(`Converting inlining to array: [${content}]`); + } + } catch (e) { + logger.error(`Failed to parse inlining binding: ${propContent}`, e); + } + } + // keywordsの特殊処理 + if (propName === 'keywords') { + try { + const content = propContent.trim(); + + // 配列式の場合 + if (content.startsWith('[') && content.endsWith(']')) { + // i18n参照や特殊な式を保持するため、各要素を個別に解析 + const elements = parseArrayExpression(content); + if (elements.length > 0) { + bindings.keywords = elements; + logger.info(`Parsed keywords array: ${JSON5.stringify(elements)}`); + } else { + bindings.keywords = []; + logger.info('Empty keywords array'); + } + } + // その他の式(非配列) + else if (content) { + bindings.keywords = content; // 式をそのまま保持 + logger.info(`Keeping keywords as expression: ${content}`); + } else { + bindings.keywords = []; + logger.info('No keywords provided'); + } + } catch (e) { + logger.error(`Failed to parse keywords binding: ${propContent}`, e); + // エラーが起きても何らかの値を設定 + bindings.keywords = propContent || []; + } + } + // その他のプロパティ + else if (propName === 'label') { + // ラベルの場合も式として保持 + bindings[propName] = propContent; + logger.info(`Set label from bind expression: ${propContent}`); + } + else { + bindings[propName] = propContent; + } + } + } + + return bindings; +} + +// 配列式をパースする補助関数(文字列リテラル処理を改善) +function parseArrayExpression(expr: string): any[] { + try { + // 単純なケースはJSON5でパースを試みる + return JSON5.parse(expr.replace(/'/g, '"')); + } catch (e) { + // 複雑なケース(i18n.ts.xxx などの式を含む場合)は手動パース + logger.info(`Complex array expression, trying manual parsing: ${expr}`); + + // "["と"]"を取り除く + const content = expr.substring(1, expr.length - 1).trim(); + if (!content) return []; + + const result: any[] = []; + let currentItem = ''; + let depth = 0; + let inString = false; + let stringChar = ''; + + // カンマで区切る(ただし文字列内や入れ子の配列内のカンマは無視) + for (let i = 0; i < content.length; i++) { + const char = content[i]; + + if (inString) { + if (char === stringChar && content[i - 1] !== '\\') { + inString = false; + } + currentItem += char; + } else if (char === '"' || char === "'") { + inString = true; + stringChar = char; + currentItem += char; + } else if (char === '[') { + depth++; + currentItem += char; + } else if (char === ']') { + depth--; + currentItem += char; + } else if (char === ',' && depth === 0) { + // 項目の区切りを検出 + const trimmed = currentItem.trim(); + + // 純粋な文字列リテラルの場合、実際の値に変換 + if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || + (trimmed.startsWith('"') && trimmed.endsWith('"'))) { + try { + result.push(JSON5.parse(trimmed)); + } catch (err) { + result.push(trimmed); + } + } else { + // それ以外の式はそのまま(i18n.ts.xxx など) + result.push(trimmed); + } + + currentItem = ''; + } else { + currentItem += char; + } + } + + // 最後の項目を処理 + if (currentItem.trim()) { + const trimmed = currentItem.trim(); + + // 純粋な文字列リテラルの場合、実際の値に変換 + if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || + (trimmed.startsWith('"') && trimmed.endsWith('"'))) { + try { + result.push(JSON5.parse(trimmed)); + } catch (err) { + result.push(trimmed); + } + } else { + // それ以外の式はそのまま(i18n.ts.xxx など) + result.push(trimmed); + } + } + + logger.info(`Parsed complex array expression: ${expr} -> ${JSON.stringify(result)}`); + return result; + } +} + +export async function analyzeVueProps(options: Options & { + transformedCodeCache: Record, +}): Promise { + initLogger(options); + + const allMarkers: SearchIndexItem[] = []; + + // 対象ファイルパスを glob で展開 + const filePaths = options.targetFilePaths.reduce((acc, filePathPattern) => { + const matchedFiles = glob.sync(filePathPattern); + return [...acc, ...matchedFiles]; + }, []); + + logger.info(`Found ${filePaths.length} matching files to analyze`); + + for (const filePath of filePaths) { + const absolutePath = path.join(process.cwd(), filePath); + const id = absolutePath.replace(/\\/g, '/'); // 絶対パスに変換 + const code = options.transformedCodeCache[id]; // options 経由でキャッシュ参照 + if (!code) { // キャッシュミスの場合 + logger.error(`Error: No cached code found for: ${id}.`); // エラーログ + throw new Error(`No cached code found for: ${id}.`); // エラーを投げる + } + + try { + const { descriptor, errors } = vueSfcParse(options.transformedCodeCache[id], { + filename: filePath, + }); + + if (errors.length > 0) { + logger.error(`Compile Error: ${filePath}, ${errors}`); + continue; // エラーが発生したファイルはスキップ + } + + const fileMarkers = extractUsageInfoFromTemplateAst(descriptor.template?.ast, id); + + if (fileMarkers && fileMarkers.length > 0) { + allMarkers.push(...fileMarkers); // すべてのマーカーを収集 + logger.info(`Successfully extracted ${fileMarkers.length} markers from ${filePath}`); + } else { + logger.info(`No markers found in ${filePath}`); + } + } catch (error) { + logger.error(`Error analyzing file ${filePath}:`, error); + } + } + + // 収集したすべてのマーカー情報を使用 + const analysisResult: AnalysisResult[] = [ + { + filePath: "combined-markers", // すべてのファイルのマーカーを1つのエントリとして扱う + usage: allMarkers, + } + ]; + + outputAnalysisResultAsTS(options.exportFilePath, analysisResult); // すべてのマーカー情報を渡す +} + +interface MarkerRelation { + parentId?: string; + markerId: string; + node: VueAstNode; +} + +async function processVueFile( + code: string, + id: string, + options: Options, + transformedCodeCache: Record +): Promise<{ + code: string, + map: any, + transformedCodeCache: Record +}> { + const normalizedId = id.replace(/\\/g, '/'); // ファイルパスを正規化 + // すでにキャッシュに存在する場合は、そのまま返す + if (transformedCodeCache[normalizedId] && transformedCodeCache[normalizedId].includes('markerId=')) { + logger.info(`Using cached version for ${id}`); + return { + code: transformedCodeCache[normalizedId], + map: null, + transformedCodeCache + }; + } + + const s = new MagicString(code); // magic-string のインスタンスを作成 + const parsed = vueSfcParse(code, { filename: id }); + if (!parsed.descriptor.template) { + return { + code, + map: null, + transformedCodeCache + }; + } + const ast = parsed.descriptor.template.ast; // テンプレート AST を取得 + const markerRelations: MarkerRelation[] = []; // MarkerRelation 配列を初期化 + + if (ast) { + function traverse(node: any, currentParent?: any) { + if (node.type === 1 && node.tag === 'SearchMarker') { + // 行番号はコード先頭からの改行数で取得 + const lineNumber = code.slice(0, node.loc.start.offset).split('\n').length; + // ファイルパスと行番号からハッシュ値を生成 + // この際実行環境で差が出ないようにファイルパスを正規化 + const idKey = id.replace(/\\/g, '/').split('packages/frontend/')[1] + const generatedMarkerId = toBase62(hash(`${idKey}:${lineNumber}`)); + + const props = node.props || []; + const hasMarkerIdProp = props.some((prop: any) => prop.type === 6 && prop.name === 'markerId'); + const nodeMarkerId = hasMarkerIdProp + ? props.find((prop: any) => prop.type === 6 && prop.name === 'markerId')?.value?.content as string + : generatedMarkerId; + node.__markerId = nodeMarkerId; + + // 子マーカーの場合、親ノードに __children を設定しておく + if (currentParent && currentParent.type === 1 && currentParent.tag === 'SearchMarker') { + currentParent.__children = currentParent.__children || []; + currentParent.__children.push(nodeMarkerId); + } + + const parentMarkerId = currentParent && currentParent.__markerId; + markerRelations.push({ + parentId: parentMarkerId, + markerId: nodeMarkerId, + node: node, + }); + + if (!hasMarkerIdProp) { + const nodeStart = node.loc.start.offset; + let endOfStartTag; + + if (node.children && node.children.length > 0) { + // 子要素がある場合、最初の子要素の開始位置を基準にする + endOfStartTag = code.lastIndexOf('>', node.children[0].loc.start.offset); + } else if (node.loc.end.offset > nodeStart) { + // 子要素がない場合、自身の終了位置から逆算 + const nodeSource = code.substring(nodeStart, node.loc.end.offset); + // 自己終了タグか通常の終了タグかを判断 + if (nodeSource.includes('/>')) { + endOfStartTag = code.indexOf('/>', nodeStart) - 1; + } else { + endOfStartTag = code.indexOf('>', nodeStart); + } + } + + if (endOfStartTag !== undefined && endOfStartTag !== -1) { + // markerId が既に存在しないことを確認 + const tagText = code.substring(nodeStart, endOfStartTag + 1); + const markerIdRegex = /\s+markerId\s*=\s*["'][^"']*["']/; + + if (!markerIdRegex.test(tagText)) { + s.appendRight(endOfStartTag, ` markerId="${generatedMarkerId}" data-in-app-search-marker-id="${generatedMarkerId}"`); + logger.info(`Adding markerId="${generatedMarkerId}" to ${id}:${lineNumber}`); + } else { + logger.info(`markerId already exists in ${id}:${lineNumber}`); + } + } + } + } + + const newParent = node.type === 1 && node.tag === 'SearchMarker' ? node : currentParent; + if (node.children && Array.isArray(node.children)) { + node.children.forEach(child => traverse(child, newParent)); + } + } + + traverse(ast); // AST を traverse (1段階目: ID 生成と親子関係記録) + + // 2段階目: :children 属性の追加 + // 最初に親マーカーごとに子マーカーIDを集約する処理を追加 + const parentChildrenMap = new Map(); + + // 1. まず親ごとのすべての子マーカーIDを収集 + markerRelations.forEach(relation => { + if (relation.parentId) { + if (!parentChildrenMap.has(relation.parentId)) { + parentChildrenMap.set(relation.parentId, []); + } + parentChildrenMap.get(relation.parentId)?.push(relation.markerId); + } + }); + + // 2. 親ごとにまとめて :children 属性を処理 + for (const [parentId, childIds] of parentChildrenMap.entries()) { + const parentRelation = markerRelations.find(r => r.markerId === parentId); + if (!parentRelation || !parentRelation.node) continue; + + const parentNode = parentRelation.node; + const childrenProp = parentNode.props?.find((prop: any) => prop.type === 7 && prop.name === 'bind' && prop.arg?.content === 'children'); + + // 親ノードの開始位置を特定 + const parentNodeStart = parentNode.loc!.start.offset; + const endOfParentStartTag = parentNode.children && parentNode.children.length > 0 + ? code.lastIndexOf('>', parentNode.children[0].loc!.start.offset) + : code.indexOf('>', parentNodeStart); + + if (endOfParentStartTag === -1) continue; + + // 親タグのテキストを取得 + const parentTagText = code.substring(parentNodeStart, endOfParentStartTag + 1); + + if (childrenProp) { + // AST で :children 属性が検出された場合、それを更新 + try { + const childrenStart = code.indexOf('[', childrenProp.exp!.loc.start.offset); + const childrenEnd = code.indexOf(']', childrenProp.exp!.loc.start.offset); + if (childrenStart !== -1 && childrenEnd !== -1) { + const childrenArrayStr = code.slice(childrenStart, childrenEnd + 1); + let childrenArray = JSON5.parse(childrenArrayStr.replace(/'/g, '"')); + + // 新しいIDを追加(重複は除外) + const newIds = childIds.filter(id => !childrenArray.includes(id)); + if (newIds.length > 0) { + childrenArray = [...childrenArray, ...newIds]; + const updatedChildrenArrayStr = JSON5.stringify(childrenArray).replace(/"/g, "'"); + s.overwrite(childrenStart, childrenEnd + 1, updatedChildrenArrayStr); + logger.info(`Added ${newIds.length} child markerIds to existing :children in ${id}`); + } + } + } catch (e) { + logger.error('Error updating :children attribute:', e); + } + } else { + // AST では検出されなかった場合、タグテキストを調べる + const childrenRegex = /:children\s*=\s*["']\[(.*?)\]["']/; + const childrenMatch = parentTagText.match(childrenRegex); + + if (childrenMatch) { + // テキストから :children 属性値を解析して更新 + try { + const childrenContent = childrenMatch[1]; + const childrenArrayStr = `[${childrenContent}]`; + const childrenArray = JSON5.parse(childrenArrayStr.replace(/'/g, '"')); + + // 新しいIDを追加(重複は除外) + const newIds = childIds.filter(id => !childrenArray.includes(id)); + if (newIds.length > 0) { + childrenArray.push(...newIds); + + // :children="[...]" の位置を特定して上書き + const attrStart = parentTagText.indexOf(':children='); + if (attrStart > -1) { + const attrValueStart = parentTagText.indexOf('[', attrStart); + const attrValueEnd = parentTagText.indexOf(']', attrValueStart) + 1; + if (attrValueStart > -1 && attrValueEnd > -1) { + const absoluteStart = parentNodeStart + attrValueStart; + const absoluteEnd = parentNodeStart + attrValueEnd; + const updatedArrayStr = JSON5.stringify(childrenArray).replace(/"/g, "'"); + s.overwrite(absoluteStart, absoluteEnd, updatedArrayStr); + logger.info(`Updated existing :children in tag text for ${id}`); + } + } + } + } catch (e) { + logger.error('Error updating :children in tag text:', e); + } + } else { + // :children 属性がまだない場合、新規作成 + s.appendRight(endOfParentStartTag, ` :children="${JSON5.stringify(childIds).replace(/"/g, "'")}"`); + logger.info(`Created new :children attribute with ${childIds.length} markerIds in ${id}`); + } + } + } + } + + const transformedCode = s.toString(); // 変換後のコードを取得 + transformedCodeCache[normalizedId] = transformedCode; // 変換後のコードをキャッシュに保存 + + return { + code: transformedCode, // 変更後のコードを返す + map: s.generateMap({ source: id, includeContent: true }), // ソースマップも生成 (sourceMap: true が必要) + transformedCodeCache // キャッシュも返す + }; +} + + +// Rollup プラグインとして export +export default function pluginCreateSearchIndex(options: Options): Plugin { + let transformedCodeCache: Record = {}; // キャッシュオブジェクトをプラグインスコープで定義 + const isDevServer = process.env.NODE_ENV === 'development'; // 開発サーバーかどうか + + initLogger(options); // ロガーを初期化 + + return { + name: 'createSearchIndex', + enforce: 'pre', + + async buildStart() { + if (!isDevServer) { + return; + } + + const filePaths = options.targetFilePaths.reduce((acc, filePathPattern) => { + const matchedFiles = glob.sync(filePathPattern); + return [...acc, ...matchedFiles]; + }, []); + + for (const filePath of filePaths) { + const id = path.resolve(filePath); // 絶対パスに変換 + const code = fs.readFileSync(filePath, 'utf-8'); // ファイル内容を読み込む + const { transformedCodeCache: newCache } = await processVueFile(code, id, options, transformedCodeCache); // processVueFile 関数を呼び出す + transformedCodeCache = newCache; // キャッシュを更新 + } + + await analyzeVueProps({ ...options, transformedCodeCache }); // 開発サーバー起動時にも analyzeVueProps を実行 + }, + + async transform(code, id) { + if (!id.endsWith('.vue')) { + return; + } + + // targetFilePaths にマッチするファイルのみ処理を行う + // glob パターンでマッチング + let isMatch = false; // isMatch の初期値を false に設定 + for (const pattern of options.targetFilePaths) { // パターンごとにマッチング確認 + const globbedFiles = glob.sync(pattern); + for (const globbedFile of globbedFiles) { + const normalizedGlobbedFile = path.resolve(globbedFile); // glob 結果を絶対パスに + const normalizedId = path.resolve(id); // id を絶対パスに + if (normalizedGlobbedFile === normalizedId) { // 絶対パス同士で比較 + isMatch = true; + break; // マッチしたらループを抜ける + } + } + if (isMatch) break; // いずれかのパターンでマッチしたら、outer loop も抜ける + } + + + if (!isMatch) { + return; + } + + const transformed = await processVueFile(code, id, options, transformedCodeCache); + transformedCodeCache = transformed.transformedCodeCache; // キャッシュを更新 + if (isDevServer) { + await analyzeVueProps({ ...options, transformedCodeCache }); // analyzeVueProps を呼び出す + } + return transformed; + }, + + async writeBundle() { + await analyzeVueProps({ ...options, transformedCodeCache }); // ビルド時にも analyzeVueProps を実行 + }, + }; +} + +// i18n参照を検出するためのヘルパー関数を追加 +function isI18nReference(text: string | null | undefined): boolean { + if (!text) return false; + // ドット記法(i18n.ts.something) + const dotPattern = /i18n\.ts\.\w+/; + // ブラケット記法(i18n.ts['something']) + const bracketPattern = /i18n\.ts\[['"][^'"]+['"]\]/; + return dotPattern.test(text) || bracketPattern.test(text); +} diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 2bf7728d0..88b57e57a 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -51,6 +51,7 @@ "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", "json5": "2.2.3", + "magic-string": "0.30.17", "matter-js": "0.20.0", "mfm-js": "0.24.0", "misskey-bubble-game": "workspace:*", @@ -72,30 +73,30 @@ "typescript": "5.8.2", "uuid": "11.1.0", "v-code-diff": "1.13.1", - "vite": "6.2.0", + "vite": "6.2.1", "vue": "3.5.13", "vuedraggable": "next" }, "devDependencies": { "@misskey-dev/summaly": "5.2.0", - "@storybook/addon-actions": "8.6.3", - "@storybook/addon-essentials": "8.6.3", - "@storybook/addon-interactions": "8.6.3", - "@storybook/addon-links": "8.6.3", - "@storybook/addon-mdx-gfm": "8.6.3", - "@storybook/addon-storysource": "8.6.3", - "@storybook/blocks": "8.6.3", - "@storybook/components": "8.6.3", - "@storybook/core-events": "8.6.3", - "@storybook/manager-api": "8.6.3", - "@storybook/preview-api": "8.6.3", - "@storybook/react": "8.6.3", - "@storybook/react-vite": "8.6.3", - "@storybook/test": "8.6.3", - "@storybook/theming": "8.6.3", - "@storybook/types": "8.6.3", - "@storybook/vue3": "8.6.3", - "@storybook/vue3-vite": "8.6.3", + "@storybook/addon-actions": "8.6.4", + "@storybook/addon-essentials": "8.6.4", + "@storybook/addon-interactions": "8.6.4", + "@storybook/addon-links": "8.6.4", + "@storybook/addon-mdx-gfm": "8.6.4", + "@storybook/addon-storysource": "8.6.4", + "@storybook/blocks": "8.6.4", + "@storybook/components": "8.6.4", + "@storybook/core-events": "8.6.4", + "@storybook/manager-api": "8.6.4", + "@storybook/preview-api": "8.6.4", + "@storybook/react": "8.6.4", + "@storybook/react-vite": "8.6.4", + "@storybook/test": "8.6.4", + "@storybook/theming": "8.6.4", + "@storybook/types": "8.6.4", + "@storybook/vue3": "8.6.4", + "@storybook/vue3-vite": "8.6.4", "@testing-library/vue": "8.1.0", "@types/canvas-confetti": "1.9.0", "@types/estree": "1.0.6", @@ -110,15 +111,15 @@ "@types/ws": "8.18.0", "@typescript-eslint/eslint-plugin": "8.26.0", "@typescript-eslint/parser": "8.26.0", - "@vitest/coverage-v8": "3.0.7", + "@vitest/coverage-v8": "3.0.8", "@vue/runtime-core": "3.5.13", - "acorn": "8.14.0", + "acorn": "8.14.1", "cross-env": "7.0.3", "cypress": "14.1.0", "eslint-plugin-import": "2.31.0", - "eslint-plugin-vue": "9.33.0", + "eslint-plugin-vue": "10.0.0", "fast-glob": "3.3.3", - "happy-dom": "17.2.2", + "happy-dom": "17.3.0", "intersection-observer": "0.12.2", "micromatch": "4.0.8", "msw": "2.7.3", @@ -129,13 +130,13 @@ "react-dom": "19.0.0", "seedrandom": "3.0.5", "start-server-and-test": "2.0.10", - "storybook": "8.6.3", + "storybook": "8.6.4", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "vite-plugin-turbosnap": "1.0.3", - "vitest": "3.0.7", + "vitest": "3.0.8", "vitest-fetch-mock": "0.4.5", "vue-component-type-helpers": "2.2.8", - "vue-eslint-parser": "9.4.3", + "vue-eslint-parser": "10.1.1", "vue-tsc": "2.2.8" } } diff --git a/idea/MkDisableSection.vue b/packages/frontend/src/components/MkDisableSection.vue similarity index 86% rename from idea/MkDisableSection.vue rename to packages/frontend/src/components/MkDisableSection.vue index 360705071..bd7ecf225 100644 --- a/idea/MkDisableSection.vue +++ b/packages/frontend/src/components/MkDisableSection.vue @@ -24,7 +24,8 @@ defineProps<{ } .disabled { - opacity: 0.7; + opacity: 0.3; + filter: saturate(0.5); } .cover { @@ -34,7 +35,7 @@ defineProps<{ width: 100%; height: 100%; cursor: not-allowed; - --color: color(from var(--MI_THEME-error) srgb r g b / 0.25); + --color: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05)); background-size: auto auto; background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px); } diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index e725d2a15..c3fc1961e 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -91,6 +91,14 @@ const buttonsRight = computed(() => { }); const reloadCount = ref(0); +function getSearchMarker(path: string) { + const hash = path.split('#')[1]; + if (hash == null) return null; + return hash; +} + +const searchMarkerId = ref(getSearchMarker(props.initialPath)); + windowRouter.addListener('push', ctx => { history.value.push({ path: ctx.path, key: ctx.key }); }); @@ -101,7 +109,8 @@ windowRouter.addListener('replace', ctx => { }); windowRouter.addListener('change', ctx => { - console.log('windowRouter: change', ctx.path); + if (_DEV_) console.log('windowRouter: change', ctx.path); + searchMarkerId.value = getSearchMarker(ctx.path); analytics.page({ path: ctx.path, title: ctx.path, @@ -111,6 +120,7 @@ windowRouter.addListener('change', ctx => { windowRouter.init(); provide('router', windowRouter); +provide('inAppSearchMarkerId', searchMarkerId); provideMetadataReceiver((metadataGetter) => { const info = metadataGetter(); pageMetadata.value = info; diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 397aa68ed..d8dec3aa2 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -4,27 +4,60 @@ SPDX-License-Identifier: AGPL-3.0-only --> @@ -58,10 +91,98 @@ export type SuperMenuDef = { diff --git a/packages/frontend/src/components/global/SearchKeyword.vue b/packages/frontend/src/components/global/SearchKeyword.vue new file mode 100644 index 000000000..27a284faf --- /dev/null +++ b/packages/frontend/src/components/global/SearchKeyword.vue @@ -0,0 +1,14 @@ + + + + + + + diff --git a/packages/frontend/src/components/global/SearchLabel.vue b/packages/frontend/src/components/global/SearchLabel.vue new file mode 100644 index 000000000..27a284faf --- /dev/null +++ b/packages/frontend/src/components/global/SearchLabel.vue @@ -0,0 +1,14 @@ + + + + + + + diff --git a/packages/frontend/src/components/global/SearchMarker.vue b/packages/frontend/src/components/global/SearchMarker.vue new file mode 100644 index 000000000..c5ec626cf --- /dev/null +++ b/packages/frontend/src/components/global/SearchMarker.vue @@ -0,0 +1,116 @@ + + + + + + + diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index 0252bf025..ebbad3e5b 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -3,8 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { App } from 'vue'; - import Mfm from './global/MkMfm.js'; import MkA from './global/MkA.vue'; import MkAcct from './global/MkAcct.vue'; @@ -26,6 +24,11 @@ import MkSpacer from './global/MkSpacer.vue'; import MkFooterSpacer from './global/MkFooterSpacer.vue'; import MkStickyContainer from './global/MkStickyContainer.vue'; import MkLazy from './global/MkLazy.vue'; +import SearchMarker from './global/SearchMarker.vue'; +import SearchLabel from './global/SearchLabel.vue'; +import SearchKeyword from './global/SearchKeyword.vue'; + +import type { App } from 'vue'; export default function(app: App) { for (const [key, value] of Object.entries(components)) { @@ -55,6 +58,9 @@ export const components = { MkFooterSpacer: MkFooterSpacer, MkStickyContainer: MkStickyContainer, MkLazy: MkLazy, + SearchMarker: SearchMarker, + SearchLabel: SearchLabel, + SearchKeyword: SearchKeyword, }; declare module '@vue/runtime-core' { @@ -80,5 +86,8 @@ declare module '@vue/runtime-core' { MkFooterSpacer: typeof MkFooterSpacer; MkStickyContainer: typeof MkStickyContainer; MkLazy: typeof MkLazy; + SearchMarker: typeof SearchMarker; + SearchLabel: typeof SearchLabel; + SearchKeyword: typeof SearchKeyword; } } diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index 776f59dda..806599e80 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -4,74 +4,82 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/pages/settings/appearance.vue b/packages/frontend/src/pages/settings/appearance.vue new file mode 100644 index 000000000..465c2a38c --- /dev/null +++ b/packages/frontend/src/pages/settings/appearance.vue @@ -0,0 +1,287 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue index 9fca306f9..79be2b9b1 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.vue @@ -4,44 +4,46 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index 5acbc5075..6b67a9a1a 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -4,118 +4,143 @@ SPDX-License-Identifier: AGPL-3.0-only -->