Skip to content

Commit 9e9d0df

Browse files
authored
fix(web): add url and npm plugin source types to marketplace schema (#114)
* fix(web): add url and npm plugin source types to marketplace schema - Added url and npm source types to Zod validation schemas (marketplace-schema.ts, content.config.ts) - Added corresponding TypeScript interfaces (marketplace.ts, PluginCard.vue) - Updated PluginCard.vue to handle url/npm sources for metadata fetch, source URL display, and source text - Fixed parseGitHubRepo to strip .git suffix from URLs - Fixed import.meta.env to process.env for GITHUB_TOKEN in server utils - Installed @iconify-json/simple-icons for icon collection * chore: apply AI code review suggestions - Add ref/sha fields to PluginSourceGitHub interface to fix TypeScript error - Restructure fetchPluginMetadata to ensure loading is always reset via finally block - Add sourceText computed property and simplify template ternary - Revert Node engine from 24.x to 22.x to match repo baseline - Use z.string().url() for stricter URL validation in content.config.ts
1 parent fcea948 commit 9e9d0df

7 files changed

Lines changed: 134 additions & 14 deletions

File tree

apps/web/app/components/PluginCard.vue

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import { useTimeoutFn } from '@vueuse/core'
44
interface PluginSourceGitHub {
55
source: 'github'
66
repo: string
7+
ref?: string
8+
sha?: string
9+
}
10+
11+
interface PluginSourceUrl {
12+
source: 'url'
13+
url: string
14+
ref?: string
15+
sha?: string
716
}
817
918
interface PluginSourceGitSubdir {
@@ -14,7 +23,14 @@ interface PluginSourceGitSubdir {
1423
sha?: string
1524
}
1625
17-
type PluginSource = PluginSourceGitHub | PluginSourceGitSubdir
26+
interface PluginSourceNpm {
27+
source: 'npm'
28+
package: string
29+
version?: string
30+
registry?: string
31+
}
32+
33+
type PluginSource = PluginSourceGitHub | PluginSourceUrl | PluginSourceGitSubdir | PluginSourceNpm
1834
1935
interface Plugin {
2036
name: string
@@ -60,22 +76,44 @@ async function fetchPluginMetadata() {
6076
return
6177
}
6278
63-
// Only GitHub source plugins support metadata fetch
64-
if (props.plugin.source.source !== 'github') {
79+
// Only GitHub and url source plugins support metadata fetch
80+
if (props.plugin.source.source !== 'github' && props.plugin.source.source !== 'url') {
6581
return
6682
}
6783
68-
// GitHub plugin - fetch from GitHub
84+
// Build raw URL based on source type
6985
loading.value = true
86+
let url: string | undefined
7087
try {
71-
const url = `https://raw.githubusercontent.com/${props.plugin.source.repo}/main/.claude-plugin/plugin.json`
88+
let repoPath: string | undefined
89+
let ref = 'main'
90+
91+
if (props.plugin.source.source === 'github') {
92+
repoPath = props.plugin.source.repo
93+
ref = props.plugin.source.ref || ref
94+
}
95+
else {
96+
// url source - try to construct raw GitHub URL
97+
const gitUrl = props.plugin.source.url.replace(/\.git$/, '')
98+
const match = gitUrl.match(/github\.com\/([^/]+\/[^/]+)/)
99+
if (match) {
100+
repoPath = match[1]
101+
ref = props.plugin.source.ref || ref
102+
}
103+
}
104+
105+
if (!repoPath) {
106+
return
107+
}
108+
109+
url = `https://raw.githubusercontent.com/${repoPath}/${ref}/.claude-plugin/plugin.json`
72110
const response = await fetch(url)
73111
74112
if (!response.ok) {
75113
// Log specific HTTP error
76114
console.error(`Failed to fetch plugin metadata: HTTP ${response.status}`, {
77115
plugin: props.plugin.name,
78-
repo: props.plugin.source.repo,
116+
url,
79117
status: response.status,
80118
statusText: response.statusText,
81119
})
@@ -107,7 +145,7 @@ async function fetchPluginMetadata() {
107145
// Network-level errors (connection failed, CORS, etc.)
108146
console.error('Network error fetching plugin metadata:', {
109147
plugin: props.plugin.name,
110-
repo: props.plugin.source.source === 'github' ? props.plugin.source.repo : props.plugin.source.url,
148+
url,
111149
error: err instanceof Error ? err.message : String(err),
112150
})
113151
}
@@ -184,6 +222,10 @@ const githubSourceUrl = computed(() => {
184222
185223
return url
186224
}
225+
// url plugin
226+
if (props.plugin.source.source === 'url') {
227+
return props.plugin.source.url.replace(/\.git$/, '')
228+
}
187229
// git-subdir plugin
188230
if (props.plugin.source.source === 'git-subdir') {
189231
const url = props.plugin.source.url
@@ -195,6 +237,11 @@ const githubSourceUrl = computed(() => {
195237
}
196238
return `https://github.com/${url}/tree/${ref}/${path}`
197239
}
240+
// npm plugin
241+
if (props.plugin.source.source === 'npm') {
242+
const registry = props.plugin.source.registry || 'https://www.npmjs.com/package'
243+
return `${registry}/${props.plugin.source.package}`
244+
}
198245
// GitHub plugin
199246
return `https://github.com/${props.plugin.source.repo}`
200247
})
@@ -285,6 +332,25 @@ const badges = computed<Badge[]>(() => {
285332
return badgeList
286333
})
287334
335+
// Computed source text for display
336+
const sourceText = computed(() => {
337+
const { source } = props.plugin
338+
if (typeof source === 'string')
339+
return source
340+
341+
switch (source.source) {
342+
case 'github':
343+
return source.repo
344+
case 'npm':
345+
return source.package
346+
case 'url':
347+
case 'git-subdir':
348+
return source.url
349+
default:
350+
return ''
351+
}
352+
})
353+
288354
// Fetch metadata on mount
289355
onMounted(() => {
290356
fetchPluginMetadata()
@@ -332,7 +398,7 @@ watch(() => props.autoOpenModal, (shouldOpen) => {
332398
</div>
333399
<div class="flex items-center gap-2 text-xs text-muted">
334400
<UIcon name="i-heroicons-code-bracket" class="shrink-0" />
335-
<span class="truncate">{{ typeof plugin.source === 'string' ? plugin.source : plugin.source.source === 'github' ? plugin.source.repo : plugin.source.url }}</span>
401+
<span class="truncate">{{ sourceText }}</span>
336402
</div>
337403
</div>
338404
<div class="shrink-0">

apps/web/app/types/marketplace.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ export interface MarketplaceSource {
1414
export interface PluginSourceGitHub {
1515
source: 'github'
1616
repo: string
17+
ref?: string
18+
sha?: string
19+
}
20+
21+
export interface PluginSourceUrl {
22+
source: 'url'
23+
url: string
24+
ref?: string
25+
sha?: string
1726
}
1827

1928
export interface PluginSourceGitSubdir {
@@ -24,7 +33,14 @@ export interface PluginSourceGitSubdir {
2433
sha?: string
2534
}
2635

27-
export type PluginSource = PluginSourceGitHub | PluginSourceGitSubdir
36+
export interface PluginSourceNpm {
37+
source: 'npm'
38+
package: string
39+
version?: string
40+
registry?: string
41+
}
42+
43+
export type PluginSource = PluginSourceGitHub | PluginSourceUrl | PluginSourceGitSubdir | PluginSourceNpm
2844

2945
export interface Plugin {
3046
name: string

apps/web/content.config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ export default defineContentConfig({
3737
z.object({
3838
source: z.literal('github'),
3939
repo: z.string(),
40+
ref: z.string().optional(),
41+
sha: z.string().optional(),
42+
}),
43+
z.object({
44+
source: z.literal('url'),
45+
url: z.string().url(),
46+
ref: z.string().optional(),
47+
sha: z.string().optional(),
4048
}),
4149
z.object({
4250
source: z.literal('git-subdir'),
@@ -45,6 +53,12 @@ export default defineContentConfig({
4553
ref: z.string().optional(),
4654
sha: z.string().optional(),
4755
}),
56+
z.object({
57+
source: z.literal('npm'),
58+
package: z.string(),
59+
version: z.string().optional(),
60+
registry: z.string().optional(),
61+
}),
4862
z.string(),
4963
]),
5064
}),

apps/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
},
2626
"devDependencies": {
2727
"@iconify-json/heroicons": "^1.2.3",
28+
"@iconify-json/simple-icons": "^1.2.75",
2829
"@nuxt/devtools": "latest",
2930
"@nuxt/eslint": "^1.15.2",
3031
"@nuxt/test-utils": "3.19.2",
3132
"@pleaseai/eslint-config": "workspace:*",
3233
"@pleaseai/typescript-config": "workspace:*",
34+
"baseline-browser-mapping": "^2.10.11",
3335
"eslint": "^10.0.2",
3436
"typescript": "^5.9.3"
3537
}

apps/web/server/utils/github.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import process from 'node:process'
2+
13
/**
24
* GitHub API utilities for fetching repository information
35
*/
@@ -16,12 +18,12 @@ interface GitHubRepo {
1618
*/
1719
export function parseGitHubRepo(source: string): { owner: string, repo: string } | null {
1820
try {
19-
// Handle URL format: https://github.com/owner/repo
21+
// Handle URL format: https://github.com/owner/repo or https://github.com/owner/repo.git
2022
if (source.startsWith('http')) {
2123
const url = new URL(source)
2224
const parts = url.pathname.split('/').filter(Boolean)
2325
if (parts.length >= 2) {
24-
return { owner: parts[0], repo: parts[1] }
26+
return { owner: parts[0], repo: parts[1].replace(/\.git$/, '') }
2527
}
2628
}
2729

@@ -54,7 +56,7 @@ export async function fetchGitHubStars(owner: string, repo: string): Promise<num
5456
'User-Agent': 'claude-code-plugins-marketplace',
5557
}
5658

57-
const githubToken = import.meta.env.GITHUB_TOKEN
59+
const githubToken = process.env.GITHUB_TOKEN
5860
if (githubToken) {
5961
headers.Authorization = `token ${githubToken}`
6062
}

apps/web/server/utils/marketplace-schema.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ export const pluginSourceSchema = z.union([
99
z.object({
1010
source: z.literal('github'),
1111
repo: z.string(),
12+
ref: z.string().optional(),
13+
sha: z.string().optional(),
14+
}),
15+
z.object({
16+
source: z.literal('url'),
17+
url: z.string(),
18+
ref: z.string().optional(),
19+
sha: z.string().optional(),
1220
}),
1321
z.object({
1422
source: z.literal('git-subdir'),
@@ -17,6 +25,12 @@ export const pluginSourceSchema = z.union([
1725
ref: z.string().optional(),
1826
sha: z.string().optional(),
1927
}),
28+
z.object({
29+
source: z.literal('npm'),
30+
package: z.string(),
31+
version: z.string().optional(),
32+
registry: z.string().optional(),
33+
}),
2034
z.string(),
2135
])
2236

bun.lock

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)