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
2 changes: 2 additions & 0 deletions app/components/Package/Playgrounds.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const providerIcons: Record<string, string> = {
'solid-playground': 'i-simple-icons:solid',
'svelte-playground': 'i-simple-icons:svelte',
'tailwind-playground': 'i-simple-icons:tailwindcss',
'storybook': 'i-simple-icons:storybook',
}

// Map provider id to color class
Expand All @@ -37,6 +38,7 @@ const providerColors: Record<string, string> = {
'solid-playground': 'text-provider-solid',
'svelte-playground': 'text-provider-svelte',
'tailwind-playground': 'text-provider-tailwind',
'storybook': 'text-provider-storybook',
}

function getIcon(provider: string): string {
Expand Down
9 changes: 9 additions & 0 deletions app/composables/npm/usePackage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ export function transformPackument(
license = license.type
}

// Extract storybook field from the requested version (custom package.json field)
const requestedPkgVersion = requestedVersion ? pkg.versions[requestedVersion] : null
const rawStorybook = requestedPkgVersion?.storybook
Comment on lines +123 to +125
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -20

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

rg "PackumentVersion" --type ts -A 10 -B 2 | head -80

Repository: npmx-dev/npmx.dev

Length of output: 6995


🏁 Script executed:

fd -name "types.ts" -o -name "types.d.ts" | grep shared | head -10

Repository: npmx-dev/npmx.dev

Length of output: 291


🏁 Script executed:

find . -path "*/shared/*types*" -name "*.ts" | head -20

Repository: npmx-dev/npmx.dev

Length of output: 613


🏁 Script executed:

rg "export.*PackumentVersion" --type ts -B 5 -A 15 | head -100

Repository: npmx-dev/npmx.dev

Length of output: 5135


🏁 Script executed:

cat -n shared/types/npm-registry.ts | head -60

Repository: npmx-dev/npmx.dev

Length of output: 2619


🏁 Script executed:

rg "PackumentVersionWithoutAttestations" --type ts -B 2 -A 5 | head -60

Repository: 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 -20

Repository: npmx-dev/npmx.dev

Length of output: 1133


🏁 Script executed:

npx tsc --noEmit 2>&1 | grep -i "storybook\|usePackage" | head -20

Repository: 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.ts

Repository: 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 2

Repository: 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 3

Repository: npmx-dev/npmx.dev

Length of output: 606


🏁 Script executed:

npm ls `@npm/types` 2>&1 | head -20

Repository: 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 -80

Repository: 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 -20

Repository: 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
EOF

Repository: 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 -20

Repository: npmx-dev/npmx.dev

Length of output: 1563


🏁 Script executed:

find . -type f -name "*.json" -path "*/node_modules/@npm*" | head -5

Repository: 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.ts

Repository: 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")
EOF

Repository: npmx-dev/npmx.dev

Length of output: 122


🏁 Script executed:

cat tsconfig.json | head -40

Repository: npmx-dev/npmx.dev

Length of output: 380


🏁 Script executed:

rg "\"strict\"|\"noImplicitAny\"" tsconfig.json

Repository: 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)
EOF

Repository: 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 -20

Repository: 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 15

Repository: 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.
""")
EOF

Repository: 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']
+   : undefined

Additionally, 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.

Suggested change
// 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
? ({ url: rawStorybook.url } as { url: string })
: undefined
Comment on lines 126 to 129
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

'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.

Suggested change
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


return {
'_id': pkg._id,
'_rev': pkg._rev,
Expand All @@ -134,6 +142,7 @@ export function transformPackument(
'keywords': pkg.keywords,
'repository': pkg.repository,
'bugs': pkg.bugs,
...(storybook && { storybook }),
'requestedVersion': versionData,
'versions': filteredVersions,
'securityVersions': securityVersions,
Expand Down
20 changes: 16 additions & 4 deletions app/pages/package/[[org]]/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ const { data: readmeData } = useLazyFetch<ReadmeResponse>(
{ default: () => ({ html: '', mdExists: false, playgroundLinks: [], toc: [] }) },
)

const playgroundLinks = computed(() => [
...readmeData.value.playgroundLinks,
// Libraries with a storybook field in package.json contain a link to their deployed playground
...(pkg.value?.storybook?.url
? [
{
url: pkg.value.storybook.url,
provider: 'storybook',
providerName: 'Storybook',
label: 'Storybook',
},
]
: []),
])

const {
data: readmeMarkdownData,
status: readmeMarkdownStatus,
Expand Down Expand Up @@ -1357,10 +1372,7 @@ const showSkeleton = shallowRef(false)
/>

<!-- Playground links -->
<PackagePlaygrounds
v-if="readmeData?.playgroundLinks?.length"
:links="readmeData.playgroundLinks"
/>
<PackagePlaygrounds v-if="playgroundLinks.length" :links="playgroundLinks" />

<PackageCompatibility :engines="displayVersion?.engines" />

Expand Down
1 change: 1 addition & 0 deletions shared/types/npm-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export interface SlimPackument {
'keywords'?: string[]
'repository'?: { type?: string; url?: string; directory?: string }
'bugs'?: { url?: string; email?: string }
'storybook'?: { url: string }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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.ts

Repository: 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.ts

Repository: npmx-dev/npmx.dev

Length of output: 1375


🏁 Script executed:

# Get file size of usePackage.ts
wc -l app/composables/npm/usePackage.ts

Repository: npmx-dev/npmx.dev

Length of output: 98


🏁 Script executed:

# Read entire usePackage.ts
cat -n app/composables/npm/usePackage.ts

Repository: 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'

/** current version */
'requestedVersion': SlimPackumentVersion | null
/** Only includes dist-tag versions (with installScripts info added per version) */
Expand Down
1 change: 1 addition & 0 deletions uno.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export default defineConfig({
solid: '#2C4F7C',
svelte: '#FF3E00',
tailwind: '#06B6D4',
storybook: '#FF4785',
},
},
animation: {
Expand Down
Loading