diff --git a/.github/workflows/ecosystem-playground.yml b/.github/workflows/ecosystem-playground.yml index a32c072533..6c546741c7 100644 --- a/.github/workflows/ecosystem-playground.yml +++ b/.github/workflows/ecosystem-playground.yml @@ -31,6 +31,22 @@ jobs: # the module combo still build" signal, not a full `pnpm build`. - run: pnpm install - run: pnpm run prepare + # How the optional-peer Nitro types resolve when a consumer has only one + # of `nitro` (Nitro v3) / `nitropack` (Nitro v2) — fast, self-contained. + - name: Check nitro/nitropack single-engine type resolution + run: node playgrounds-ecosystem/scripts/check-nitro-type-resolution.mjs - run: pnpm -C playgrounds-ecosystem/modules install - - name: Build the module combo + - name: Build the module combo (Nuxt 4) run: pnpm -C playgrounds-ecosystem/modules run build + # Per-major production playgrounds: prove @nuxt/devtools builds + + # type-checks on a Nuxt 4 (Nitro v2) and a Nuxt 5 (Nitro v3) consumer. + - name: Nuxt 4 playground (Nitro v2) + run: | + pnpm -C playgrounds-ecosystem/nuxt4 install + pnpm -C playgrounds-ecosystem/nuxt4 run typecheck + pnpm -C playgrounds-ecosystem/nuxt4 run build + - name: Nuxt 5 playground (Nitro v3) + run: | + pnpm -C playgrounds-ecosystem/nuxt5 install + pnpm -C playgrounds-ecosystem/nuxt5 run typecheck + pnpm -C playgrounds-ecosystem/nuxt5 run build diff --git a/packages/devtools-kit/build.config.ts b/packages/devtools-kit/build.config.ts index 414cd756f3..ff0a374593 100644 --- a/packages/devtools-kit/build.config.ts +++ b/packages/devtools-kit/build.config.ts @@ -14,6 +14,8 @@ export default defineBuildConfig({ 'nuxt/schema', '@nuxt/schema', 'nitropack', + 'nitro', + 'nitro/types', 'unimport', 'unstorage', 'ofetch', diff --git a/packages/devtools-kit/package.json b/packages/devtools-kit/package.json index acacba8ba1..98940acde7 100644 --- a/packages/devtools-kit/package.json +++ b/packages/devtools-kit/package.json @@ -44,8 +44,18 @@ "prepack": "pnpm build" }, "peerDependencies": { + "nitro": "^3.0.0-0", + "nitropack": "^2.0.0", "vite": ">=6.0" }, + "peerDependenciesMeta": { + "nitro": { + "optional": true + }, + "nitropack": { + "optional": true + } + }, "dependencies": { "@nuxt/kit": "catalog:prod", "nostics": "catalog:prod", @@ -56,6 +66,8 @@ "birpc": "catalog:frontend", "error-stack-parser-es": "catalog:frontend", "hookable": "catalog:frontend", + "nitro": "catalog:buildtools", + "nitropack": "catalog:buildtools", "unbuild": "catalog:buildtools", "unimport": "catalog:types", "vue-router": "catalog:frontend" diff --git a/packages/devtools-kit/src/_types/nitro-compat.ts b/packages/devtools-kit/src/_types/nitro-compat.ts new file mode 100644 index 0000000000..13480a34bf --- /dev/null +++ b/packages/devtools-kit/src/_types/nitro-compat.ts @@ -0,0 +1,26 @@ +import type { Nitro as NitroV3, StorageMounts as StorageMountsV3 } from 'nitro/types' +import type { Nitro as NitroV2, StorageMounts as StorageMountsV2 } from 'nitropack' + +/** + * `nitropack` (Nitro v2) and `nitro` (Nitro v3) are both declared as *optional* + * peer dependencies, so a consumer only ever has one installed (Nuxt 4 → + * `nitropack`, Nuxt 5 → `nitro`). A missing peer resolves its `import type` to + * `any`, which would collapse a naive `V2 | V3` union to `any`. Detect which + * package actually resolved — `keyof any` matches every key, so probing an + * impossible `'___INVALID'` key tells a real Nitro type from the `any` + * fallback — and resolve to just that one. Mirrors `@nuxt/kit`'s own detection. + */ +type HasNitroV2 = 'options' extends keyof NitroV2 + ? ('___INVALID' extends keyof NitroV2 ? false : true) + : false +type HasNitroV3 = 'options' extends keyof NitroV3 + ? ('___INVALID' extends keyof NitroV3 ? false : true) + : false + +export type AnyNitro = HasNitroV2 extends true + ? (HasNitroV3 extends true ? NitroV2 | NitroV3 : NitroV2) + : NitroV3 + +export type AnyStorageMounts = HasNitroV2 extends true + ? (HasNitroV3 extends true ? StorageMountsV2 | StorageMountsV3 : StorageMountsV2) + : StorageMountsV3 diff --git a/packages/devtools-kit/src/_types/rpc.ts b/packages/devtools-kit/src/_types/rpc.ts index 0361d39d5c..20476862d2 100644 --- a/packages/devtools-kit/src/_types/rpc.ts +++ b/packages/devtools-kit/src/_types/rpc.ts @@ -1,10 +1,10 @@ -import type { Nitro, StorageMounts } from 'nitropack' import type { Component, NuxtApp, NuxtLayout, NuxtOptions, NuxtPage } from 'nuxt/schema' import type { StorageValue } from 'unstorage' import type { ResolvedConfig } from 'vite' import type { AnalyzeBuildsInfo } from './analyze-build' import type { ModuleCustomTab } from './custom-tabs' import type { AssetEntry, AssetInfo, AutoImportsWithMetadata, ComponentRelationship, HookInfo, ImageMeta, NpmCommandOptions, NpmCommandType, PackageUpdateInfo, ScannedNitroTasks, ServerRouteInfo } from './integrations' +import type { AnyNitro, AnyStorageMounts } from './nitro-compat' import type { NuxtDevtoolsNotifyInput } from './notify' import type { ModuleOptions, NuxtDevToolsOptions } from './options' import type { InstallModuleReturn, ServerDebugContext } from './server-ctx' @@ -47,7 +47,7 @@ export interface ServerFunctions { revealTerminal: (id: string) => Promise // Storage - getStorageMounts: () => Promise + getStorageMounts: () => Promise getStorageKeys: (base?: string) => Promise getStorageItem: (key: string) => Promise setStorageItem: (key: string, value: StorageValue) => Promise @@ -106,7 +106,7 @@ export interface ClientFunctions { */ export interface NuxtServerData { nuxt: NuxtOptions - nitro?: Nitro['options'] + nitro?: AnyNitro['options'] vite: { server?: ResolvedConfig client?: ResolvedConfig diff --git a/packages/devtools/client/components/ComponentsList.vue b/packages/devtools/client/components/ComponentsList.vue index 82dd195e1a..434c1ac5db 100644 --- a/packages/devtools/client/components/ComponentsList.vue +++ b/packages/devtools/client/components/ComponentsList.vue @@ -4,7 +4,7 @@ import type { ComponentRelationship, ComponentWithRelationships } from '../../ty import Fuse from 'fuse.js' import { computed, ref } from 'vue' import { DETAILS_MAX_ITEMS } from '~/composables/constants' -import { getComponentRelationships, getModuleNameFromPath, isNodeModulePath } from '~/composables/utils' +import { getComponentRelationships, getModuleNameFromPath, isNodeModulePath, isNuxtPackageName } from '~/composables/utils' const props = defineProps<{ components: Component[] @@ -73,7 +73,7 @@ const filtered = computed(() => { const name = getModuleNameFromPath(c.filePath) if (!name) return - if (name === 'nuxt') { + if (isNuxtPackageName(name)) { c.meta ??= {} c.meta.docs ??= builtinComponentDocs?.[c.pascalName as keyof typeof builtinComponentDocs] builtin.push(component) diff --git a/packages/devtools/client/components/ModuleTreeNode.vue b/packages/devtools/client/components/ModuleTreeNode.vue index c0b9532df0..340be954ee 100644 --- a/packages/devtools/client/components/ModuleTreeNode.vue +++ b/packages/devtools/client/components/ModuleTreeNode.vue @@ -1,5 +1,6 @@