Skip to content
Open
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
18 changes: 17 additions & 1 deletion .github/workflows/ecosystem-playground.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions packages/devtools-kit/build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export default defineBuildConfig({
'nuxt/schema',
'@nuxt/schema',
'nitropack',
'nitro',
'nitro/types',
'unimport',
'unstorage',
'ofetch',
Expand Down
12 changes: 12 additions & 0 deletions packages/devtools-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
26 changes: 26 additions & 0 deletions packages/devtools-kit/src/_types/nitro-compat.ts
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions packages/devtools-kit/src/_types/rpc.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -47,7 +47,7 @@ export interface ServerFunctions {
revealTerminal: (id: string) => Promise<boolean>

// Storage
getStorageMounts: () => Promise<StorageMounts>
getStorageMounts: () => Promise<AnyStorageMounts>
getStorageKeys: (base?: string) => Promise<string[]>
getStorageItem: (key: string) => Promise<StorageValue>
setStorageItem: (key: string, value: StorageValue) => Promise<void>
Expand Down Expand Up @@ -106,7 +106,7 @@ export interface ClientFunctions {
*/
export interface NuxtServerData {
nuxt: NuxtOptions
nitro?: Nitro['options']
nitro?: AnyNitro['options']
vite: {
server?: ResolvedConfig
client?: ResolvedConfig
Expand Down
4 changes: 2 additions & 2 deletions packages/devtools/client/components/ComponentsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion packages/devtools/client/components/ModuleTreeNode.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { TreeNode } from '~/composables/tree'
import { computed } from 'vue'
import { useRoute } from '#app/composables/router'

withDefaults(defineProps<{
Expand All @@ -10,6 +11,10 @@ withDefaults(defineProps<{
})

const route = useRoute()
// This generic tree node renders on several routes; only some carry an `id`
// param, so Nuxt's typed-routes union for `route.params` doesn't include it.
// Read it defensively rather than assuming the current route's param shape.
const activeId = computed(() => (route.params as { id?: string }).id)
</script>

<template>
Expand All @@ -35,7 +40,7 @@ const route = useRoute()

p="x2 y1"
block rounded text-sm
:class="{ 'bg-gray/10': i.file === route.params.id }"
:class="{ 'bg-gray/10': i.file === activeId }"
>
<FileIcon :id="i.file" />
<span ml-1>
Expand Down
15 changes: 14 additions & 1 deletion packages/devtools/client/composables/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,23 @@ function getModuleSubpathFromPath(path: string) {
return match
}

/**
* The `nuxt` package name itself, as it appears in `node_modules` / package
* names. Aliased installs (`"nuxt": "npm:nuxt-nightly@5x"`, the older
* `nuxt-edge` pre-release channel, ...) physically install under a different
* package name, so anything that special-cases "is this Nuxt's own code"
* needs to check all of them, not just the literal `'nuxt'` string.
*/
export const NUXT_PACKAGE_NAMES = new Set(['nuxt', 'nuxt-nightly', 'nuxt-edge'])

export function isNuxtPackageName(name: string | undefined) {
return !!name && NUXT_PACKAGE_NAMES.has(name)
}

export function isBuiltInModule(name: string | undefined) {
if (!name)
return
return ['nuxt', '#app', '#head', 'vue'].includes(name)
return isNuxtPackageName(name) || ['#app', '#head', 'vue'].includes(name)
}

export function parseReadablePath(path: string, root: string) {
Expand Down
7 changes: 6 additions & 1 deletion packages/devtools/client/modules/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ export default defineNuxtModule({
}
},
})
}, { server: false, client: true })
// `prepend: true` because Nuxt's `addVitePlugin` hard-codes `enforce`
// to `post` for non-isomorphic (client-only/server-only) plugins unless
// prepended — which would otherwise discard `unplugin-vue-markdown`'s
// own `enforce: 'pre'` and let `@vitejs/plugin-vue` see raw markdown
// before it's transformed into a Vue SFC.
}, { server: false, client: true, prepend: true })
},
})
4 changes: 3 additions & 1 deletion packages/devtools/client/pages/modules/error.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ const HTML_TAG_RE = /<.*?>/g

const client = useClient()
const error = computed(() => {
const err = client.value?.nuxt?.payload?.error as {
// `NuxtError`'s shape no longer overlaps enough with this loose display
// type for a direct cast; go through `unknown` as TS suggests.
const err = client.value?.nuxt?.payload?.error as unknown as {
url?: string
statusCode?: number
statusMessage?: string
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools/client/server/api/echo.post.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { defineEventHandler, readBody } from 'h3'

export default defineEventHandler(async (ctx) => {
const body = await readBody<Record<string, unknown>>(ctx)
return {
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools/client/server/api/hello.get.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { defineEventHandler } from 'h3'

export default defineEventHandler(() => {
return {
msg: 'Hello World!',
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools/client/server/tasks/collection/1.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { defineTask } from 'nitro/task'

export default defineTask({
meta: {
name: 'collection:1',
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools/client/server/tasks/collection/2.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { defineTask } from 'nitro/task'

export default defineTask({
meta: {
name: 'collection:2',
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools/client/server/tasks/echo.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { defineTask } from 'nitro/task'

export default defineTask({
meta: {
name: 'echo',
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools/client/server/tasks/ping.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { defineTask } from 'nitro/task'

export default defineTask({
meta: {
name: 'ping',
Expand Down
13 changes: 12 additions & 1 deletion packages/devtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,17 @@
"prepack": "pnpm build"
},
"peerDependencies": {
"vite": "^8.0.14"
"nitro": "^3.0.0-0",
"nitropack": "^2.0.0",
"vite": "^8.1.5"
},
"peerDependenciesMeta": {
"nitro": {
"optional": true
},
"nitropack": {
"optional": true
}
},
"dependencies": {
"@devframes/plugin-code-server": "catalog:prod",
Expand Down Expand Up @@ -106,6 +116,7 @@
"lightningcss": "catalog:buildtools",
"markdown-it": "catalog:frontend",
"markdown-it-link-attributes": "catalog:frontend",
"nitro": "catalog:buildtools",
"nitropack": "catalog:buildtools",
"nuxt": "catalog:buildtools",
"ofetch": "catalog:frontend",
Expand Down
6 changes: 3 additions & 3 deletions packages/devtools/src/integrations/data-inspector.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Nitro } from 'nitropack'
import type { Nuxt } from 'nuxt/schema'
import type { ResolvedConfig, ViteDevServer } from 'vite'
import type { NuxtDevtoolsServerContext, NuxtServerData } from '../types'
import type { AnyNitro } from '../utils/nitro-compat'
import { createDataInspectorDevframe, registerDataSource } from '@devframes/plugin-data-inspector'
import { deprecate, NUXT_DEVTOOLS_GROUP_ID, onDevtoolsReady } from '@nuxt/devtools-kit'
import { mountDevframe } from '@vitejs/devtools-kit/node'
Expand All @@ -18,7 +18,7 @@ import { mountDevframe } from '@vitejs/devtools-kit/node'
* process is unsupported.
*/
interface CapturedServerData {
nitro?: Nitro
nitro?: AnyNitro
vite?: ViteDevServer
}

Expand Down Expand Up @@ -65,7 +65,7 @@ export function setup(ctx: NuxtDevtoolsServerContext): void {
const { nuxt } = ctx

// Capture raw Nitro options once Nitro is created.
nuxt.hook('nitro:build:before', (nitro) => {
nuxt.hook('nitro:build:before', (nitro: AnyNitro) => {
captured.nitro = nitro
})

Expand Down
24 changes: 19 additions & 5 deletions packages/devtools/src/module-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ServerResponse } from 'node:http'
import type { Nuxt } from 'nuxt/schema'
import type { Plugin } from 'vite'
import type { ModuleOptions, NuxtDevToolsOptions } from './types'
import type { AnyNitroConfig } from './utils/nitro-compat'
import { existsSync } from 'node:fs'
import fs from 'node:fs/promises'
import os from 'node:os'
Expand Down Expand Up @@ -169,15 +170,28 @@ export async function enableModule(options: ModuleOptions, nuxt: Nuxt) {
},
})

nuxt.hook('nitro:config', (config) => {
nuxt.hook('nitro:config', (config: AnyNitroConfig) => {
// Check user opted-in for tasks
if (config.experimental?.tasks)
setServerTasksEnabledByDefault(true)

// Inject inline script
config.externals = config.externals || {}
config.externals.inline = config.externals.inline || []
config.externals.inline.push(join(runtimeDir, 'nitro'))
// Inject inline script. Force our small runtime plugin to be bundled
// rather than externalized as a node_modules import at runtime — Nitro v2
// (`nitropack`) and Nitro v3 (`nitro`) expose this via different config
// shapes (`externals.inline` vs `noExternals`), so `config`'s type here is
// a union of both; handle whichever applies.
const inlinePath = join(runtimeDir, 'nitro')
if ('externals' in config) {
config.externals ||= {}
config.externals.inline ||= []
config.externals.inline.push(inlinePath)
}
if ('noExternals' in config && Array.isArray(config.noExternals)) {
config.noExternals.push(inlinePath)
}
else if (!('noExternals' in config) || config.noExternals === undefined) {
config.noExternals = [inlinePath]
}
config.virtual = config.virtual || {}
config.virtual['#nuxt-devtools-inline'] = `export const script = \`
if (!window.__NUXT_DEVTOOLS_TIME_METRIC__) {
Expand Down
6 changes: 6 additions & 0 deletions packages/devtools/src/runtime/nitro/inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import type { NitroAppPlugin } from 'nitropack'
import { script } from '#nuxt-devtools-inline'

export default <NitroAppPlugin> function (nitro) {
// `render:html` is still called at runtime regardless of Nitro engine (it's
// part of Nuxt's own render handler, layered atop Nitro v2/v3 alike) but
// `@nuxt/nitro-server-nightly`'s type augmentations currently only target
// Nitro v3's `nitro/types` module, leaving `nitropack`'s `NitroRuntimeHooks`
// (used here) without it. Suppress until the nightly restores that augmentation.
// @ts-expect-error see comment above
nitro.hooks.hook('render:html', (htmlContext) => {
htmlContext.head.push(`<script>${script}</script>`)
})
Expand Down
Loading