Skip to content
Open
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
15 changes: 14 additions & 1 deletion app/components/Package/InstallScripts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ import { getOutdatedTooltip, getVersionClass } from '~/utils/npm/outdated-depend

const props = defineProps<{
packageName: string
version: string
installScripts: {
scripts: ('preinstall' | 'install' | 'postinstall')[]
content?: Record<string, string>
npxDependencies: Record<string, string>
}
}>()

function getCodeLink(scriptContent: string): string {
const filePath = getInstallScriptFilePath(scriptContent)
return `/code/${props.packageName}/v/${props.version}/${filePath}`
}

const outdatedNpxDeps = useOutdatedDependencies(() => props.installScripts.npxDependencies)
const hasNpxDeps = computed(() => Object.keys(props.installScripts.npxDependencies).length > 0)
const sortedNpxDeps = computed(() => {
Expand All @@ -34,7 +40,14 @@ const isExpanded = shallowRef(false)
class="font-mono text-sm text-fg-subtle m-0 truncate focus:whitespace-normal focus:overflow-visible cursor-help rounded focus-visible:(outline-2 outline-accent outline-offset-2)"
:title="installScripts.content?.[scriptName]"
>
{{ installScripts.content?.[scriptName] || $t('package.install_scripts.script_label') }}
<NuxtLink
v-if="installScripts.content?.[scriptName]"
:to="getCodeLink(installScripts.content[scriptName])"
class="hover:text-fg transition-colors duration-200"
>
{{ installScripts.content[scriptName] }}
</NuxtLink>
<span v-else>{{ $t('package.install_scripts.script_label') }}</span>
</dd>
</div>
</dl>
Expand Down
1 change: 1 addition & 0 deletions app/pages/package/[...package].vue
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,7 @@ onKeyStroke(
<PackageInstallScripts
v-if="displayVersion?.installScripts"
:package-name="pkg.name"
:version="displayVersion.version"
:install-scripts="displayVersion.installScripts"
/>

Expand Down
34 changes: 34 additions & 0 deletions app/utils/install-scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,37 @@ export function extractInstallScriptsInfo(
npxDependencies: extractNpxDependencies(scripts),
}
}

/**
* Pattern to match scripts that are just `node <file-path>`
* Captures the file path (relative paths with alphanumeric chars, dots, hyphens, underscores, and slashes)
*/
const NODE_SCRIPT_PATTERN = /^node\s+([\w./-]+)$/

/**
* Get the file path for an install script link.
* - If the script is `node <file-path>`, returns that file path
* - Otherwise, returns 'package.json'
*
* @param scriptContent - The content of the script
* @returns The file path to link to in the code tab
*/
export function getInstallScriptFilePath(scriptContent: string): string {
const match = NODE_SCRIPT_PATTERN.exec(scriptContent)

if (match?.[1]) {
// Script is `node <file-path>`, link to that file
// Normalize path: strip leading ./
const filePath = match[1].replace(/^\.\//, '')

// Fall back to package.json if path contains navigational elements (the client-side routing can't handle these well)
if (filePath.includes('../') || filePath.includes('./')) {
return 'package.json'
}

return filePath
}

// Default: link to package.json
return 'package.json'
}
24 changes: 23 additions & 1 deletion test/unit/app/utils/install-scripts.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, expect, it } from 'vitest'
import { extractInstallScriptsInfo } from '../../../../app/utils/install-scripts'
import {
extractInstallScriptsInfo,
getInstallScriptFilePath,
} from '../../../../app/utils/install-scripts'

describe('extractInstallScriptsInfo', () => {
it('returns null when no install scripts exist', () => {
Expand Down Expand Up @@ -75,3 +78,22 @@ describe('extractInstallScriptsInfo', () => {
})
})
})

describe('getInstallScriptFilePath', () => {
it('returns file path when script is `node <file-path>`', () => {
expect(getInstallScriptFilePath('node scripts/postinstall.js')).toBe('scripts/postinstall.js')
})

it('returns package.json when script is not a simple node command', () => {
expect(getInstallScriptFilePath('npx prisma generate')).toBe('package.json')
})

it('strips leading ./ from relative paths', () => {
expect(getInstallScriptFilePath('node ./scripts/setup.js')).toBe('scripts/setup.js')
})

it('falls back to package.json for parent directory references', () => {
expect(getInstallScriptFilePath('node ../scripts/setup.js')).toBe('package.json')
expect(getInstallScriptFilePath('node ./scripts/../lib/setup.js')).toBe('package.json')
})
})
Loading