feat: add storybook playground link#1624
Conversation
Co-Authored-By: Scott Wu <sw@scottwu.ca>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
📝 WalkthroughWalkthroughAdds Storybook as a playground provider across the app: maps the "storybook" provider to an icon and colour in the Playgrounds component, merges package.json-derived Storybook links into the package page's playgroundLinks, augments SlimPackument with an optional Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
app/pages/package/[[org]]/[name].vue (1)
112-115: Addserver: falseto avoid an external CDN round-trip on the SSR path.The Storybook playground link is a non-critical enhancement. Other similarly non-critical
useLazyFetchcalls in this file (installSize,provenance) already opt out of SSR withserver: false. Without it this fetch runs server-side for every page load, adding jsDelivr latency to your SSR response time.♻️ Suggested change
-const { data: packageJson } = useLazyFetch<{ storybook?: { title: string; url: string } }>(() => { - const version = requestedVersion.value ?? 'latest' - return `https://cdn.jsdelivr.net/npm/${packageName.value}@${version}/package.json` -}) +const { data: packageJson } = useLazyFetch<{ storybook?: { title?: string; url: string } }>( + () => { + const version = requestedVersion.value ?? 'latest' + return `https://cdn.jsdelivr.net/npm/${packageName.value}@${version}/package.json` + }, + { server: false }, +)Also note:
titleis optional in the Storybook package composition schema —titlehas no default and is not required; typing it asstring(non-optional) is inaccurate (fix shown in the diff above).
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
app/components/Package/Playgrounds.vueapp/pages/package/[[org]]/[name].vueuno.config.ts
app/pages/package/[[org]]/[name].vue
Outdated
| const playgroundLinks = computed(() => [ | ||
| ...readmeData.value.playgroundLinks, | ||
| // Libraries with a storybook field in package.json contain a link to their deployed playground | ||
| ...(packageJson.value?.storybook |
There was a problem hiding this comment.
| ...(packageJson.value?.storybook | |
| ...(packageJson.value?.storybook?.url |
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
app/composables/npm/usePackage.tsapp/pages/package/[[org]]/[name].vueshared/types/npm-registry.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- app/pages/package/[[org]]/[name].vue
| // Extract storybook field from the requested version (custom package.json field) | ||
| const requestedPkgVersion = requestedVersion ? pkg.versions[requestedVersion] : null | ||
| const rawStorybook = requestedPkgVersion?.storybook |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n app/composables/npm/usePackage.ts | sed -n '120,135p'Repository: npmx-dev/npmx.dev
Length of output: 712
🏁 Script executed:
find . -path "*/node_modules/@npm/types*" -name "*.d.ts" -o -path "*/node_modules/@npm/types*" -name "*.ts" | head -20Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
rg "PackumentVersion" --type ts -A 10 -B 2 | head -80Repository: npmx-dev/npmx.dev
Length of output: 6995
🏁 Script executed:
fd -name "types.ts" -o -name "types.d.ts" | grep shared | head -10Repository: npmx-dev/npmx.dev
Length of output: 291
🏁 Script executed:
find . -path "*/shared/*types*" -name "*.ts" | head -20Repository: npmx-dev/npmx.dev
Length of output: 613
🏁 Script executed:
rg "export.*PackumentVersion" --type ts -B 5 -A 15 | head -100Repository: npmx-dev/npmx.dev
Length of output: 5135
🏁 Script executed:
cat -n shared/types/npm-registry.ts | head -60Repository: npmx-dev/npmx.dev
Length of output: 2619
🏁 Script executed:
rg "PackumentVersionWithoutAttestations" --type ts -B 2 -A 5 | head -60Repository: npmx-dev/npmx.dev
Length of output: 1421
🏁 Script executed:
cat package.json | grep -A 5 "@npm/types"Repository: npmx-dev/npmx.dev
Length of output: 269
🏁 Script executed:
rg "storybook" --type ts -i | head -20Repository: npmx-dev/npmx.dev
Length of output: 1133
🏁 Script executed:
npx tsc --noEmit 2>&1 | grep -i "storybook\|usePackage" | head -20Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
cat -n shared/types/npm-registry.ts | sed -n '15,100p'Repository: npmx-dev/npmx.dev
Length of output: 3876
🏁 Script executed:
rg "'storybook'" --type ts -B 5 -A 3 shared/types/npm-registry.tsRepository: npmx-dev/npmx.dev
Length of output: 433
🏁 Script executed:
cat -n shared/types/npm-registry.ts | sed -n '1,30p'Repository: npmx-dev/npmx.dev
Length of output: 1497
🏁 Script executed:
rg "interface PackumentVersion|type PackumentVersion" --type ts -A 10 -B 2Repository: npmx-dev/npmx.dev
Length of output: 1118
🏁 Script executed:
cat -n app/composables/npm/usePackage.ts | sed -n '120,165p'Repository: npmx-dev/npmx.dev
Length of output: 1917
🏁 Script executed:
rg "PackumentVersionWithoutAttestations\[" --type ts -A 3 -B 3Repository: npmx-dev/npmx.dev
Length of output: 606
🏁 Script executed:
npm ls `@npm/types` 2>&1 | head -20Repository: npmx-dev/npmx.dev
Length of output: 103
🏁 Script executed:
cat -n app/composables/npm/usePackage.ts | sed -n '1,50p'Repository: npmx-dev/npmx.dev
Length of output: 2073
🏁 Script executed:
rg "transformPackument" --type ts -B 5 -A 50 | head -80Repository: npmx-dev/npmx.dev
Length of output: 5709
🏁 Script executed:
node -e "
const pkg = require('./package.json');
console.log('@npm/types version:', pkg.dependencies['@npm/types']);
"Repository: npmx-dev/npmx.dev
Length of output: 90
🏁 Script executed:
ls -la node_modules/@npm/types/ 2>/dev/null | head -20Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
python3 << 'EOF'
import json
import subprocess
# Try to find and inspect the actual `@npm/types` definitions
result = subprocess.run(['find', '.', '-path', '*/node_modules/@npm/types*', '-name', 'index.d.ts'],
capture_output=True, text=True, timeout=5)
files = result.stdout.strip().split('\n')
if files and files[0]:
with open(files[0], 'r') as f:
content = f.read()
# Find PackumentVersion definition
lines = content.split('\n')
for i, line in enumerate(lines):
if 'PackumentVersion' in line and 'interface' in line:
# Print context around the definition
start = max(0, i - 3)
end = min(len(lines), i + 30)
print('\n'.join(f'{start+j}: {lines[start+j]}' for j in range(end-start)))
break
EOFRepository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
cat package.json | jq '.dependencies, .devDependencies' | grep -A 2 -B 2 "@npm"Repository: npmx-dev/npmx.dev
Length of output: 214
🏁 Script executed:
rg "@npm/types" --type ts | head -20Repository: npmx-dev/npmx.dev
Length of output: 1563
🏁 Script executed:
find . -type f -name "*.json" -path "*/node_modules/@npm*" | head -5Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
cat tsconfig.json | jq '.compilerOptions | {strict, noImplicitAny, suppressImplicitAnyIndexErrors}'Repository: npmx-dev/npmx.dev
Length of output: 117
🏁 Script executed:
rg "storybook" --type ts -B 5 -A 5 app/composables/npm/usePackage.tsRepository: npmx-dev/npmx.dev
Length of output: 896
🏁 Script executed:
python3 << 'EOF'
# Let's check if `@npm/types` has an index signature by checking the installed version
import subprocess
import json
# Read package.json
with open('package.json', 'r') as f:
pkg = json.load(f)
version = pkg.get('dependencies', {}).get('@npm/types', 'not found')
print(f"@npm/types version: {version}")
# Try to find the actual type definitions
result = subprocess.run(['find', 'node_modules/@npm/types', '-name', '*.d.ts'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0 and result.stdout:
files = result.stdout.strip().split('\n')
if files[0]:
print(f"\nFound type definition files:\n{chr(10).join(files[:3])}")
# Try to read the first one
try:
with open(files[0], 'r') as f:
content = f.read()
# Look for PackumentVersion
if 'PackumentVersion' in content:
lines = content.split('\n')
for i, line in enumerate(lines):
if 'PackumentVersion' in line:
start = max(0, i - 2)
end = min(len(lines), i + 25)
print(f"\nFound at line {i}:")
print('\n'.join(lines[start:end]))
break
except Exception as e:
print(f"Error reading file: {e}")
else:
print("Could not find `@npm/types` files")
EOFRepository: npmx-dev/npmx.dev
Length of output: 122
🏁 Script executed:
cat tsconfig.json | head -40Repository: npmx-dev/npmx.dev
Length of output: 380
🏁 Script executed:
rg "\"strict\"|\"noImplicitAny\"" tsconfig.jsonRepository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
python3 << 'EOF'
# Analyze the code structure to understand type safety
content = '''
shared/types/npm-registry.ts line 21-24:
export interface PackumentVersion extends PackumentVersionWithoutAttestations {
_npmUser?: Contact & { trustedPublisher?: NpmTrustedPublisherEvidence }
dist: PackumentVersionWithoutAttestations['dist'] & { attestations?: NpmVersionAttestations }
}
usePackage.ts line 124-125:
const requestedPkgVersion = requestedVersion ? pkg.versions[requestedVersion] : null
const rawStorybook = requestedPkgVersion?.storybook
'''
print("Type Analysis:")
print("=" * 60)
print("requestedPkgVersion type: PackumentVersion | null")
print("PackumentVersion properties: _npmUser (optional), dist (required)")
print(" + inherited from PackumentVersionWithoutAttestations")
print("\nAccessing: requestedPkgVersion?.storybook")
print("Property 'storybook' is NOT defined on PackumentVersion")
print("\nConclusion: This is a type error IF strict mode is enabled")
print("=" * 60)
EOFRepository: npmx-dev/npmx.dev
Length of output: 553
🏁 Script executed:
cat -n app/composables/npm/usePackage.ts | sed -n '36,80p'Repository: npmx-dev/npmx.dev
Length of output: 2090
🏁 Script executed:
find .nuxt -name "tsconfig*.json" -exec cat {} \; 2>/dev/null | grep -A 5 '"strict"' | head -20Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
cat -n shared/types/npm-registry.ts | sed -n '80,95p'Repository: npmx-dev/npmx.dev
Length of output: 717
🏁 Script executed:
rg "// Extract storybook" --type ts -B 2 -A 15Repository: npmx-dev/npmx.dev
Length of output: 1282
🏁 Script executed:
cat -n shared/types/npm-registry.ts | sed -n '1,30p'Repository: npmx-dev/npmx.dev
Length of output: 1497
🏁 Script executed:
python3 << 'EOF'
# Let's trace the actual type flow
print("""
TYPE FLOW ANALYSIS:
==================
Line 124 (usePackage.ts):
const requestedPkgVersion = requestedVersion ? pkg.versions[requestedVersion] : null
Type: PackumentVersion | null
(pkg is of type Packument, pkg.versions is Record<string, PackumentVersion>)
Line 125:
const rawStorybook = requestedPkgVersion?.storybook
PackumentVersion definition (shared/types/npm-registry.ts lines 21-24):
export interface PackumentVersion extends PackumentVersionWithoutAttestations {
_npmUser?: Contact & { trustedPublisher?: NpmTrustedPublisherEvidence }
dist: PackumentVersionWithoutAttestations['dist'] & { attestations?: NpmVersionAttestations }
}
Properties on PackumentVersion:
- _npmUser (optional, from local interface)
- dist (required, overridden)
- All properties from PackumentVersionWithoutAttestations (`@npm/types`)
Property 'storybook':
- NOT present on PackumentVersion
- IS present on SlimPackument (line 86: 'storybook'?: { url: string })
- The code is trying to EXTRACT storybook from PackumentVersion to build SlimPackument
This is a TYPE SAFETY ISSUE: accessing undefined property on a typed interface.
The guard at line 127-129:
rawStorybook && typeof rawStorybook === 'object' && 'url' in rawStorybook
Only checks key presence, not value type. 'url' could be null/undefined/empty string.
""")
EOFRepository: npmx-dev/npmx.dev
Length of output: 1408
Add type-safe accessor for undeclared storybook field.
requestedPkgVersion is typed as PackumentVersion (from shared/types/npm-registry.ts), which does not declare a storybook property. Although this is a custom package.json field that the code intentionally extracts, accessing it without proper typing creates a type safety violation. Use bracket notation with a type assertion to safely access custom fields:
Proposed fix
// Extract storybook field from the requested version (custom package.json field)
const requestedPkgVersion = requestedVersion ? pkg.versions[requestedVersion] : null
- const rawStorybook = requestedPkgVersion?.storybook
+ const rawStorybook = requestedPkgVersion
+ ? (requestedPkgVersion as Record<string, unknown>)['storybook']
+ : undefinedAdditionally, the guard at line 127 checks only for key presence ('url' in rawStorybook), not value validity. The url property could be null, undefined, or an empty string, making the downstream type assertion unsafe. Strengthen the validation:
const storybook =
rawStorybook && typeof rawStorybook === 'object' && 'url' in rawStorybook
+ && typeof (rawStorybook as { url?: unknown }).url === 'string'
+ && (rawStorybook as { url: string }).url.length > 0
? ({ url: rawStorybook.url } as { url: string })
: undefined📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Extract storybook field from the requested version (custom package.json field) | |
| const requestedPkgVersion = requestedVersion ? pkg.versions[requestedVersion] : null | |
| const rawStorybook = requestedPkgVersion?.storybook | |
| // Extract storybook field from the requested version (custom package.json field) | |
| const requestedPkgVersion = requestedVersion ? pkg.versions[requestedVersion] : null | |
| const rawStorybook = requestedPkgVersion | |
| ? (requestedPkgVersion as Record<string, unknown>)['storybook'] | |
| : undefined |
| const storybook = | ||
| rawStorybook && typeof rawStorybook === 'object' && 'url' in rawStorybook | ||
| ? (rawStorybook as { title?: string; url: string }) | ||
| : undefined |
There was a problem hiding this comment.
'url' in rawStorybook only checks key presence — the url value may still be null, undefined, or a non-string.
The type assertion on line 128 types url as string, but { url: null } or { url: 42 } would pass the current guard. This would propagate an invalid URL into the slim package payload, which the downstream playground link UI would then render broken.
🛡️ Proposed fix — validate that `url` is a non-empty string
const storybook =
- rawStorybook && typeof rawStorybook === 'object' && 'url' in rawStorybook
- ? (rawStorybook as { title?: string; url: string })
+ rawStorybook && typeof rawStorybook === 'object'
+ && typeof (rawStorybook as Record<string, unknown>)['url'] === 'string'
+ && ((rawStorybook as Record<string, unknown>)['url'] as string).length > 0
+ ? (rawStorybook as { title?: string; url: string })
: undefined📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const storybook = | |
| rawStorybook && typeof rawStorybook === 'object' && 'url' in rawStorybook | |
| ? (rawStorybook as { title?: string; url: string }) | |
| : undefined | |
| const storybook = | |
| rawStorybook && typeof rawStorybook === 'object' | |
| && typeof (rawStorybook as Record<string, unknown>)['url'] === 'string' | |
| && ((rawStorybook as Record<string, unknown>)['url'] as string).length > 0 | |
| ? (rawStorybook as { title?: string; url: string }) | |
| : undefined |
|
Thanks for your first contribution, @yannbf! 🎊 We'd love to welcome you to the npmx community. Come and say hi on Discord! And once you've joined, visit npmx.wamellow.com to claim the contributor role. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
app/pages/package/[[org]]/[name].vue (1)
112-125: LGTM — the priorstorybook.urlguard concern is now resolved.The condition correctly uses
pkg.value?.storybook?.url, ensuring noPlaygroundLinkentry is emitted whenstorybookis present buturlis absent (e.g., Storybook addon packages).One minor edge case to keep in mind:
readmeData.value.playgroundLinkscould theoretically contain a Storybook entry if the README parser ever starts detectingstorybook.ioURLs, which would produce a duplicate entry in the merged array. Deduplication logic (e.g., byproviderorurl) would guard against that if it becomes a concern.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
app/composables/npm/usePackage.tsapp/pages/package/[[org]]/[name].vueshared/types/npm-registry.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- app/composables/npm/usePackage.ts
| 'keywords'?: string[] | ||
| 'repository'?: { type?: string; url?: string; directory?: string } | ||
| 'bugs'?: { url?: string; email?: string } | ||
| 'storybook'?: { url: string } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Locate usePackage.ts and npm-registry.ts files
fd -t f "usePackage\\.ts" -o "npm-registry\\.ts"Repository: npmx-dev/npmx.dev
Length of output: 212
🏁 Script executed:
# Locate usePackage.ts and npm-registry.ts files
fd -t f "usePackage\.ts"
fd -t f "npm-registry\.ts"Repository: npmx-dev/npmx.dev
Length of output: 123
🏁 Script executed:
# Get context around line 86 in npm-registry.ts
wc -l shared/types/npm-registry.tsRepository: npmx-dev/npmx.dev
Length of output: 93
🏁 Script executed:
# Read npm-registry.ts around line 86 to see the full type definition
sed -n '70,105p' shared/types/npm-registry.tsRepository: npmx-dev/npmx.dev
Length of output: 1375
🏁 Script executed:
# Get file size of usePackage.ts
wc -l app/composables/npm/usePackage.tsRepository: npmx-dev/npmx.dev
Length of output: 98
🏁 Script executed:
# Read entire usePackage.ts
cat -n app/composables/npm/usePackage.tsRepository: npmx-dev/npmx.dev
Length of output: 7211
The guard at line 127 only checks property existence, not that url is a string.
The condition 'url' in rawStorybook on line 127 returns true even if url is undefined. Line 128 then creates { url: undefined } and casts it as { url: string }, violating the SlimPackument contract where url: string is required (non-optional). The guard should additionally validate that rawStorybook.url is a string:
rawStorybook && typeof rawStorybook === 'object' && typeof rawStorybook.url === 'string'
🔗 Linked issue
resolves #1298
🧭 Context
Libraries with a storybook field in package.json contain a link to their deployed playground, e.g. Chakra UI
has this playground: https://storybook.chakra-ui.com/?path=/story/layout-absolutecenter--basic
You can read more about that standard in the Storybook docs.
It would be amazing to surface that automatically, which is what this PR is providing.
📚 Description
This PR adds a new type of playground: Storybook. Works automatically based on packages that contain the storybook field in their package.json
Thanks a lot @sacrosanctic for doing incredible experimentation on integrating Storybook with npmx!