Skip to content

Commit db689ea

Browse files
committed
fix: update to use new LinkBase, linkify only file path + update tests
1 parent 244ae26 commit db689ea

File tree

4 files changed

+102
-12
lines changed

4 files changed

+102
-12
lines changed

app/components/Package/InstallScripts.vue

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,29 @@ const props = defineProps<{
1111
}
1212
}>()
1313
14-
function getCodeLink(scriptContent: string): string {
15-
const filePath = getInstallScriptFilePath(scriptContent)
14+
function getCodeLink(filePath: string): string {
1615
return `/code/${props.packageName}/v/${props.version}/${filePath}`
1716
}
1817
18+
const scriptParts = computed(() => {
19+
const parts: Record<string, { prefix: string | null; filePath: string | null; link: string }> = {}
20+
for (const scriptName of props.installScripts.scripts) {
21+
const content = props.installScripts.content?.[scriptName]
22+
if (!content) continue
23+
const parsed = parseNodeScript(content)
24+
if (parsed) {
25+
parts[scriptName] = {
26+
prefix: parsed.prefix,
27+
filePath: parsed.filePath,
28+
link: getCodeLink(parsed.filePath),
29+
}
30+
} else {
31+
parts[scriptName] = { prefix: null, filePath: null, link: getCodeLink('package.json') }
32+
}
33+
}
34+
return parts
35+
})
36+
1937
const outdatedNpxDeps = useOutdatedDependencies(() => props.installScripts.npxDependencies)
2038
const hasNpxDeps = computed(() => Object.keys(props.installScripts.npxDependencies).length > 0)
2139
const sortedNpxDeps = computed(() => {
@@ -36,18 +54,23 @@ const isExpanded = shallowRef(false)
3654
<div v-for="scriptName in installScripts.scripts" :key="scriptName">
3755
<dt class="font-mono text-xs text-fg-muted">{{ scriptName }}</dt>
3856
<dd
39-
tabindex="0"
40-
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)"
57+
class="font-mono text-sm text-fg-subtle m-0 truncate"
4158
:title="installScripts.content?.[scriptName]"
4259
>
43-
<NuxtLink
44-
v-if="installScripts.content?.[scriptName]"
45-
:to="getCodeLink(installScripts.content[scriptName])"
46-
class="hover:text-fg transition-colors duration-200"
47-
>
48-
{{ installScripts.content[scriptName] }}
49-
</NuxtLink>
50-
<span v-else>{{ $t('package.install_scripts.script_label') }}</span>
60+
<template v-if="installScripts.content?.[scriptName] && scriptParts[scriptName]">
61+
<template v-if="scriptParts[scriptName].prefix">
62+
{{ scriptParts[scriptName].prefix
63+
}}<LinkBase :to="scriptParts[scriptName].link">{{
64+
scriptParts[scriptName].filePath
65+
}}</LinkBase>
66+
</template>
67+
<LinkBase v-else :to="scriptParts[scriptName].link">
68+
{{ installScripts.content[scriptName] }}
69+
</LinkBase>
70+
</template>
71+
<span v-else tabindex="0" class="cursor-help">
72+
{{ $t('package.install_scripts.script_label') }}
73+
</span>
5174
</dd>
5275
</div>
5376
</dl>

app/utils/install-scripts.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,33 @@ export function getInstallScriptFilePath(scriptContent: string): string {
148148
// Default: link to package.json
149149
return 'package.json'
150150
}
151+
152+
/**
153+
* Parse an install script into a prefix and a linkable file path.
154+
* - If the script is `node <file-path>`, returns { prefix: 'node ', filePath: '<file-path>' }
155+
* so only the file path portion can be rendered as a link.
156+
* - Otherwise, returns null (the entire script content should link to package.json).
157+
*
158+
* @param scriptContent - The content of the script
159+
* @returns Parsed parts, or null if no node file path was extracted
160+
*/
161+
export function parseNodeScript(
162+
scriptContent: string,
163+
): { prefix: string; filePath: string } | null {
164+
const match = NODE_SCRIPT_PATTERN.exec(scriptContent)
165+
166+
if (match?.[1]) {
167+
const filePath = match[1].replace(/^\.\//, '')
168+
169+
// Fall back if path contains navigational elements
170+
if (filePath.includes('../') || filePath.includes('./')) {
171+
return null
172+
}
173+
174+
// Reconstruct the prefix (everything before the captured file path)
175+
const prefix = scriptContent.slice(0, match.index + match[0].indexOf(match[1]))
176+
return { prefix, filePath }
177+
}
178+
179+
return null
180+
}

test/nuxt/a11y.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1993,6 +1993,7 @@ describe('component accessibility audits', () => {
19931993
const component = await mountSuspended(PackageInstallScripts, {
19941994
props: {
19951995
packageName: 'esbuild',
1996+
version: '0.25.0',
19961997
installScripts: {
19971998
scripts: ['postinstall'],
19981999
content: { postinstall: 'node install.js' },
@@ -2008,6 +2009,7 @@ describe('component accessibility audits', () => {
20082009
const component = await mountSuspended(PackageInstallScripts, {
20092010
props: {
20102011
packageName: 'husky',
2012+
version: '9.1.0',
20112013
installScripts: {
20122014
scripts: ['postinstall'],
20132015
content: { postinstall: 'husky install' },

test/unit/app/utils/install-scripts.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
22
import {
33
extractInstallScriptsInfo,
44
getInstallScriptFilePath,
5+
parseNodeScript,
56
} from '../../../../app/utils/install-scripts'
67

78
describe('extractInstallScriptsInfo', () => {
@@ -96,4 +97,38 @@ describe('getInstallScriptFilePath', () => {
9697
expect(getInstallScriptFilePath('node ../scripts/setup.js')).toBe('package.json')
9798
expect(getInstallScriptFilePath('node ./scripts/../lib/setup.js')).toBe('package.json')
9899
})
100+
101+
it('returns package.json for bare node command without arguments', () => {
102+
expect(getInstallScriptFilePath('node')).toBe('package.json')
103+
expect(getInstallScriptFilePath('node ')).toBe('package.json')
104+
})
105+
})
106+
107+
describe('parseNodeScript', () => {
108+
it('returns prefix and filePath for node scripts', () => {
109+
expect(parseNodeScript('node scripts/postinstall.js')).toEqual({
110+
prefix: 'node ',
111+
filePath: 'scripts/postinstall.js',
112+
})
113+
})
114+
115+
it('strips leading ./ from file path', () => {
116+
expect(parseNodeScript('node ./scripts/setup.js')).toEqual({
117+
prefix: 'node ',
118+
filePath: 'scripts/setup.js',
119+
})
120+
})
121+
122+
it('returns null for non-node scripts', () => {
123+
expect(parseNodeScript('npx prisma generate')).toBeNull()
124+
})
125+
126+
it('returns null for bare node command', () => {
127+
expect(parseNodeScript('node')).toBeNull()
128+
expect(parseNodeScript('node ')).toBeNull()
129+
})
130+
131+
it('returns null for parent directory references', () => {
132+
expect(parseNodeScript('node ../scripts/setup.js')).toBeNull()
133+
})
99134
})

0 commit comments

Comments
 (0)