-
Notifications
You must be signed in to change notification settings - Fork 2
feat(extension): ツールバーアイコンクリックで現在のGitHubリポジトリをPera1展開 #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
f7bbd94
feat(extension): open current GitHub repo in Pera1 from toolbar icon
kazuph 83f3d13
chore: sync pnpm-lock.yaml with package.json devDependencies
kazuph 4c23f5f
docs: add evidence report for chrome-extension feature
kazuph 51c0ce8
fix: address Copilot review comments on PR #3
kazuph File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| # chrome-extension / Chrome拡張ワンクリックPera1展開 | ||
|
|
||
| Created: 2026-06-12 | ||
| Branch: feature/chrome-extension | ||
| Status: Awaiting Review (Round 2) | ||
|
|
||
| ## 🔄 User Request ⇄ Response (修正依頼と対処) | ||
|
|
||
| | # | User Request (原文) | Response (対処内容) | 検証方法 | | ||
| |---|---------------------|---------------------|----------| | ||
| | 1 | 「この位置のボタン微妙。」(スクショ: リポジトリ名の下に折り返して表示) | `extension/content.js` の挿入先を「リポジトリ名の横」→「Watch/Fork/Starが並ぶ `ul.pagehead-actions` の先頭」に変更。高さもGitHub小ボタンと同じ28pxに統一 | E2E: `el.closest("ul.pagehead-actions")` でアクションエリア内に配置されることをアサート。ヘッダー部のライト/ダーク両モードのスクショを取得(下記Evidence) | | ||
|
|
||
| ### Round 2 ビフォーアフター | ||
|
|
||
| | Before(指摘時: タイトル下に折り返し) | After(Notifications/Fork/Starの並びに統合) | | ||
| |--------|-------| | ||
| | リポジトリ名の直下にボタンが落ちてレイアウト崩れ |  | | ||
| | (ユーザー提供スクショ参照) |  | | ||
|
|
||
| ## 📦 依頼内容(これは何のアウトプット?) | ||
|
|
||
| > 「この拡張機能を開発してほしいです。chromeの拡張を押した瞬間に、githubのURLをpera1のパラメーターにしてもらい、展開したやつを表示してほしいです。」 | ||
|
|
||
| → **ツールバーの拡張アイコンをクリックした瞬間**、現在タブのGitHub URLを `https://pera1.kazu-san.workers.dev/github.com/...` に変換し、新規タブでコード1ページ展開ビューを開くChrome拡張(Manifest V3)を実装しました。 | ||
|
|
||
| ## 📌 Attention Required(今回の確認項目) | ||
|
|
||
| | # | 確認ポイント | 前提・判断材料 | 質問 | | ||
| |---|------|---------------|------| | ||
| | 1 | **Pera1のドメインを `pera1.kazu-san.workers.dev` に統一した** | wrangler.toml にカスタムドメイン設定なし。Cloudflare APIでworkers.devサブドメインが `kazu-san` であることを確認し、実際に200応答することをcurlで検証済み。既存 `extension/content.js` が向いていた `pera1.pages.dev` は**DNSすら解決しない死にホスト**だった | この統一でOK?カスタムドメインの予定があれば教えて | | ||
| | 2 | **既存の `extension/` に統合**(新規ディレクトリを作らず) | リモートmainに既に content script型(ページ内ボタン挿入)の拡張があったため、そこへ action(アイコンクリック)機能を追加する形にした。ページ内ボタンも引き続き動作する | 統合方針でOK? | | ||
| | 3 | **Issues/PR等のページではリポジトリルートにフォールバック** | 例: `github.com/owner/repo/issues` でクリック → `pera1/github.com/owner/repo` を開く。GitHub以外のタブではバッジ「!」を2秒表示して何も開かない | この挙動でOK? | | ||
| | 4 | **アイコンを favicon.svg 由来に刷新** | 既存の icon48/128 はImageMagick製の「P1」文字プレースホルダーだった。`public/favicon.svg`(青丸+書類)から16/32/48/128pxを生成して統一 | 見た目OK? | | ||
|
|
||
| ## 🔀 変換仕様(クリック時の挙動) | ||
|
|
||
| ```mermaid | ||
| flowchart LR | ||
| A[拡張アイコンをクリック] --> B{現在タブのURL} | ||
| B -->|github.com/owner/repo| C[pera1/github.com/owner/repo] | ||
| B -->|.../tree/branch/dir| D[pera1/.../tree/branch/dir<br/>そのまま渡す] | ||
| B -->|.../blob/branch/file#L10| E[pera1/.../blob/branch/file<br/>ハッシュ・クエリは除去] | ||
| B -->|.../issues や /pulls 等| F[pera1/github.com/owner/repo<br/>ルートにフォールバック] | ||
| B -->|GitHub以外・gist等| G[何も開かずバッジ「!」を2秒表示] | ||
| C & D & E & F --> H[新規タブで1ページ展開ビュー] | ||
| ``` | ||
|
|
||
| ## 🛠 変更前 → 変更後 | ||
|
|
||
| | 項目 | Before | After | | ||
| |------|--------|-------| | ||
| | アイコンクリック | ❌ 何も起きない(actionなし) | ✅ **押した瞬間**Pera1の展開ビューが新規タブで開く | | ||
| | ページ内ボタンのリンク先 | ❌ `pera1.pages.dev`(DNS解決不可の死にホスト) | ✅ `pera1.kazu-san.workers.dev`(200応答を実測確認) | | ||
| | アイコン画像 | 「P1」文字のプレースホルダー(48/128のみ) | favicon.svg由来の統一デザイン(16/32/48/128) | | ||
| | URL変換テスト | なし | vitest 9ケース追加(モックなし純粋関数) | | ||
| | 拡張のE2E | なし | Playwright実ブラウザE2E(`tests/e2e/extension.e2e.mjs`) | | ||
|
|
||
| ## 🖼 Evidence | ||
|
|
||
| | ① GitHubリポジトリページ(ボタンはアクションエリアに配置) | ② アイコンクリック相当 → Pera1展開ビュー | | ||
| |--------|-------| | ||
| |  |  | | ||
|
|
||
| ①は「Go to Pera1」ボタンがNotifications/Fork/Starの並びに表示されている実GitHubページ。②はクリック後に開いた実Pera1(File Tree+全コード展開)。**モック・スタブなし**、実GitHub・実Workerに対するE2Eのスクショです。 | ||
|
|
||
| ### Test Results | ||
|
|
||
| ```bash | ||
| # ユニット(既存33 + 追加9 = 42件) | ||
| pnpm test | ||
| # Test Files 3 passed (3) | ||
| # Tests 42 passed (42) | ||
|
|
||
| # 型チェック | ||
| pnpm typecheck # エラーなし | ||
|
|
||
| # E2E(実Chromium + 実GitHub + 実Pera1 worker)×2回連続 | ||
| node tests/e2e/extension.e2e.mjs | ||
| # ✔ 拡張のservice workerがロードされた | ||
| # ✔ content scriptのボタン表示・リンク先OK | ||
| # ✔ Pera1タブが開きコードが展開された | ||
| # ✔ GitHub以外のタブでは何も開かない | ||
| # E2E ALL GREEN 🎉 | ||
| ``` | ||
|
|
||
| ### Verification Checklist | ||
| - [x] vitest 42件パス(既存テストも全て通過) | ||
| - [x] `tsc --noEmit` パス | ||
| - [x] Playwright E2Eを2回連続実行しflakeなし | ||
| - [x] `pera1.kazu-san.workers.dev` の死活をcurlで実測(200) | ||
| - [x] スクショを自分の目で確認 | ||
|
|
||
| <details> | ||
| <summary>📂 変更ファイル一覧(WHAT詳細)</summary> | ||
|
|
||
| | ファイル | 種別 | 内容 | | ||
| |---------|------|------| | ||
| | `extension/pera1-url.js` | 新規 | GitHub URL→Pera1 URL変換ロジック(tree/blob/raw対応、予約パス処理、クエリ・ハッシュ除去) | | ||
| | `extension/background.js` | 新規 | service worker。`chrome.action.onClicked` で変換→`chrome.tabs.create`。変換不可時はバッジ「!」 | | ||
| | `extension/manifest.json` | 変更 | v1.1.0。`action` / `background`(module) / `permissions:["activeTab"]` / icons 16〜128 を追加。content_scriptsは維持 | | ||
| | `extension/content.js` | 修正 | `PERA1_HOST` を死にホスト `pera1.pages.dev` → `pera1.kazu-san.workers.dev` | | ||
| | `extension/generate-icons.sh` | 変更 | favicon.svgからrsvg-convertで4サイズ生成する方式に刷新 | | ||
| | `extension/icon{16,32,48,128}.png` | 新規/差替 | favicon.svg由来のアイコン | | ||
| | `tests/unit/extension-url.spec.ts` | 新規 | 変換ロジックのvitestユニットテスト9件 | | ||
| | `tests/e2e/extension.e2e.mjs` | 新規 | 拡張実ロードのPlaywright E2E | | ||
|
|
||
| 権限は `activeTab` のみ(クリックした瞬間だけ現在タブのURLを読める最小権限)。`tabs` 権限や全サイトアクセスは要求しません。 | ||
|
|
||
| </details> | ||
|
|
||
| <details> | ||
| <summary>⚠️ 既知の制約</summary> | ||
|
|
||
| - ツールバーの実クリックそのものはPlaywrightから発火できない(Chromeの仕様)ため、E2Eでは `onClicked` に渡るtabオブジェクトと同形でハンドラを直接呼んで検証。実クリックは手元での読み込み確認を推奨: | ||
| 1. `chrome://extensions` → デベロッパーモードON → 「パッケージ化されていない拡張機能を読み込む」→ `extension/` を選択 | ||
| 2. GitHubの任意のリポジトリページでツールバーのPera1アイコンをクリック | ||
| - gist.github.com は Pera1 worker側が未対応のため対象外(バッジ「!」表示) | ||
|
|
||
| </details> | ||
|
|
||
| ### How to Reproduce | ||
| ```bash | ||
| pnpm install | ||
| pnpm test # ユニット42件 | ||
| node tests/e2e/extension.e2e.mjs # E2E(要playwright) | ||
| ``` |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { toPera1Url } from "./pera1-url.js"; | ||
|
|
||
| // 変換できないタブで押されたときに、バッジで「!」を短時間表示して知らせる | ||
| async function flashErrorBadge(tabId) { | ||
| await chrome.action.setBadgeBackgroundColor({ color: "#d73a49", tabId }); | ||
| await chrome.action.setBadgeText({ text: "!", tabId }); | ||
| setTimeout(() => { | ||
| chrome.action.setBadgeText({ text: "", tabId }).catch(() => {}); | ||
| }, 2000); | ||
| } | ||
|
|
||
| async function openInPera1(tab) { | ||
| const pera1Url = toPera1Url(tab?.url ?? ""); | ||
| if (!pera1Url) { | ||
| if (tab?.id != null) await flashErrorBadge(tab.id); | ||
| return null; | ||
| } | ||
| return chrome.tabs.create({ | ||
| url: pera1Url, | ||
| index: tab.index + 1, | ||
| openerTabId: tab.id, | ||
| }); | ||
| } | ||
|
|
||
| chrome.action.onClicked.addListener((tab) => { | ||
| openInPera1(tab); | ||
| }); | ||
|
Comment on lines
+27
to
+31
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ご指摘の通りです。51c0ce8 で onClicked リスナーに |
||
|
|
||
| // E2Eテスト(Playwright)からservice worker経由で呼び出すために公開 | ||
| globalThis.openInPera1 = openInPera1; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,15 @@ | ||
| #!/bin/bash | ||
| # Generate simple placeholder icons for the extension | ||
| # Requires ImageMagick (convert command) | ||
| # If not available, create the icons manually | ||
| # Generate extension icons from public/favicon.svg | ||
| # Requires rsvg-convert (brew install librsvg) | ||
| set -euo pipefail | ||
| cd "$(dirname "$0")/.." | ||
|
|
||
| if command -v magick &> /dev/null; then | ||
| magick -size 48x48 xc:'#0969da' -fill white -font Helvetica -pointsize 24 -gravity center -annotate 0 'P1' extension/icon48.png | ||
| magick -size 128x128 xc:'#0969da' -fill white -font Helvetica -pointsize 64 -gravity center -annotate 0 'P1' extension/icon128.png | ||
| echo "Icons generated!" | ||
| elif command -v convert &> /dev/null; then | ||
| convert -size 48x48 xc:'#0969da' -fill white -font Helvetica -pointsize 24 -gravity center -annotate 0 'P1' extension/icon48.png | ||
| convert -size 128x128 xc:'#0969da' -fill white -font Helvetica -pointsize 64 -gravity center -annotate 0 'P1' extension/icon128.png | ||
| echo "Icons generated!" | ||
| else | ||
| echo "ImageMagick not found. Please create icon48.png and icon128.png manually." | ||
| if ! command -v rsvg-convert &> /dev/null; then | ||
| echo "rsvg-convert not found. Install with: brew install librsvg" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| for size in 16 32 48 128; do | ||
| rsvg-convert -w "$size" -h "$size" public/favicon.svg -o "extension/icon${size}.png" | ||
| echo "Generated extension/icon${size}.png" | ||
| done |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| // GitHubのURLをPera1(1ページ展開ビュー)のURLへ変換するロジック。 | ||
| // background.js(service worker)とユニットテストの両方から import される。 | ||
|
|
||
| export const PERA1_BASE = "https://pera1.kazu-san.workers.dev"; | ||
|
|
||
| // github.com/<owner>/<repo>/<section> のうち、コード閲覧ではない予約セクション。 | ||
| // これらのページで押された場合はリポジトリルートの展開にフォールバックする。 | ||
| const NON_CODE_SECTIONS = new Set([ | ||
| "issues", | ||
| "pulls", | ||
| "pull", | ||
| "actions", | ||
| "projects", | ||
| "wiki", | ||
| "security", | ||
| "pulse", | ||
| "settings", | ||
| "releases", | ||
| "tags", | ||
| "branches", | ||
| "commits", | ||
| "commit", | ||
| "compare", | ||
| "discussions", | ||
| "milestones", | ||
| "labels", | ||
| "network", | ||
| "graphs", | ||
| "forks", | ||
| "stargazers", | ||
| "watchers", | ||
| "deployments", | ||
| "packages", | ||
| "activity", | ||
| ]); | ||
|
|
||
| // github.com/<first> がユーザー/Org名ではない予約パス。 | ||
| // この場合はリポジトリを特定できないので変換不可。 | ||
| const RESERVED_TOP_LEVEL = new Set([ | ||
| "orgs", | ||
| "organizations", | ||
| "settings", | ||
| "notifications", | ||
| "marketplace", | ||
| "explore", | ||
| "topics", | ||
| "trending", | ||
| "sponsors", | ||
| "login", | ||
| "logout", | ||
| "signup", | ||
| "features", | ||
| "about", | ||
| "pricing", | ||
| "search", | ||
| "codespaces", | ||
| "collections", | ||
| "events", | ||
| "new", | ||
| "apps", | ||
| "dashboard", | ||
| ]); | ||
|
|
||
| /** | ||
| * GitHubのURLをPera1のURLに変換する。 | ||
| * 変換できないURL(GitHub以外・リポジトリを特定できないページ)は null を返す。 | ||
| */ | ||
| export function toPera1Url(rawUrl) { | ||
| let url; | ||
| try { | ||
| url = new URL(rawUrl); | ||
| } catch { | ||
| return null; | ||
| } | ||
|
|
||
| if (url.hostname !== "github.com" && url.hostname !== "www.github.com") { | ||
| return null; | ||
| } | ||
|
|
||
| const segments = url.pathname.split("/").filter(Boolean); | ||
| if (segments.length < 2) return null; | ||
|
|
||
| const [owner, repo, section] = segments; | ||
| if (RESERVED_TOP_LEVEL.has(owner)) return null; | ||
|
|
||
| let path; | ||
| if (segments.length === 2) { | ||
| // リポジトリルート | ||
| path = `${owner}/${repo}`; | ||
| } else if (section === "tree" || section === "blob") { | ||
| // ブランチ/ディレクトリ/ファイル指定はPera1がそのまま解釈できる | ||
| path = segments.join("/"); | ||
| } else if (section === "raw" && segments.length >= 4) { | ||
| // raw表示はblob相当として扱う | ||
| path = [owner, repo, "blob", ...segments.slice(3)].join("/"); | ||
| } else if (NON_CODE_SECTIONS.has(section)) { | ||
| // Issues/PRs等のページからはリポジトリルートの展開にフォールバック | ||
| path = `${owner}/${repo}`; | ||
| } else { | ||
| // 未知のパスはそのまま渡す(Pera1側の "デフォルトブランチのdir指定" 形式) | ||
| path = segments.join("/"); | ||
| } | ||
|
|
||
| // GitHub側のクエリ(?tab=...等)やハッシュ(#L10等)はPera1には不要なので捨てる | ||
| return `${PERA1_BASE}/github.com/${path}`; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ご指摘の通りです。51c0ce8 で
index/openerTabIdを型ガードしました(typeof === "number" && >= 0のときのみ指定、それ以外は undefined)。E2Eで実ブラウザ動作も再確認済みです。