Conversation
Add an interactive URI generator component to the SIP002 page that lets users build Shadowsocks URIs with QR codes directly in the docs. Also fix typos, update outdated references, and modernize examples across documentation pages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8fd1f1a to
55d3022
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
82507e6 to
7f9ad04
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an interactive SIP002 URI + QR generator to the VitePress docs site, while upgrading the docs toolchain and modernizing several documentation pages/links.
Changes:
- Upgrade VitePress (and related toolchain deps) and add
qrcodefor client-side QR rendering. - Add a custom VitePress theme that globally registers a new
<SIP002Generator />Vue component, and embed it in the SIP002 page. - Update/modernize multiple docs pages (clean URLs, typo fixes, outdated install instructions, removed legacy Python section).
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
package.json |
Upgrades VitePress and adds qrcode dependency used by the generator. |
yarn.lock |
Lockfile refresh for VitePress upgrade + new dependencies (incl. qrcode). |
docs/.vitepress/theme/index.ts |
Introduces a custom theme to register SIP002Generator globally. |
docs/.vitepress/theme/components/SIP002Generator.vue |
Implements SIP002 URI + QR generation UI and copy-to-clipboard. |
docs/doc/sip002.md |
Embeds the new generator component on the SIP002 docs page. |
docs/.vitepress/config.ts |
Minor footer update (copyright range). |
docs/doc/sip023.md |
Updates internal link to extensionless route. |
docs/doc/sip022.md |
Fixes section numbering. |
docs/doc/sip008.md |
Fixes typos/grammar in transport guidance. |
docs/doc/getting-started.md |
Removes legacy Python implementation references; updates internal links. |
docs/doc/deploying.md |
Modernizes OS guidance and install commands; removes Python 2-era section. |
docs/doc/configs.md |
Updates cipher guidance, examples, and SIP002 references/links. |
docs/doc/advanced.md |
Updates kernel guidance and removes deprecated sysctl (tcp_tw_recycle). |
.github/workflows/ci.yml |
Adds CI workflow to build the docs site on PRs/pushes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| enhanceApp({ app }) { | ||
| app.component('SIP002Generator', SIP002Generator) |
There was a problem hiding this comment.
enhanceApp overrides the default theme’s enhanceApp hook, so any setup performed by DefaultTheme.enhanceApp will be skipped. Call DefaultTheme.enhanceApp?.(ctx) inside your enhanceApp implementation (and keep the same ctx signature) before registering the global component.
| enhanceApp({ app }) { | |
| app.component('SIP002Generator', SIP002Generator) | |
| enhanceApp(ctx) { | |
| DefaultTheme.enhanceApp?.(ctx) | |
| ctx.app.component('SIP002Generator', SIP002Generator) |
| function base64urlEncode(str: string): string { | ||
| const encoded = btoa(str) | ||
| return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') | ||
| } | ||
|
|
||
| const uri = computed(() => { | ||
| if (!password.value || !hostname.value || !port.value) return '' | ||
|
|
||
| let userinfo: string | ||
| if (isAead2022(method.value)) { | ||
| userinfo = encodeURIComponent(method.value) + ':' + encodeURIComponent(password.value) | ||
| } else { | ||
| userinfo = base64urlEncode(method.value + ':' + password.value) | ||
| } |
There was a problem hiding this comment.
base64urlEncode uses btoa, which is not available during VitePress SSR/build, and uri is computed during SSR render. This can cause yarn docs:build to fail with btoa is not defined. Use an SSR-safe base64 implementation (e.g., globalThis.btoa fallback to Buffer.from(...).toString('base64')) or gate base64 encoding behind a client-only check.
| function base64urlEncode(str: string): string { | ||
| const encoded = btoa(str) | ||
| return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') | ||
| } | ||
|
|
||
| const uri = computed(() => { | ||
| if (!password.value || !hostname.value || !port.value) return '' | ||
|
|
||
| let userinfo: string | ||
| if (isAead2022(method.value)) { | ||
| userinfo = encodeURIComponent(method.value) + ':' + encodeURIComponent(password.value) | ||
| } else { | ||
| userinfo = base64urlEncode(method.value + ':' + password.value) | ||
| } |
There was a problem hiding this comment.
btoa(str) only supports Latin-1; if users enter a non-ASCII password/tag, this will throw and prevent URI generation. Consider base64-encoding UTF-8 bytes (e.g., via TextEncoder) before applying base64url transformations.
| onMounted(async () => { | ||
| const qr = await import('qrcode') | ||
| toDataURL = qr.toDataURL | ||
|
|
||
| watch(uri, async (val) => { | ||
| if (val && toDataURL) { | ||
| qrDataUrl.value = await toDataURL(val) | ||
| } else { |
There was a problem hiding this comment.
const qr = await import('qrcode') is likely to return a module namespace where the QRCode API is under default (CJS interop). Using qr.toDataURL can be undefined, breaking QR generation. Prefer const mod = await import('qrcode'); toDataURL = (mod.default ?? mod).toDataURL (or equivalent) to support both ESM/CJS shapes.
| await navigator.clipboard.writeText(uri.value) | ||
| copied.value = true | ||
| setTimeout(() => { copied.value = false }, 2000) |
There was a problem hiding this comment.
navigator.clipboard.writeText can throw/reject (permissions, non-secure context, unsupported browsers). Right now an exception will bubble and leave copied stuck false with an unhandled rejection. Wrap this in try/catch and provide a fallback (or at least a user-visible failure state).
| await navigator.clipboard.writeText(uri.value) | |
| copied.value = true | |
| setTimeout(() => { copied.value = false }, 2000) | |
| // Prefer modern Clipboard API when available | |
| if (typeof navigator !== 'undefined' && | |
| navigator.clipboard && | |
| typeof navigator.clipboard.writeText === 'function') { | |
| try { | |
| await navigator.clipboard.writeText(uri.value) | |
| copied.value = true | |
| setTimeout(() => { copied.value = false }, 2000) | |
| return | |
| } catch (e) { | |
| console.error('Failed to copy using navigator.clipboard:', e) | |
| // fall through to legacy fallback | |
| } | |
| } | |
| // Fallback: use a temporary textarea and document.execCommand('copy') | |
| try { | |
| const textarea = document.createElement('textarea') | |
| textarea.value = uri.value | |
| // Avoid scrolling to bottom | |
| textarea.style.position = 'fixed' | |
| textarea.style.top = '0' | |
| textarea.style.left = '0' | |
| textarea.style.width = '1px' | |
| textarea.style.height = '1px' | |
| textarea.style.padding = '0' | |
| textarea.style.border = 'none' | |
| textarea.style.outline = 'none' | |
| textarea.style.boxShadow = 'none' | |
| textarea.style.background = 'transparent' | |
| document.body.appendChild(textarea) | |
| textarea.focus() | |
| textarea.select() | |
| const successful = document.execCommand && document.execCommand('copy') | |
| document.body.removeChild(textarea) | |
| if (successful) { | |
| copied.value = true | |
| setTimeout(() => { copied.value = false }, 2000) | |
| return | |
| } | |
| } catch (e) { | |
| console.error('Failed to copy using fallback method:', e) | |
| } | |
| // If we reach here, copying failed; provide a visible failure state | |
| copied.value = false | |
| if (typeof window !== 'undefined') { | |
| alert('Failed to copy URI to clipboard. Please copy it manually.') | |
| } |
Summary
qrcodedependency for client-side QR code rendering (SSR-safe via dynamic import)SIP002GeneratorVue component globallyTest plan
yarn install && yarn docs:dev— component renders at/doc/sip002.html, form fields work, QR code generatesaes-128-gcm:test→YWVzLTEyOC1nY206dGVzdA)yarn docs:build— SSR build succeeds without errors🤖 Generated with Claude Code