Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions app/components/ConnectorModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const { isConnected, isConnecting, npmUser, error, hasOperations, connect, disco

const tokenInput = shallowRef('')
const portInput = shallowRef('31415')
const copied = shallowRef(false)
const { copied, copy } = useClipboard({ copiedDuring: 2000 })

async function handleConnect() {
const port = Number.parseInt(portInput.value, 10) || 31415
Expand All @@ -26,11 +26,7 @@ function copyCommand() {
if (portInput.value !== '31415') {
command += ` --port ${portInput.value}`
}
navigator.clipboard.writeText(command)
copied.value = true
setTimeout(() => {
copied.value = false
}, 2000)
copy(command)
}

const selectedPM = useSelectedPackageManager()
Expand Down
6 changes: 2 additions & 4 deletions app/composables/useInstallCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,11 @@ export function useInstallCommand(
})

// Copy state
const copied = ref(false)
const { copied, copy } = useClipboard({ copiedDuring: 2000 })

async function copyInstallCommand() {
if (!fullInstallCommand.value) return
await navigator.clipboard.writeText(fullInstallCommand.value)
copied.value = true
setTimeout(() => (copied.value = false), 2000)
await copy(fullInstallCommand.value)
}

return {
Expand Down
3 changes: 2 additions & 1 deletion app/composables/usePackageAnalysis.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ModuleFormat, TypesStatus } from '#shared/utils/package-analysis'
import type { ModuleFormat, TypesStatus, CreatePackageInfo } from '#shared/utils/package-analysis'

export interface PackageAnalysisResponse {
package: string
Expand All @@ -9,6 +9,7 @@ export interface PackageAnalysisResponse {
node?: string
npm?: string
}
createPackage?: CreatePackageInfo
}

/**
Expand Down
327 changes: 312 additions & 15 deletions app/pages/[...package].vue

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions app/pages/code/[...path].vue
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,10 @@ function handleLineClick(lineNum: number, event: MouseEvent) {
}

// Copy link to current line(s)
async function copyPermalink() {
const { copied: permalinkCopied, copy: copyPermalink } = useClipboard({ copiedDuring: 2000 })
function copyPermalinkUrl() {
const url = new URL(window.location.href)
await navigator.clipboard.writeText(url.toString())
copyPermalink(url.toString())
}

// Canonical URL for this code page
Expand Down Expand Up @@ -373,9 +374,9 @@ useSeoMeta({
v-if="selectedLines"
type="button"
class="px-2 py-1 font-mono text-xs text-fg-muted bg-bg-subtle border border-border rounded hover:text-fg hover:border-border-hover transition-colors"
@click="copyPermalink"
@click="copyPermalinkUrl"
>
{{ $t('code.copy_link') }}
{{ permalinkCopied ? $t('common.copied') : $t('code.copy_link') }}
</button>
<a
:href="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`"
Expand Down
61 changes: 52 additions & 9 deletions app/utils/install-command.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,62 @@
import type { JsrPackageInfo } from '#shared/types/jsr'
import { getCreateShortName } from '#shared/utils/package-analysis'

// @unocss-include
export const packageManagers = [
{
id: 'npm',
label: 'npm',
action: 'install',
execute: 'npx',
executeLocal: 'npx',
executeRemote: 'npx',
create: 'npm create',
icon: 'i-simple-icons:npm',
},
{
id: 'pnpm',
label: 'pnpm',
action: 'add',
execute: 'pnpm dlx',
executeLocal: 'pnpm exec',
executeRemote: 'pnpm dlx',
create: 'pnpm create',
icon: 'i-simple-icons:pnpm',
},
{
id: 'yarn',
label: 'yarn',
action: 'add',
execute: 'yarn dlx',
executeLocal: 'yarn',
executeRemote: 'yarn dlx',
create: 'yarn create',
icon: 'i-simple-icons:yarn',
},
{ id: 'bun', label: 'bun', action: 'add', execute: 'bunx', icon: 'i-simple-icons:bun' },
{
id: 'bun',
label: 'bun',
action: 'add',
executeLocal: 'bunx',
executeRemote: 'bunx',
create: 'bun create',
icon: 'i-simple-icons:bun',
},
{
id: 'deno',
label: 'deno',
action: 'add',
execute: 'deno run',
executeLocal: 'deno run',
executeRemote: 'deno run',
create: 'deno run',
icon: 'i-simple-icons:deno',
},
{ id: 'vlt', label: 'vlt', action: 'install', execute: 'vlt x', icon: 'i-custom-vlt' },
{
id: 'vlt',
label: 'vlt',
action: 'install',
executeLocal: 'vlt x',
executeRemote: 'vlt x',
create: 'vlt x',
icon: 'i-custom-vlt',
},
] as const

export type PackageManagerId = (typeof packageManagers)[number]['id']
Expand Down Expand Up @@ -85,12 +110,30 @@ export function getInstallCommandParts(options: InstallCommandOptions): string[]
return [pm.label, pm.action, `${spec}${version}`]
}

export function getExecuteCommand(options: InstallCommandOptions): string {
export interface ExecuteCommandOptions extends InstallCommandOptions {
/** Whether this is a binary-only package (download & run vs local run) */
isBinaryOnly?: boolean
/** Whether this is a create-* package (uses shorthand create command) */
isCreatePackage?: boolean
}

export function getExecuteCommand(options: ExecuteCommandOptions): string {
return getExecuteCommandParts(options).join(' ')
}

export function getExecuteCommandParts(options: InstallCommandOptions): string[] {
export function getExecuteCommandParts(options: ExecuteCommandOptions): string[] {
const pm = packageManagers.find(p => p.id === options.packageManager)
if (!pm) return []
return [pm.execute, getPackageSpecifier(options)]

// For create-* packages, use the shorthand create command
if (options.isCreatePackage) {
const shortName = getCreateShortName(options.packageName)
if (shortName !== options.packageName) {
return [...pm.create.split(' '), shortName]
}
}

// Choose remote or local execute based on package type
const executeCmd = options.isBinaryOnly ? pm.executeRemote : pm.executeLocal
return [...executeCmd.split(' '), getPackageSpecifier(options)]
}
153 changes: 153 additions & 0 deletions app/utils/run-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import type { JsrPackageInfo } from '#shared/types/jsr'
import { getPackageSpecifier, packageManagers } from './install-command'
import type { PackageManagerId } from './install-command'

/**
* Metadata needed to determine if a package is binary-only.
*/
export interface PackageMetadata {
name: string
bin?: string | Record<string, string>
main?: string
module?: unknown
exports?: unknown
}

/**
* Determine if a package is "binary-only" (executable without library entry points).
* Binary-only packages should show execute commands without install commands.
*
* A package is binary-only if:
* - Name starts with "create-" (e.g., create-vite)
* - Scoped name contains "/create-" (e.g., @vue/create-app)
* - Has bin field but no main, module, or exports fields
*/
export function isBinaryOnlyPackage(pkg: PackageMetadata): boolean {
const baseName = pkg.name.startsWith('@') ? pkg.name.split('/')[1] : pkg.name

// Check create-* patterns
if (baseName?.startsWith('create-') || pkg.name.includes('/create-')) {
return true
}

// Has bin but no entry points
const hasBin =
pkg.bin !== undefined && (typeof pkg.bin === 'string' || Object.keys(pkg.bin).length > 0)
const hasEntryPoint = !!pkg.main || !!pkg.module || !!pkg.exports
Comment thread
43081j marked this conversation as resolved.

return hasBin && !hasEntryPoint
}

/**
* Check if a package uses the create-* naming convention.
*/
export function isCreatePackage(packageName: string): boolean {
const baseName = packageName.startsWith('@') ? packageName.split('/')[1] : packageName
return baseName?.startsWith('create-') || packageName.includes('/create-') || false
}

/**
* Information about executable commands provided by a package.
*/
export interface ExecutableInfo {
/** Primary command name (typically the package name or first bin key) */
primaryCommand: string
/** All available command names */
commands: string[]
/** Whether this package has any executables */
hasExecutable: boolean
}

/**
* Extract executable command information from a package's bin field.
* Handles both string format ("bin": "./cli.js") and object format ("bin": { "cmd": "./cli.js" }).
*/
export function getExecutableInfo(
packageName: string,
bin: string | Record<string, string> | undefined,
): ExecutableInfo {
if (!bin) {
return { primaryCommand: '', commands: [], hasExecutable: false }
}

// String format: package name becomes the command
if (typeof bin === 'string') {
return {
primaryCommand: packageName,
commands: [packageName],
hasExecutable: true,
}
}

// Object format: keys are command names
const commands = Object.keys(bin)
const firstCommand = commands[0]
if (!firstCommand) {
return { primaryCommand: '', commands: [], hasExecutable: false }
}

// Prefer command matching package name if it exists, otherwise use first
const baseName = packageName.startsWith('@') ? packageName.split('/')[1] : packageName
const primaryCommand = baseName && commands.includes(baseName) ? baseName : firstCommand

return {
primaryCommand,
commands,
hasExecutable: true,
}
}

export interface RunCommandOptions {
packageName: string
packageManager: PackageManagerId
version?: string | null
jsrInfo?: JsrPackageInfo | null
/** Specific command to run (for packages with multiple bin entries) */
command?: string
/** Whether this is a binary-only package (affects which execute command to use) */
isBinaryOnly?: boolean
}

/**
* Generate run command as an array of parts.
* First element is the package manager label (e.g., "pnpm"), rest are arguments.
* For example: ["pnpm", "exec", "eslint"] or ["pnpm", "dlx", "create-vite"]
*/
export function getRunCommandParts(options: RunCommandOptions): string[] {
const pm = packageManagers.find(p => p.id === options.packageManager)
if (!pm) return []

const spec = getPackageSpecifier(options)

// Choose execute command based on package type
const executeCmd = options.isBinaryOnly ? pm.executeRemote : pm.executeLocal
const executeParts = executeCmd.split(' ')

// For deno, always use the package specifier
if (options.packageManager === 'deno') {
return [...executeParts, spec]
}

// For local execute with specific command name different from package name
// e.g., `pnpm exec tsc` for typescript package
if (options.command && options.command !== options.packageName) {
const baseName = options.packageName.startsWith('@')
? options.packageName.split('/')[1]
: options.packageName
// If command matches base package name, use the package spec
if (options.command === baseName) {
return [...executeParts, spec]
}
// Otherwise use the command name directly
return [...executeParts, options.command]
}

return [...executeParts, spec]
}

/**
* Generate the full run command for a package.
*/
export function getRunCommand(options: RunCommandOptions): string {
return getRunCommandParts(options).join(' ')
}
8 changes: 8 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@
"copy_command": "Copy install command",
"view_types": "View {package}"
},
"create": {
"title": "Create new project",
"copy_command": "Copy create command"
},
"run": {
"title": "Run",
"locally": "Run locally"
},
"readme": {
"title": "Readme",
"no_readme": "No README available.",
Expand Down
8 changes: 8 additions & 0 deletions i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@
"copy_command": "复制安装命令",
"view_types": "查看 {package}"
},
"create": {
"title": "创建新项目",
"copy_command": "复制创建命令"
},
"run": {
"title": "运行",
"locally": "本地运行"
},
"readme": {
"title": "Readme",
"no_readme": "没有可用的 README。",
Expand Down
Loading
Loading