parseMarketplaceInput.ts
utils/plugins/parseMarketplaceInput.ts
No strong subsystem tag
163
Lines
6075
Bytes
1
Exports
5
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 163 lines, 5 detected imports, and 1 detected exports.
Important relationships
Detected exports
parseMarketplaceInput
Keywords
trimmedsourcepathrepogithuburlwithoutfragmentstartswithresolvedpathinputuser
Detected imports
ospath../errors.js../fsOperations.js./schemas.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import { homedir } from 'os'
import { resolve } from 'path'
import { getErrnoCode } from '../errors.js'
import { getFsImplementation } from '../fsOperations.js'
import type { MarketplaceSource } from './schemas.js'
/**
* Parses a marketplace input string and returns the appropriate marketplace source type.
* Handles various input formats:
* - Git SSH URLs (user@host:path or user@host:path.git)
* - Standard: git@github.com:owner/repo.git
* - GitHub Enterprise SSH certificates: org-123456@github.com:owner/repo.git
* - Custom usernames: deploy@gitlab.com:group/project.git
* - Self-hosted: user@192.168.10.123:path/to/repo
* - HTTP/HTTPS URLs
* - GitHub shorthand (owner/repo)
* - Local file paths (.json files)
* - Local directory paths
*
* @param input The marketplace source input string
* @returns MarketplaceSource object, error object, or null if format is unrecognized
*/
export async function parseMarketplaceInput(
input: string,
): Promise<MarketplaceSource | { error: string } | null> {
const trimmed = input.trim()
const fs = getFsImplementation()
// Handle git SSH URLs with any valid username (not just 'git')
// Supports: user@host:path, user@host:path.git, and with #ref suffix
// Username can contain: alphanumeric, dots, underscores, hyphens
const sshMatch = trimmed.match(
/^([a-zA-Z0-9._-]+@[^:]+:.+?(?:\.git)?)(#(.+))?$/,
)
if (sshMatch?.[1]) {
const url = sshMatch[1]
const ref = sshMatch[3]
return ref ? { source: 'git', url, ref } : { source: 'git', url }
}
// Handle URLs
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
// Extract fragment (ref) from URL if present
const fragmentMatch = trimmed.match(/^([^#]+)(#(.+))?$/)
const urlWithoutFragment = fragmentMatch?.[1] || trimmed
const ref = fragmentMatch?.[3]
// When user explicitly provides an HTTPS/HTTP URL that looks like a git
// repo, use the git source type so we clone rather than fetch-as-JSON.
// The .git suffix is a GitHub/GitLab/Bitbucket convention. Azure DevOps
// uses /_git/ in the path with NO suffix (appending .git breaks ADO:
// TF401019 "repo does not exist"). Without this check, an ADO URL falls
// through to source:'url' below, which tries to fetch it as a raw
// marketplace.json — the HTML response parses as "expected object,
// received string". (gh-31256 / CC-299)
if (
urlWithoutFragment.endsWith('.git') ||
urlWithoutFragment.includes('/_git/')
) {
return ref
? { source: 'git', url: urlWithoutFragment, ref }
: { source: 'git', url: urlWithoutFragment }
}
// Parse URL to check hostname
let url: URL
try {
url = new URL(urlWithoutFragment)
} catch (_err) {
// Not a valid URL for parsing, treat as generic URL
// new URL() throws TypeError for invalid URLs
return { source: 'url', url: urlWithoutFragment }
}
if (url.hostname === 'github.com' || url.hostname === 'www.github.com') {
const match = url.pathname.match(/^\/([^/]+\/[^/]+?)(\/|\.git|$)/)
if (match?.[1]) {
// User explicitly provided HTTPS URL - keep it as HTTPS via 'git' type
// Add .git suffix if not present for proper git clone
const gitUrl = urlWithoutFragment.endsWith('.git')
? urlWithoutFragment
: `${urlWithoutFragment}.git`
return ref
? { source: 'git', url: gitUrl, ref }
: { source: 'git', url: gitUrl }
}
}
return { source: 'url', url: urlWithoutFragment }
}
// Handle local paths
// On Windows, also recognize backslash-relative (.\, ..\) and drive letter paths (C:\)
// These are Windows-only because backslashes are valid filename chars on Unix
const isWindows = process.platform === 'win32'
const isWindowsPath =
isWindows &&
(trimmed.startsWith('.\\') ||
trimmed.startsWith('..\\') ||
/^[a-zA-Z]:[/\\]/.test(trimmed))
if (
trimmed.startsWith('./') ||
trimmed.startsWith('../') ||
trimmed.startsWith('/') ||
trimmed.startsWith('~') ||
isWindowsPath
) {
const resolvedPath = resolve(
trimmed.startsWith('~') ? trimmed.replace(/^~/, homedir()) : trimmed,
)
// Stat the path to determine if it's a file or directory. Swallow all stat
// errors (ENOENT, EACCES, EPERM, etc.) and return an error result instead
// of throwing — matches the old existsSync behavior which never threw.
let stats
try {
stats = await fs.stat(resolvedPath)
} catch (e: unknown) {
const code = getErrnoCode(e)
return {
error:
code === 'ENOENT'
? `Path does not exist: ${resolvedPath}`
: `Cannot access path: ${resolvedPath} (${code ?? e})`,
}
}
if (stats.isFile()) {
if (resolvedPath.endsWith('.json')) {
return { source: 'file', path: resolvedPath }
} else {
return {
error: `File path must point to a .json file (marketplace.json), but got: ${resolvedPath}`,
}
}
} else if (stats.isDirectory()) {
return { source: 'directory', path: resolvedPath }
} else {
return {
error: `Path is neither a file nor a directory: ${resolvedPath}`,
}
}
}
// Handle GitHub shorthand (owner/repo, owner/repo#ref, or owner/repo@ref)
// Accept both # and @ as ref separators — the display formatter uses @, so users
// naturally type @ when copying from error messages or managed settings.
if (trimmed.includes('/') && !trimmed.startsWith('@')) {
if (trimmed.includes(':')) {
return null
}
// Extract ref if present (either #ref or @ref)
const fragmentMatch = trimmed.match(/^([^#@]+)(?:[#@](.+))?$/)
const repo = fragmentMatch?.[1] || trimmed
const ref = fragmentMatch?.[2]
// Assume it's a GitHub repo
return ref ? { source: 'github', repo, ref } : { source: 'github', repo }
}
// NPM packages not yet implemented
// Returning null for unrecognized input
return null
}