Skip to content

feat: module replacements v3#2068

Open
gameroman wants to merge 36 commits intonpmx-dev:mainfrom
gameroman:module-replacements-v3
Open

feat: module replacements v3#2068
gameroman wants to merge 36 commits intonpmx-dev:mainfrom
gameroman:module-replacements-v3

Conversation

@gameroman
Copy link
Contributor

@gameroman gameroman commented Mar 13, 2026

Try module replacements v3

NOTE

AI disclosure: I used AI to help me write some of the code but verified everything manually

@vercel
Copy link

vercel bot commented Mar 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Mar 14, 2026 2:53am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 14, 2026 2:53am
npmx-lunaria Ignored Ignored Mar 14, 2026 2:53am

Request Review

@codecov
Copy link

codecov bot commented Mar 13, 2026

Codecov Report

❌ Patch coverage is 64.44444% with 16 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/components/Package/Replacement.vue 59.25% 11 Missing ⚠️
app/utils/resolve-replacement-url.ts 66.66% 4 Missing ⚠️
app/components/Compare/ReplacementSuggestion.vue 83.33% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions
Copy link

github-actions bot commented Mar 14, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
i18n/locales/ar.json Localization changed, will be marked as complete.
i18n/locales/az-AZ.json Localization changed, will be marked as complete.
i18n/locales/bg-BG.json Localization changed, will be marked as complete.
i18n/locales/bn-IN.json Localization changed, will be marked as complete.
i18n/locales/cs-CZ.json Localization changed, will be marked as complete.
i18n/locales/de-DE.json Localization changed, will be marked as complete.
i18n/locales/en.json Source changed, localizations will be marked as outdated.
i18n/locales/es.json Localization changed, will be marked as complete.
i18n/locales/fr-FR.json Localization changed, will be marked as complete.
i18n/locales/hi-IN.json Localization changed, will be marked as complete.
i18n/locales/hu-HU.json Localization changed, will be marked as complete.
i18n/locales/id-ID.json Localization changed, will be marked as complete.
i18n/locales/it-IT.json Localization changed, will be marked as complete.
i18n/locales/ja-JP.json Localization changed, will be marked as complete.
i18n/locales/kn-IN.json Localization changed, will be marked as complete.
i18n/locales/mr-IN.json Localization changed, will be marked as complete.
i18n/locales/nb-NO.json Localization changed, will be marked as complete.
i18n/locales/ne-NP.json Localization changed, will be marked as complete.
i18n/locales/pl-PL.json Localization changed, will be marked as complete.
i18n/locales/pt-BR.json Localization changed, will be marked as complete.
i18n/locales/ru-RU.json Localization changed, will be marked as complete.
i18n/locales/ta-IN.json Localization changed, will be marked as complete.
i18n/locales/te-IN.json Localization changed, will be marked as complete.
i18n/locales/tr-TR.json Localization changed, will be marked as complete.
i18n/locales/uk-UA.json Localization changed, will be marked as complete.
i18n/locales/zh-CN.json Localization changed, will be marked as complete.
i18n/locales/zh-TW.json Localization changed, will be marked as complete.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/components/Package/Replacement.vue (1)

8-22: Extract resolveUrl to a shared utility to avoid duplication.

This function is identical to the one in app/components/Compare/ReplacementSuggestion.vue (lines 17-31). Consider extracting it to a shared composable or utility file to prevent code drift and ease maintenance.

♻️ Suggested approach

Create a shared utility, e.g. app/utils/resolveReplacementUrl.ts:

import type { KnownUrl } from 'module-replacements'

export function resolveReplacementUrl(url?: KnownUrl): string | null {
  if (!url) return null
  if (typeof url === 'string') return url

  switch (url.type) {
    case 'mdn':
      return `https://developer.mozilla.org/en-US/docs/${url.id}`
    case 'node':
      return `https://nodejs.org/${url.id}`
    case 'e18e':
      return `https://e18e.dev/docs/replacements/${url.id}`
    default:
      return null
  }
}

Then import and use in both components.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d57e7a1-352a-4ca0-bbf6-8b2a365896a2

📥 Commits

Reviewing files that changed from the base of the PR and between 9e8c805 and e365d7e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (35)
  • app/components/Compare/ReplacementSuggestion.vue
  • app/components/Package/Replacement.vue
  • i18n/locales/ar.json
  • i18n/locales/az-AZ.json
  • i18n/locales/bg-BG.json
  • i18n/locales/bn-IN.json
  • i18n/locales/cs-CZ.json
  • i18n/locales/de-DE.json
  • i18n/locales/en.json
  • i18n/locales/es.json
  • i18n/locales/fr-FR.json
  • i18n/locales/hi-IN.json
  • i18n/locales/hu-HU.json
  • i18n/locales/id-ID.json
  • i18n/locales/it-IT.json
  • i18n/locales/ja-JP.json
  • i18n/locales/kn-IN.json
  • i18n/locales/mr-IN.json
  • i18n/locales/nb-NO.json
  • i18n/locales/ne-NP.json
  • i18n/locales/pl-PL.json
  • i18n/locales/pt-BR.json
  • i18n/locales/ru-RU.json
  • i18n/locales/ta-IN.json
  • i18n/locales/te-IN.json
  • i18n/locales/tr-TR.json
  • i18n/locales/uk-UA.json
  • i18n/locales/zh-CN.json
  • i18n/locales/zh-TW.json
  • i18n/schema.json
  • package.json
  • server/api/replacements/[...pkg].get.ts
  • test/nuxt/a11y.spec.ts
  • test/nuxt/composables/use-compare-replacements.spec.ts
  • test/nuxt/composables/use-replacement-dependencies.spec.ts
💤 Files with no reviewable changes (17)
  • i18n/locales/zh-CN.json
  • i18n/locales/ja-JP.json
  • i18n/locales/cs-CZ.json
  • i18n/locales/zh-TW.json
  • i18n/locales/uk-UA.json
  • i18n/locales/az-AZ.json
  • i18n/locales/pl-PL.json
  • i18n/locales/de-DE.json
  • i18n/locales/bg-BG.json
  • i18n/locales/ru-RU.json
  • i18n/locales/es.json
  • i18n/locales/en.json
  • i18n/locales/fr-FR.json
  • i18n/locales/id-ID.json
  • i18n/schema.json
  • i18n/locales/hu-HU.json
  • i18n/locales/tr-TR.json

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7424dab9-5a11-408b-9f4c-9094682f5131

📥 Commits

Reviewing files that changed from the base of the PR and between e365d7e and 63417bf.

📒 Files selected for processing (3)
  • app/components/Compare/ReplacementSuggestion.vue
  • app/components/Package/Replacement.vue
  • app/utils/resolve-replacement-url.ts

@gameroman
Copy link
Contributor Author

For some reason this doesn't have Node.js version

https://npmx-jfedsirz3-npmx.vercel.app/package/gzip-size

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/components/Compare/ReplacementSuggestion.vue (1)

18-23: De-duplicate the replacement metadata parsing.

docUrl and nodeVersion now mirror the logic in app/components/Package/Replacement.vue. Moving both into a small shared utility next to resolveReplacementUrl would keep the compare and package views aligned, and future schema changes would only need one fix.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 54a2bc78-0d01-4fee-bec0-69e066b02e3f

📥 Commits

Reviewing files that changed from the base of the PR and between 63417bf and 93babd2.

📒 Files selected for processing (2)
  • app/components/Compare/ReplacementSuggestion.vue
  • app/components/Package/Replacement.vue

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59577309-1a31-409d-b9a4-c7029692555b

📥 Commits

Reviewing files that changed from the base of the PR and between 93babd2 and 03584bd.

📒 Files selected for processing (3)
  • app/components/Compare/ReplacementSuggestion.vue
  • app/components/Package/Replacement.vue
  • i18n/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • i18n/locales/en.json

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
app/components/Package/Replacement.vue (2)

42-50: Guard against missing description for simple replacements.

If replacement.description is undefined or empty for a simple replacement, line 43 will render nothing, leaving an empty <div class="mb-2"> element. Consider adding a fallback or conditional rendering.

Proposed fix
     <div v-else-if="replacement.type === 'simple'" class="block">
-      <div class="mb-2">{{ replacement.description }}</div>
+      <div v-if="replacement.description" class="mb-2">{{ replacement.description }}</div>
       <div v-if="replacement.example">

71-73: Guard against missing description for removal replacements.

Similar to the simple type, if replacement.description is undefined for a removal, the template will render nothing. Consider adding a fallback.

Proposed fix
     <template v-else-if="replacement.type === 'removal'">
-      {{ replacement.description }}
+      {{ replacement.description || replacement.id }}
     </template>

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0dd13a4-3ed7-4e63-858d-9ea81677b376

📥 Commits

Reviewing files that changed from the base of the PR and between 03584bd and 375bd0a.

📒 Files selected for processing (2)
  • app/components/Package/Replacement.vue
  • i18n/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • i18n/locales/en.json

@gameroman
Copy link
Contributor Author

@coderabbitai what should I do with these failing readonly types?

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look at the server API file for replacements
fd -t f "pkg.get.ts" --exec cat {}

echo "---"
# Look at the resolve-replacement-url utility
fd -t f "resolve-replacement-url.ts" --exec cat {}

echo "---"
# Search for readonly-related type errors or usages
rg -n "readonly" --type ts -C 3

echo "---"
# Check the module-replacements package types
fd -t f "*.d.ts" -p "node_modules/module-replacements" --exec cat {}

echo "---"
# Also check what types are imported from module-replacements
rg -n "from 'module-replacements'" --type ts -C 2
rg -n 'from "module-replacements"' --type ts -C 2

echo "---"
# Check the composables related to replacements
fd -t f "use-replacement*" --exec cat {}
fd -t f "use-compare-replacements*" --exec cat {}

Length of output: 30290


🌐 Web query:

module-replacements 3.0.0-beta.0 TypeScript types readonly engines

💡 Result:

In module-replacements@3.0.0-beta.0, the manifest v3 work introduced an engines field on replacements to express compatibility constraints (e.g. { engine: "node", minVersion: "X.Y.Z" }). [1], [2]

TypeScript types (beta.0)

The published/source TypeScript types define:

  • interface EngineConstraint { engine: string; minVersion?: string; maxVersion?: string }
  • engines?: EngineConstraint[] on the shared ModuleReplacementLike shape (which is extended by native, simple, documented, and removal replacements)

Notably, in the source types for 3.0.0-beta.0, engines is not declared readonly (it’s a normal EngineConstraint[]). [3]

If you’re seeing readonly ...[] in your codebase, that’s typically coming from your JSON being imported/typed as const (or otherwise inferred as immutable), not from module-replacements’ own EngineConstraint[] declaration. [3]

Sources:
[1] Release notes for 3.0.0-beta.0 (schema v3 iteration) (github.com)
[2] PR #266 discussion/examples showing engines in v3 schema (github.com)
[3] src/types.ts and package.json at the 3.0.0-beta.0 tag/commit (shows EngineConstraint + engines?: EngineConstraint[]) (raw.githubusercontent.com)

Citations:


🏁 Script executed:

#!/bin/bash
# Check the server API file
cat server/api/replacements/\[...pkg\].get.ts

echo "==="
# Check all composables that import from module-replacements
cat app/composables/useModuleReplacement.ts
echo "---"
cat app/composables/useCompareReplacements.ts
echo "---"
cat app/composables/npm/useReplacementDependencies.ts

echo "==="
# Check the vue components
fd -t f "Replacement.vue" --exec cat {}
echo "---"
fd -t f "ReplacementSuggestion.vue" --exec cat {}

echo "==="
# Check the a11y spec for the full test data
cat test/nuxt/a11y.spec.ts | head -150

Length of output: 17106


🏁 Script executed:

#!/bin/bash
# Try to fetch the actual types from the npm package
curl -s "https://registry.npmjs.org/module-replacements/3.0.0-beta.0" | python3 -c "
import json, sys
data = json.load(sys.stdin)
print('dist:', data.get('dist', {}).get('tarball'))
print('types:', data.get('types'))
print('main:', data.get('main'))
print('exports:', json.dumps(data.get('exports', {}), indent=2))
"

echo "==="
# Try fetching the source types from github
curl -s "https://raw.githubusercontent.com/es-tooling/module-replacements/main/src/types.ts" | head -100

Length of output: 2086


🏁 Script executed:

#!/bin/bash
# Download and inspect the tarball
curl -sL "https://registry.npmjs.org/module-replacements/-/module-replacements-3.0.0-beta.0.tgz" -o /tmp/mr.tgz
tar -tzf /tmp/mr.tgz | grep -E "\.(d\.ts|ts)$" | head -30
echo "==="
tar -xzf /tmp/mr.tgz -C /tmp package/dist/esm/main.d.ts 2>/dev/null && cat /tmp/package/dist/esm/main.d.ts | head -80
echo "==="
tar -xzf /tmp/mr.tgz -C /tmp package/dist/commonjs/main.d.ts 2>/dev/null && cat /tmp/package/dist/commonjs/main.d.ts | head -80

Length of output: 904


🏁 Script executed:

#!/bin/bash
tar -xzf /tmp/mr.tgz -C /tmp package/dist/esm/types.d.ts 2>/dev/null
cat /tmp/package/dist/esm/types.d.ts

echo "==="
# Also check the actual JS files to understand the structure at runtime
tar -xzf /tmp/mr.tgz -C /tmp package/dist/esm/main.js 2>/dev/null
head -30 /tmp/package/dist/esm/main.js

echo "==="
# Check if there's a none type
tar -xzf /tmp/mr.tgz -C /tmp package/dist/esm/manifests-dir.d.ts 2>/dev/null
cat /tmp/package/dist/esm/manifests-dir.d.ts

Length of output: 2408


@gameroman The root cause isn't in the module-replacements package itself — its types are clean and non-readonly. The problem comes from Vue's readonly() wrapper in useCompareReplacements.ts.

When you wrap a ComputedRef in readonly():

noDepSuggestions: readonly(noDepSuggestions), // ComputedRef<ReplacementSuggestion[]>

Vue's type system produces DeepReadonly<ComputedRef<ReplacementSuggestion[]>>. Accessing .value then yields readonly DeepReadonly<ReplacementSuggestion>[], where each item's .replacement becomes DeepReadonly<ModuleReplacement>. Passing that into component props typed as ModuleReplacement causes the TypeScript errors.

The fix: ComputedRef is already inherently readonly (you can't assign to .value externally), so there's no need to wrap them in readonly(). Only the shallowRefs (replacements, loading) need that protection.

In app/composables/useCompareReplacements.ts, change the return statement:

  return {
    replacements: readonly(replacements),
-   noDepSuggestions: readonly(noDepSuggestions),
-   infoSuggestions: readonly(infoSuggestions),
+   noDepSuggestions,
+   infoSuggestions,
    loading: readonly(loading),
  }

This removes the unnecessary DeepReadonly wrapping while still protecting the mutable shallowRefs, and the component props typed as ModuleReplacement will accept the values without errors.

@serhalp
Copy link
Member

serhalp commented Mar 14, 2026

Attaching some screenshots

Screenshot 2026-03-14 at 11 56 05 Screenshot 2026-03-14 at 11 58 22 Screenshot 2026-03-14 at 11 57 40 Screenshot 2026-03-14 at 12 00 04 Screenshot 2026-03-14 at 12 00 38

Copy link
Member

@serhalp serhalp left a comment

Choose a reason for hiding this comment

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

LGTM! no notes :shipit:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants