diff --git a/bench/README.md b/bench/README.md index 32586a8ad..225fe3b71 100644 --- a/bench/README.md +++ b/bench/README.md @@ -41,7 +41,7 @@ pnpm bench:cli --suite startup --startup-reps 31 ## Suites - **`startup`** - cold `nuxi --version`, `--help`, and unknown-command time. Catches anything pulled onto the module graph of the fast paths. -- **`modules`** - how many modules each target loads for those same commands, via `bench/lib/module-hook.mjs`. A count rather than a time, so it is stable across machines and is the better regression guard of the two. +- **`modules`** - how many modules (and Node.js built-ins) each target loads for those same commands, via `bench/lib/module-hook.mjs`. A count rather than a time, so it is stable across machines and is the better regression guard of the two. - **`panel`** - time from spawn to the first painted `nuxt dev` frame, and until a keypress at that frame is answered. - **`dev`** - time from spawn to the dev server answering a request. - **`restart`** - time to serve again after a `nuxt.config.ts` edit, plus a no-op edit case to check that ignored changes stay ignored. diff --git a/bench/lib/module-hook.mjs b/bench/lib/module-hook.mjs index 625872bd6..ba5a671b8 100644 --- a/bench/lib/module-hook.mjs +++ b/bench/lib/module-hook.mjs @@ -2,6 +2,7 @@ import module from 'node:module' import process from 'node:process' const loaded = new Map() +const bootBuiltins = new Set(process.moduleLoadList) module.registerHooks({ load(url, context, nextLoad) { @@ -25,5 +26,6 @@ process.on('exit', () => { } } // `_rawDebug` writes straight to fd 2 and cannot be intercepted by the CLI's own logging - process._rawDebug(`__BENCH_MODULES__${JSON.stringify({ modules: loaded.size, nodeModules, bytes })}`) + const builtins = process.moduleLoadList.filter(entry => entry.startsWith('NativeModule ') && !bootBuiltins.has(entry)).length + process._rawDebug(`__BENCH_MODULES__${JSON.stringify({ modules: loaded.size, nodeModules, bytes, builtins })}`) }) diff --git a/bench/run.ts b/bench/run.ts index d59060cdc..6798a2d6a 100644 --- a/bench/run.ts +++ b/bench/run.ts @@ -149,7 +149,10 @@ for (const [caseId, label] of STARTUP_CASES) { const baseline = moduleResults?.find(r => r.case === caseId && r.target === 'baseline') const head = moduleResults?.find(r => r.case === caseId && r.target === 'head') if (baseline && head) { - headlineRows.push([`${label} modules loaded`, String(baseline.modules), String(head.modules), formatDelta(baseline.modules, head.modules)]) + headlineRows.push( + [`${label} modules loaded`, String(baseline.modules), String(head.modules), formatDelta(baseline.modules, head.modules)], + [`${label} built-ins loaded`, String(baseline.builtins), String(head.builtins), formatDelta(baseline.builtins, head.builtins)], + ) } } if (footprintResults) { diff --git a/bench/suites/modules.ts b/bench/suites/modules.ts index cfe8fefb8..d1be8af54 100644 --- a/bench/suites/modules.ts +++ b/bench/suites/modules.ts @@ -19,6 +19,8 @@ export interface ModuleCount { modules: number nodeModules: number bytes: number + /** Node.js built-ins loaded after bootstrap, internals included. */ + builtins: number } export async function modulesSuite(targets: Target[], cwd: string): Promise<{ results: ModuleCount[], markdown: string }> { @@ -36,7 +38,7 @@ export async function modulesSuite(targets: Target[], cwd: string): Promise<{ re if (!match) { throw new Error(`no module stats for ${target.id} ${testCase.id}:\n${result.stderr.slice(-2000)}`) } - const parsed = JSON.parse(match[1]!) as { modules: number, nodeModules: number, bytes: number } + const parsed = JSON.parse(match[1]!) as { modules: number, nodeModules: number, bytes: number, builtins: number } const entry = { case: testCase.id, target: target.id, ...parsed } results.push(entry) perTarget.push(entry) @@ -50,14 +52,17 @@ export async function modulesSuite(targets: Target[], cwd: string): Promise<{ re formatBytes(baseline!.bytes), formatBytes(head!.bytes), formatDelta(baseline!.bytes, head!.bytes), + String(baseline!.builtins), + String(head!.builtins), + formatDelta(baseline!.builtins, head!.builtins), ]) } const markdown = [ - 'Counted with a `module.registerHooks` load hook, compile cache disabled. Counts every JS module actually evaluated on that code path (built-ins excluded, native addons excluded).', + 'Counted with a `module.registerHooks` load hook, compile cache disabled. Counts every JS module actually evaluated on that code path (native addons excluded). Built-ins loaded after bootstrap are counted separately, including the internal modules they load.', '', markdownTable( - ['Command', `${shortLabel(targets[0]!)} modules`, `${shortLabel(targets[1]!)} modules`, 'Delta', `${shortLabel(targets[0]!)} source bytes`, `${shortLabel(targets[1]!)} source bytes`, 'Delta'], + ['Command', `${shortLabel(targets[0]!)} modules`, `${shortLabel(targets[1]!)} modules`, 'Delta', `${shortLabel(targets[0]!)} source bytes`, `${shortLabel(targets[1]!)} source bytes`, 'Delta', `${shortLabel(targets[0]!)} built-ins`, `${shortLabel(targets[1]!)} built-ins`, 'Delta'], rows, ), ].join('\n') diff --git a/packages/nuxt-cli/src/commands/docs.ts b/packages/nuxt-cli/src/commands/docs.ts index 35f84647e..60726221a 100644 --- a/packages/nuxt-cli/src/commands/docs.ts +++ b/packages/nuxt-cli/src/commands/docs.ts @@ -8,7 +8,6 @@ import { defineCommand } from 'citty' import { openBrowser } from '../dev/listen' import { releaseStdin, withDirectStdout } from '../utils/console' -import { DOCS_BASE_URL, DOCS_PATH, resolveDocsIndex } from '../utils/docs-index' import { logger } from '../utils/logger' import { resolveRootDir } from '../utils/paths' import { withSpinner } from '../utils/spinner' @@ -37,9 +36,10 @@ export default defineCommand({ }, }, async run(ctx) { + const { DOCS_BASE_URL, DOCS_PATH, resolveDocsIndex } = await import('../utils/docs-index') const query = ctx.args._.join(' ').trim() if (!query) { - return visit(DOCS_PATH, ctx.args.open) + return visit(DOCS_BASE_URL + DOCS_PATH, ctx.args.open) } const found = await withSpinner(`Searching the Nuxt documentation for ${styleText('cyan', query)}`, async (spinner) => { @@ -58,13 +58,13 @@ export default defineCommand({ if (!found) { logger.warn(`Could not read the Nuxt documentation for this project. Run again with ${styleText('cyan', 'DEBUG=nuxi*')} to see why.`) - return visit(DOCS_PATH, ctx.args.open) + return visit(DOCS_BASE_URL + DOCS_PATH, ctx.args.open) } const { index, results } = found if (results.length === 0) { logger.warn(`Nothing in the Nuxt ${index.version} documentation matches ${styleText('cyan', query)}.`) - return visit(DOCS_PATH, ctx.args.open) + return visit(DOCS_BASE_URL + DOCS_PATH, ctx.args.open) } if (results.length > 1 && ctx.args.open && isInteractive()) { @@ -82,7 +82,7 @@ export default defineCommand({ cancel(`Nuxt documentation: ${DOCS_BASE_URL}${index.base}${results[0]!.path}`) return } - return visit(index.base + choice, true) + return visit(DOCS_BASE_URL + index.base + choice, true) } const width = Math.max(...results.map(entry => entry.title.length)) @@ -92,12 +92,11 @@ export default defineCommand({ }) process.stdout.write(`${lines.join('\n')}\n`) - return visit(index.base + results[0]!.path, ctx.args.open) + return visit(DOCS_BASE_URL + index.base + results[0]!.path, ctx.args.open) }, }) -function visit(path: string, open: boolean): void { - const url = DOCS_BASE_URL + path +function visit(url: string, open: boolean): void { logger.info(`${open ? 'Opening' : 'Nuxt documentation:'} ${styleText('cyan', url)}`) if (open) { openBrowser(url) diff --git a/packages/nuxt-cli/src/commands/info.ts b/packages/nuxt-cli/src/commands/info.ts index 2831860b7..640af78a3 100644 --- a/packages/nuxt-cli/src/commands/info.ts +++ b/packages/nuxt-cli/src/commands/info.ts @@ -14,7 +14,6 @@ import { writeText } from 'tinyclip' import { version as nuxiVersion } from '../../package.json' import { getBuilder } from '../utils/banner' -import { resolveCatalogEntry } from '../utils/catalog' import { printJson } from '../utils/console' import { formatInfoBox } from '../utils/formatting' import { logger } from '../utils/logger' @@ -186,6 +185,7 @@ async function resolveDependencyVersion( return pkg.version } } + const { resolveCatalogEntry } = await import('../utils/catalog') return resolveCatalogEntry(cwd, projectPkg, name)?.specifier ?? dependencies[name] ?? devDependencies[name] diff --git a/packages/nuxt-cli/src/commands/module/add.ts b/packages/nuxt-cli/src/commands/module/add.ts index ef1d36f10..2cde25317 100644 --- a/packages/nuxt-cli/src/commands/module/add.ts +++ b/packages/nuxt-cli/src/commands/module/add.ts @@ -19,7 +19,6 @@ import { createInstallLog, isVerboseInstall, runInstall, takeUnreportedIgnoredBu import { logger } from '../../utils/logger' import { logNetworkError } from '../../utils/network' import { defaultPackageManager, detectPackageManager, isPackageManagerName, packageManagerNames } from '../../utils/package-managers' -import { detectNpmRegistry } from '../../utils/registry' import { createSpinner } from '../../utils/spinner' import { getNuxtVersion } from '../../utils/versions' import { cwdArgs, logLevelArgs } from '../_shared' @@ -403,6 +402,7 @@ async function resolveModule(moduleName: string, cwd: string, modulesDB: NuxtMod let version = pkgVersion || 'latest' const pkgScope = pkgName.startsWith('@') ? pkgName.split('/')[0]! : null + const { detectNpmRegistry } = await import('../../utils/registry') const meta = await detectNpmRegistry(pkgScope, cwd) const headers: HeadersInit = meta.authorization ? { Authorization: meta.authorization } : {} diff --git a/packages/nuxt-cli/src/commands/upgrade.ts b/packages/nuxt-cli/src/commands/upgrade.ts index b64af14ed..3ae567c73 100644 --- a/packages/nuxt-cli/src/commands/upgrade.ts +++ b/packages/nuxt-cli/src/commands/upgrade.ts @@ -11,7 +11,6 @@ import { cancel, isCancel, note, select } from '@clack/prompts' import { defineCommand } from 'citty' import { dirname, relative, resolve } from 'pathe' import { findWorkspaceDir, readPackageJSON } from 'pkg-types' -import { resolveCatalogEntry, updateCatalogEntries } from '../utils/catalog' import { createInstallLog, runDedupe, runInstall, takeUnreportedIgnoredBuilds } from '../utils/install' import { loadKit } from '../utils/kit' @@ -173,7 +172,7 @@ export default defineCommand({ intro(styleText('cyan', 'Upgrading Nuxt ...')) - const [packageManager, workspaceDir = cwd] = await Promise.all([detectPackageManager(cwd), findWorkspaceDir(cwd, { try: true })]) + const [packageManager, workspaceDir = cwd, { resolveCatalogEntry, updateCatalogEntries }] = await Promise.all([detectPackageManager(cwd), findWorkspaceDir(cwd, { try: true }), import('../utils/catalog')]) if (!packageManager) { logger.error( `Unable to determine the package manager used by this project.\n\nNo lock files found in ${styleText('cyan', relativeToProcess(cwd))}, and no ${styleText('cyan', 'packageManager')} field specified in ${styleText('cyan', 'package.json')}.`, diff --git a/packages/nuxt-cli/src/dev/tui/index.ts b/packages/nuxt-cli/src/dev/tui/index.ts index bea9c8ad1..bf296ff6a 100644 --- a/packages/nuxt-cli/src/dev/tui/index.ts +++ b/packages/nuxt-cli/src/dev/tui/index.ts @@ -16,11 +16,11 @@ import { styleText } from 'node:util' import { resolveStackVersions } from '../../utils/banner' import { withDirectStdout } from '../../utils/console' import { formatDuration, terminalLink } from '../../utils/formatting' +import { releaseNotesUrl } from '../../utils/release-notes' import { startupElapsedMs } from '../../utils/startup-clock' import { registerTerminalHost } from '../../utils/terminal-host' import { MUTED, paint } from '../../utils/terminal-theme' -import { checkForUpdate, isUpdateCheckEnabled, releaseNotesUrl } from '../../utils/update-check' import { openBrowser, writeClipboard } from '../listen' import { setupShortcuts } from '../shortcuts' import { isShutdownAdopted } from '../shutdown' @@ -843,10 +843,14 @@ async function resolveQRCode(context: ShortcutContext): Promise { - if (!current || !isUpdateCheckEnabled()) { + if (!current) { return undefined } try { + const { checkForUpdate, isUpdateCheckEnabled } = await import('../../utils/update-check') + if (!isUpdateCheckEnabled()) { + return undefined + } const update = await checkForUpdate('nuxt', current) return update?.latest } diff --git a/packages/nuxt-cli/src/utils/network.ts b/packages/nuxt-cli/src/utils/network.ts index 5b8ef55ce..e97cfe6f2 100644 --- a/packages/nuxt-cli/src/utils/network.ts +++ b/packages/nuxt-cli/src/utils/network.ts @@ -1,4 +1,3 @@ -import http from 'node:http' import process from 'node:process' import { styleText } from 'node:util' @@ -40,7 +39,10 @@ export type ProxySetupResult = 'unused' | 'active' | 'children-only' type EnableGlobalProxy = (env: NodeJS.ProcessEnv) => unknown -const setGlobalProxyFromEnv = (http as { setGlobalProxyFromEnv?: EnableGlobalProxy }).setGlobalProxyFromEnv +// `node:http` loads tls, crypto, http2 and undici, so it stays off the startup path. +function getGlobalProxySetter(): EnableGlobalProxy | null { + return (process.getBuiltinModule('node:http') as { setGlobalProxyFromEnv?: EnableGlobalProxy }).setGlobalProxyFromEnv ?? null +} let envProxyActive: boolean | undefined let proxyHintShown = false @@ -49,7 +51,7 @@ let proxyHintShown = false * Route requests from this process and its children through the proxy * environment variables, and record whether this process is proxy-aware. */ -export function setupProxySupport(env: NodeJS.ProcessEnv = process.env, enableGlobalProxy: EnableGlobalProxy | null = setGlobalProxyFromEnv ?? null): ProxySetupResult { +export function setupProxySupport(env: NodeJS.ProcessEnv = process.env, enableGlobalProxy?: EnableGlobalProxy | null): ProxySetupResult { proxyHintShown = false if (!hasProxyEnv(env)) { @@ -57,9 +59,12 @@ export function setupProxySupport(env: NodeJS.ProcessEnv = process.env, enableGl return 'unused' } envProxyActive = isEnvProxyActive(env) - if (!envProxyActive && enableGlobalProxy && env.NODE_USE_ENV_PROXY !== '0') { - enableGlobalProxy(env) - envProxyActive = true + if (!envProxyActive && env.NODE_USE_ENV_PROXY !== '0') { + const enable = enableGlobalProxy === undefined ? getGlobalProxySetter() : enableGlobalProxy + if (enable) { + enable(env) + envProxyActive = true + } } env.NODE_USE_ENV_PROXY ||= '1' diff --git a/packages/nuxt-cli/src/utils/registry.ts b/packages/nuxt-cli/src/utils/registry.ts index 0c70b7cda..1fb3366d0 100644 --- a/packages/nuxt-cli/src/utils/registry.ts +++ b/packages/nuxt-cli/src/utils/registry.ts @@ -6,7 +6,7 @@ import { homedir } from 'node:os' import { join } from 'node:path' import process from 'node:process' -import { parseINI } from 'confbox' +import { parseINI } from 'confbox/ini' const TRAILING_SLASH_RE = /\/$/ const ENV_REFERENCE_RE = /\$\{([^}]+)\}/g diff --git a/packages/nuxt-cli/src/utils/release-notes.ts b/packages/nuxt-cli/src/utils/release-notes.ts new file mode 100644 index 000000000..617e4f23d --- /dev/null +++ b/packages/nuxt-cli/src/utils/release-notes.ts @@ -0,0 +1,19 @@ +/** Repositories whose releases are published under a `v`-prefixed tag. */ +const RELEASE_REPOS: Record = { + 'nuxt': 'nuxt/nuxt', + '@nuxt/cli': 'nuxt/cli', + 'nuxi': 'nuxt/cli', + 'create-nuxt': 'nuxt/cli', +} + +/** + * The release notes for a published version, when the package has a known + * repository. Nightlies are skipped: their versions have no matching tag. + */ +export function releaseNotesUrl(pkg: string, version: string): string | undefined { + const repo = RELEASE_REPOS[pkg] + if (!repo || version.includes('nightly')) { + return undefined + } + return `https://github.com/${repo}/releases/tag/v${version}` +} diff --git a/packages/nuxt-cli/src/utils/update-check.ts b/packages/nuxt-cli/src/utils/update-check.ts index ef875562a..5989ec53f 100644 --- a/packages/nuxt-cli/src/utils/update-check.ts +++ b/packages/nuxt-cli/src/utils/update-check.ts @@ -10,6 +10,7 @@ import { fetchJson } from './fetch' import { terminalLink } from './formatting' import { debug, writeNotice } from './logger' import { detectNpmRegistry } from './registry' +import { releaseNotesUrl } from './release-notes' import { trackOutputSpacing } from './stdout' const RC_FILE = '.nuxtrc' @@ -148,26 +149,6 @@ export interface UpdateNudgeOptions { pkg?: string } -/** Repositories whose releases are published under a `v`-prefixed tag. */ -const RELEASE_REPOS: Record = { - 'nuxt': 'nuxt/nuxt', - '@nuxt/cli': 'nuxt/cli', - 'nuxi': 'nuxt/cli', - 'create-nuxt': 'nuxt/cli', -} - -/** - * The release notes for a published version, when the package has a known - * repository. Nightlies are skipped: their versions have no matching tag. - */ -export function releaseNotesUrl(pkg: string, version: string): string | undefined { - const repo = RELEASE_REPOS[pkg] - if (!repo || version.includes('nightly')) { - return undefined - } - return `https://github.com/${repo}/releases/tag/v${version}` -} - function describeUpdate({ current, latest }: NuxtUpdate, name: string, pkg: string): string { const notes = releaseNotesUrl(pkg, latest) const version = styleText('green', latest) diff --git a/packages/nuxt-cli/src/utils/versions.ts b/packages/nuxt-cli/src/utils/versions.ts index 1562731f4..39ddfe44d 100644 --- a/packages/nuxt-cli/src/utils/versions.ts +++ b/packages/nuxt-cli/src/utils/versions.ts @@ -1,12 +1,10 @@ import { readPackageJSON } from 'pkg-types' import { coerce, findMaxSatisfying, normalize } from 'verkit' -import { resolveCatalogEntry } from './catalog' import { fetchJson } from './fetch' import { debug } from './logger' import { NUXT_PACKAGES } from './nuxt-packages' import { readDependencyPackageJson } from './package-json' -import { detectNpmRegistry, PUBLIC_REGISTRY } from './registry' /** How long to wait on the registry before giving up on a version lookup. */ const FETCH_TIMEOUT = 10_000 @@ -27,7 +25,7 @@ export async function getNuxtVersion(cwd: string) { if (nuxtPkg?.version && NUXT_PACKAGE_NAMES.has(nuxtPkg.name!)) { return nuxtPkg.version } - const pkg = await readPackageJSON(cwd) + const [pkg, { resolveCatalogEntry }] = await Promise.all([readPackageJSON(cwd), import('./catalog')]) const pkgDep = resolveCatalogEntry(cwd, pkg, 'nuxt')?.specifier ?? (pkg?.dependencies?.nuxt || pkg?.devDependencies?.nuxt) const coerced = pkgDep && coerce(pkgDep) @@ -41,6 +39,7 @@ export async function getNuxtVersion(cwd: string) { */ export async function resolveRegistryVersion(pkg: string, range: string): Promise { const scope = pkg.startsWith('@') ? pkg.split('/')[0]! : null + const { detectNpmRegistry, PUBLIC_REGISTRY } = await import('./registry') const { registry, authorization } = await detectNpmRegistry(scope) const packument = await fetchPackument(pkg, registry, authorization) diff --git a/packages/nuxt-cli/test/unit/utils/release-notes.spec.ts b/packages/nuxt-cli/test/unit/utils/release-notes.spec.ts index 0f751c1d5..6d992cfce 100644 --- a/packages/nuxt-cli/test/unit/utils/release-notes.spec.ts +++ b/packages/nuxt-cli/test/unit/utils/release-notes.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { terminalLink } from '../../../src/utils/formatting' -import { releaseNotesUrl } from '../../../src/utils/update-check' +import { releaseNotesUrl } from '../../../src/utils/release-notes' describe('release notes links', () => { it('points at the tag for packages with a known repository', () => {