diff --git a/.changeset/fancy-cases-act.md b/.changeset/fancy-cases-act.md new file mode 100644 index 000000000000..ab7a50da0d7b --- /dev/null +++ b/.changeset/fancy-cases-act.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Improves `astro info` diff --git a/packages/astro/src/cli/flags.ts b/packages/astro/src/cli/flags.ts index 0eaa70ac043f..5e1ea2f9258f 100644 --- a/packages/astro/src/cli/flags.ts +++ b/packages/astro/src/cli/flags.ts @@ -6,6 +6,7 @@ import type { AstroInlineConfig } from '../types/public/config.js'; // Alias for now, but allows easier migration to node's `parseArgs` in the future. export type Flags = Arguments; +/** @deprecated Use AstroConfigResolver instead */ export function flagsToAstroInlineConfig(flags: Flags): AstroInlineConfig { return { // Inline-only configs diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index 301824325716..a9cfc2fb873f 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -96,9 +96,65 @@ async function runCommand(cmd: string, flags: yargs.Arguments) { return; } case 'info': { - const { printInfo } = await import('./info/index.js'); - await printInfo({ flags }); - return; + const [ + { createProcessOperatingSystemProvider }, + { createCliAstroConfigResolver }, + { createProcessPackageManagerUserAgentProvider }, + { createProcessNodeVersionProvider }, + { createCliDebugInfoProvider }, + { createTinyexecCommandExecutor }, + { getPackageManager }, + { createStyledDebugInfoFormatter }, + { createPromptsPrompt }, + { createCliClipboard }, + { createPassthroughTextStyler }, + { infoCommand }, + ] = await Promise.all([ + import('./infra/process-operating-system-provider.js'), + import('./info/infra/cli-astro-config-resolver.js'), + import('./info/infra/process-package-manager-user-agent-provider.js'), + import('./info/infra/process-node-version-provider.js'), + import('./info/infra/cli-debug-info-provider.js'), + import('./infra/tinyexec-command-executor.js'), + import('./info/core/get-package-manager.js'), + import('./info/infra/styled-debug-info-formatter.js'), + import('./info/infra/prompts-prompt.js'), + import('./info/infra/cli-clipboard.js'), + import('./infra/passthrough-text-styler.js'), + import('./info/core/info.js'), + ]); + const operatingSystemProvider = createProcessOperatingSystemProvider(); + const astroConfigResolver = createCliAstroConfigResolver({ flags }); + const commandExecutor = createTinyexecCommandExecutor(); + const packageManagerUserAgentProvider = createProcessPackageManagerUserAgentProvider(); + const nodeVersionProvider = createProcessNodeVersionProvider(); + const debugInfoProvider = createCliDebugInfoProvider({ + config: await astroConfigResolver.resolve(), + astroVersionProvider, + operatingSystemProvider, + packageManager: await getPackageManager({ + packageManagerUserAgentProvider, + commandExecutor, + }), + nodeVersionProvider, + }); + const prompt = createPromptsPrompt({ force: flags.copy }); + const clipboard = createCliClipboard({ + commandExecutor, + logger, + operatingSystemProvider, + prompt, + }); + + return await runner.run(infoCommand, { + logger, + debugInfoProvider, + getDebugInfoFormatter: ({ pretty }) => + createStyledDebugInfoFormatter({ + textStyler: pretty ? textStyler : createPassthroughTextStyler(), + }), + clipboard, + }); } case 'create-key': { const [{ createCryptoKeyGenerator }, { createKeyCommand }] = await Promise.all([ diff --git a/packages/astro/src/cli/info/core/get-package-manager.ts b/packages/astro/src/cli/info/core/get-package-manager.ts new file mode 100644 index 000000000000..0ed1acb9237a --- /dev/null +++ b/packages/astro/src/cli/info/core/get-package-manager.ts @@ -0,0 +1,44 @@ +import type { CommandExecutor } from '../../definitions.js'; +import type { PackageManager, PackageManagerUserAgentProvider } from '../definitions.js'; + +interface Options { + packageManagerUserAgentProvider: PackageManagerUserAgentProvider; + commandExecutor: CommandExecutor; +} + +export async function getPackageManager({ + packageManagerUserAgentProvider, + commandExecutor, +}: Options): Promise { + const userAgent = packageManagerUserAgentProvider.getUserAgent(); + if (!userAgent) { + const { createNoopPackageManager } = await import('../infra/noop-package-manager.js'); + return createNoopPackageManager(); + } + const specifier = userAgent.split(' ')[0]; + const _name = specifier.substring(0, specifier.lastIndexOf('/')); + const name = _name === 'npminstall' ? 'cnpm' : _name; + + switch (name) { + case 'pnpm': { + const { createPnpmPackageManager } = await import('../infra/pnpm-package-manager.js'); + return createPnpmPackageManager({ commandExecutor }); + } + case 'npm': { + const { createNpmPackageManager } = await import('../infra/npm-package-manager.js'); + return createNpmPackageManager({ commandExecutor }); + } + case 'yarn': { + const { createYarnPackageManager } = await import('../infra/yarn-package-manager.js'); + return createYarnPackageManager({ commandExecutor }); + } + case 'bun': { + const { createBunPackageManager } = await import('../infra/bun-package-manager.js'); + return createBunPackageManager(); + } + default: { + const { createNoopPackageManager } = await import('../infra/noop-package-manager.js'); + return createNoopPackageManager(); + } + } +} diff --git a/packages/astro/src/cli/info/core/info.ts b/packages/astro/src/cli/info/core/info.ts new file mode 100644 index 000000000000..4d4135430055 --- /dev/null +++ b/packages/astro/src/cli/info/core/info.ts @@ -0,0 +1,29 @@ +import type { Logger } from '../../../core/logger/core.js'; +import { defineCommand } from '../../domain/command.js'; +import type { Clipboard, DebugInfoFormatter, DebugInfoProvider } from '../definitions.js'; + +interface Options { + debugInfoProvider: DebugInfoProvider; + getDebugInfoFormatter: (options: { pretty: boolean }) => DebugInfoFormatter; + logger: Logger; + clipboard: Clipboard; +} + +export const infoCommand = defineCommand({ + help: { + commandName: 'astro info', + tables: { + Flags: [ + ['--help (-h)', 'See all available flags.'], + ['--copy', 'Force copy of the output.'], + ], + }, + description: + 'Reports useful information about your current Astro environment. Useful for providing information when opening an issue.', + }, + async run({ debugInfoProvider, getDebugInfoFormatter, logger, clipboard }: Options) { + const debugInfo = await debugInfoProvider.get(); + logger.info('SKIP_FORMAT', getDebugInfoFormatter({ pretty: true }).format(debugInfo)); + await clipboard.copy(getDebugInfoFormatter({ pretty: false }).format(debugInfo)); + }, +}); diff --git a/packages/astro/src/cli/info/definitions.ts b/packages/astro/src/cli/info/definitions.ts new file mode 100644 index 000000000000..3a4bbaa45630 --- /dev/null +++ b/packages/astro/src/cli/info/definitions.ts @@ -0,0 +1,35 @@ +import type { AstroConfig } from '../../types/public/index.js'; +import type { DebugInfo } from './domain/debug-info.js'; + +export interface DebugInfoProvider { + get: () => Promise; +} + +export interface DebugInfoFormatter { + format: (info: DebugInfo) => string; +} + +export interface Clipboard { + copy: (text: string) => Promise; +} + +export interface PackageManager { + getName: () => string; + getPackageVersion: (name: string) => Promise; +} + +export interface AstroConfigResolver { + resolve: () => Promise; +} + +export interface Prompt { + confirm: (input: { message: string; defaultValue?: boolean }) => Promise; +} + +export interface PackageManagerUserAgentProvider { + getUserAgent: () => string | null; +} + +export interface NodeVersionProvider { + get: () => string; +} diff --git a/packages/astro/src/cli/info/domain/debug-info.ts b/packages/astro/src/cli/info/domain/debug-info.ts new file mode 100644 index 000000000000..9c60b5c7c5c3 --- /dev/null +++ b/packages/astro/src/cli/info/domain/debug-info.ts @@ -0,0 +1 @@ +export type DebugInfo = Array<[string, string | Array]>; diff --git a/packages/astro/src/cli/info/index.ts b/packages/astro/src/cli/info/index.ts deleted file mode 100644 index c9ff80b408bd..000000000000 --- a/packages/astro/src/cli/info/index.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { spawn, spawnSync } from 'node:child_process'; -import { arch, platform } from 'node:os'; -import colors from 'picocolors'; -import prompts from 'prompts'; -import { resolveConfig } from '../../core/config/index.js'; -import { ASTRO_VERSION } from '../../core/constants.js'; -import type { AstroConfig, AstroUserConfig } from '../../types/public/config.js'; -import { type Flags, flagsToAstroInlineConfig } from '../flags.js'; - -interface InfoOptions { - flags: Flags; -} - -export async function getInfoOutput({ - userConfig, - print, -}: { - userConfig: AstroUserConfig | AstroConfig; - print: boolean; -}): Promise { - const packageManager = getPackageManager(); - - const rows: Array<[string, string | string[]]> = [ - ['Astro', `v${ASTRO_VERSION}`], - ['Node', process.version], - ['System', getSystem()], - ['Package Manager', packageManager], - ]; - - if (print) { - const viteVersion = await getVersion(packageManager, 'vite'); - - if (viteVersion) { - rows.splice(1, 0, ['Vite', viteVersion]); - } - } - - const hasAdapter = 'adapter' in userConfig && userConfig.adapter?.name; - let adapterVersion: string | undefined = undefined; - - if (print && hasAdapter) { - adapterVersion = await getVersion(packageManager, userConfig.adapter!.name); - } - - const adatperOutputString = hasAdapter - ? `${userConfig.adapter!.name}${adapterVersion ? ` (${adapterVersion})` : ''}` - : 'none'; - - try { - rows.push([ - 'Output', - 'adapter' in userConfig && userConfig.output ? userConfig.output : 'static', - ]); - rows.push(['Adapter', adatperOutputString]); - - const integrations = (userConfig?.integrations ?? []) - .filter(Boolean) - .flat() - .map(async (i: any) => { - if (!i.name) return; - if (!print) return i.name; - - const version = await getVersion(packageManager, i.name); - - return `${i.name}${version ? ` (${version})` : ''}`; - }); - - const awaitedIntegrations = (await Promise.all(integrations)).filter(Boolean); - - rows.push(['Integrations', awaitedIntegrations.length > 0 ? awaitedIntegrations : 'none']); - } catch {} - - let output = ''; - for (const [label, value] of rows) { - output += printRow(label, value, print); - } - - return output.trim(); -} - -export async function printInfo({ flags }: InfoOptions) { - const { userConfig } = await resolveConfig(flagsToAstroInlineConfig(flags), 'info'); - const output = await getInfoOutput({ userConfig, print: true }); - await copyToClipboard(output, flags.copy); -} - -async function copyToClipboard(text: string, force?: boolean) { - text = text.trim(); - const system = platform(); - let command = ''; - let args: Array = []; - - if (system === 'darwin') { - command = 'pbcopy'; - } else if (system === 'win32') { - command = 'clip'; - } else { - // Unix: check if a supported command is installed - - const unixCommands: Array<[string, Array]> = [ - ['xclip', ['-selection', 'clipboard', '-l', '1']], - ['wl-copy', []], - ]; - for (const [unixCommand, unixArgs] of unixCommands) { - try { - const output = spawnSync('which', [unixCommand], { encoding: 'utf8' }); - if (output.stdout.trim()) { - command = unixCommand; - args = unixArgs; - break; - } - } catch { - continue; - } - } - } - - if (!command) { - console.error(colors.red('\nClipboard command not found!')); - console.info('Please manually copy the text above.'); - return; - } - - if (!force) { - const { shouldCopy } = await prompts({ - type: 'confirm', - name: 'shouldCopy', - message: 'Copy to clipboard?', - initial: true, - }); - - if (!shouldCopy) return; - } - - try { - const result = spawnSync(command, args, { input: text, stdio: ['pipe', 'ignore', 'ignore'] }); - if (result.error) { - throw result.error; - } - console.info(colors.green('Copied to clipboard!')); - } catch { - console.error( - colors.red(`\nSorry, something went wrong!`) + ` Please copy the text above manually.`, - ); - } -} - -export function readFromClipboard() { - const system = platform(); - let command = ''; - let args: Array = []; - - if (system === 'darwin') { - command = 'pbpaste'; - } else if (system === 'win32') { - command = 'powershell'; - args = ['-command', 'Get-Clipboard']; - } else { - const unixCommands: Array<[string, Array]> = [ - ['xclip', ['-sel', 'clipboard', '-o']], - ['wl-paste', []], - ]; - for (const [unixCommand, unixArgs] of unixCommands) { - try { - const output = spawnSync('which', [unixCommand], { encoding: 'utf8' }); - if (output.stdout.trim()) { - command = unixCommand; - args = unixArgs; - break; - } - } catch { - continue; - } - } - } - - if (!command) { - throw new Error('Clipboard read command not found!'); - } - - const result = spawnSync(command, args, { encoding: 'utf8' }); - if (result.error) { - throw result.error; - } - return result.stdout.trim(); -} - -const PLATFORM_TO_OS: Partial, string>> = { - darwin: 'macOS', - win32: 'Windows', - linux: 'Linux', -}; - -function getSystem() { - const system = PLATFORM_TO_OS[platform()] ?? platform(); - return `${system} (${arch()})`; -} - -function getPackageManager() { - if (!process.env.npm_config_user_agent) { - return 'unknown'; - } - const specifier = process.env.npm_config_user_agent.split(' ')[0]; - const name = specifier.substring(0, specifier.lastIndexOf('/')); - return name === 'npminstall' ? 'cnpm' : name; -} - -const MAX_PADDING = 25; -function printRow(label: string, value: string | string[], print: boolean) { - const padding = MAX_PADDING - label.length; - const [first, ...rest] = Array.isArray(value) ? value : [value]; - let plaintext = `${label}${' '.repeat(padding)}${first}`; - let richtext = `${colors.bold(label)}${' '.repeat(padding)}${colors.green(first)}`; - if (rest.length > 0) { - for (const entry of rest) { - plaintext += `\n${' '.repeat(MAX_PADDING)}${entry}`; - richtext += `\n${' '.repeat(MAX_PADDING)}${colors.green(entry)}`; - } - } - plaintext += '\n'; - if (print) { - console.info(richtext); - } - return plaintext; -} - -function formatPnpmVersionOutput(versionOutput: string): string { - return versionOutput.startsWith('link:') ? 'Local' : `v${versionOutput}`; -} - -type BareNpmLikeVersionOutput = { - version: string; - dependencies: Record; -}; - -async function spawnAsync(executable: string, opts: Array): Promise { - return new Promise((resolve, reject) => { - const child = spawn(executable, opts, { shell: true }); - let stdout = ''; - let stderr = ''; - - child.stdout.on('data', (d) => (stdout += d)); - child.stderr.on('data', (d) => (stderr += d)); - child.on('error', reject); - child.on('close', (code) => { - if (code !== 0) reject(new Error(stderr)); - else resolve(stdout); - }); - }); -} - -async function getVersionUsingPNPM(dependency: string): Promise { - const output = await spawnAsync('pnpm', ['why', dependency, '--json']); - - const parsedOutput = JSON.parse(output) as Array; - - const deps = parsedOutput[0].dependencies; - - if (parsedOutput.length === 0 || !deps) { - return undefined; - } - - const userProvidedDependency = deps[dependency]; - - if (userProvidedDependency) { - return userProvidedDependency.version.startsWith('link:') - ? 'Local' - : `v${userProvidedDependency.version}`; - } - - const astroDependency = deps.astro?.dependencies[dependency]; - return astroDependency ? formatPnpmVersionOutput(astroDependency.version) : undefined; -} - -async function getVersionUsingNPM(dependency: string): Promise { - const output = await spawnAsync('npm', ['ls', dependency, '--json', '--depth=1']); - const parsedNpmOutput = JSON.parse(output) as BareNpmLikeVersionOutput; - - if (!parsedNpmOutput.dependencies) { - return undefined; - } - - if (parsedNpmOutput.dependencies[dependency]) { - return `v${parsedNpmOutput.dependencies[dependency].version}`; - } - - const astro = parsedNpmOutput.dependencies.astro; - return astro ? `v${astro.dependencies[dependency].version}` : undefined; -} - -type YarnVersionOutputLine = { - children: Record; -}; - -function getYarnOutputDepVersion(dependency: string, outputLine: string) { - const parsed = JSON.parse(outputLine) as YarnVersionOutputLine; - - for (const [key, value] of Object.entries(parsed.children)) { - if (key.startsWith(`${dependency}@`)) { - return `v${value.locator.split(':').pop()}`; - } - } -} - -async function getVersionUsingYarn(dependency: string): Promise { - const yarnOutput = await spawnAsync('yarn', ['why', dependency, '--json']); - - const hasUserDefinition = yarnOutput.includes('workspace:.'); - - for (const line of yarnOutput.split('\n')) { - if (hasUserDefinition && line.includes('workspace:.')) - return getYarnOutputDepVersion(dependency, line); - if (!hasUserDefinition && line.includes('astro@')) - return getYarnOutputDepVersion(dependency, line); - } -} - -async function getVersion(packageManager: string, dependency: string): Promise { - try { - switch (packageManager) { - case 'pnpm': - return await getVersionUsingPNPM(dependency); - case 'npm': - return getVersionUsingNPM(dependency); - case 'yarn': - return getVersionUsingYarn(dependency); - case 'bun': - return undefined; - } - - return undefined; - } catch { - return undefined; - } -} diff --git a/packages/astro/src/cli/info/infra/bun-package-manager.ts b/packages/astro/src/cli/info/infra/bun-package-manager.ts new file mode 100644 index 000000000000..6a8036000c93 --- /dev/null +++ b/packages/astro/src/cli/info/infra/bun-package-manager.ts @@ -0,0 +1,12 @@ +import type { PackageManager } from '../definitions.js'; + +export function createBunPackageManager(): PackageManager { + return { + getName() { + return 'bun'; + }, + async getPackageVersion() { + return undefined; + }, + }; +} diff --git a/packages/astro/src/cli/info/infra/cli-astro-config-resolver.ts b/packages/astro/src/cli/info/infra/cli-astro-config-resolver.ts new file mode 100644 index 000000000000..ef5ff6d8dc38 --- /dev/null +++ b/packages/astro/src/cli/info/infra/cli-astro-config-resolver.ts @@ -0,0 +1,51 @@ +import { resolveConfig } from '../../../core/config/config.js'; +import type { Flags } from '../../flags.js'; +import type { AstroConfigResolver } from '../definitions.js'; + +interface Options { + // TODO: find something better + flags: Flags; +} + +export function createCliAstroConfigResolver({ flags }: Options): AstroConfigResolver { + return { + async resolve() { + const { astroConfig } = await resolveConfig( + // TODO: consider testing flags => astro inline config + { + // Inline-only configs + configFile: typeof flags.config === 'string' ? flags.config : undefined, + mode: typeof flags.mode === 'string' ? flags.mode : undefined, + logLevel: flags.verbose ? 'debug' : flags.silent ? 'silent' : undefined, + force: flags.force ? true : undefined, + + // Astro user configs + root: typeof flags.root === 'string' ? flags.root : undefined, + site: typeof flags.site === 'string' ? flags.site : undefined, + base: typeof flags.base === 'string' ? flags.base : undefined, + outDir: typeof flags.outDir === 'string' ? flags.outDir : undefined, + server: { + port: typeof flags.port === 'number' ? flags.port : undefined, + host: + typeof flags.host === 'string' || typeof flags.host === 'boolean' + ? flags.host + : undefined, + open: + typeof flags.open === 'string' || typeof flags.open === 'boolean' + ? flags.open + : undefined, + allowedHosts: + typeof flags.allowedHosts === 'string' + ? flags.allowedHosts.split(',') + : typeof flags.allowedHosts === 'boolean' && flags.allowedHosts === true + ? flags.allowedHosts + : [], + }, + }, + 'info', + ); + + return astroConfig; + }, + }; +} diff --git a/packages/astro/src/cli/info/infra/cli-clipboard.ts b/packages/astro/src/cli/info/infra/cli-clipboard.ts new file mode 100644 index 000000000000..5264324e9c73 --- /dev/null +++ b/packages/astro/src/cli/info/infra/cli-clipboard.ts @@ -0,0 +1,84 @@ +import type { Logger } from '../../../core/logger/core.js'; +import type { CommandExecutor, OperatingSystemProvider } from '../../definitions.js'; +import type { Clipboard, Prompt } from '../definitions.js'; + +interface Options { + operatingSystemProvider: OperatingSystemProvider; + commandExecutor: CommandExecutor; + logger: Logger; + prompt: Prompt; +} + +async function getExecInputForPlatform({ + platform, + commandExecutor, +}: { + commandExecutor: CommandExecutor; + platform: NodeJS.Platform; +}): Promise<[command: string, args?: Array] | null> { + if (platform === 'darwin') { + return ['pbcopy']; + } + if (platform === 'win32') { + return ['clip']; + } + // Unix: check if a supported command is installed + const unixCommands: Array<[string, Array]> = [ + ['xclip', ['-selection', 'clipboard']], + ['wl-copy', []], + ]; + for (const [unixCommand, unixArgs] of unixCommands) { + try { + const { stdout } = await commandExecutor.execute('which', [unixCommand]); + if (stdout.trim()) { + return [unixCommand, unixArgs]; + } + } catch { + continue; + } + } + return null; +} + +export function createCliClipboard({ + operatingSystemProvider, + commandExecutor, + logger, + prompt, +}: Options): Clipboard { + return { + async copy(text) { + text = text.trim(); + const platform = operatingSystemProvider.getName(); + const input = await getExecInputForPlatform({ platform, commandExecutor }); + if (!input) { + logger.warn('SKIP_FORMAT', 'Clipboard command not found!'); + logger.info('SKIP_FORMAT', 'Please manually copy the text above.'); + return; + } + + if ( + !(await prompt.confirm({ + message: 'Copy to clipboard?', + defaultValue: true, + })) + ) { + return; + } + + try { + const [command, args] = input; + await commandExecutor.execute(command, args, { + input: text, + stdio: ['pipe', 'ignore', 'ignore'], + }); + logger.info('SKIP_FORMAT', 'Copied to clipboard!'); + } catch { + logger.error( + 'SKIP_FORMAT', + 'Sorry, something went wrong! Please copy the text above manually.', + ); + } + }, + }; +} diff --git a/packages/astro/src/cli/info/infra/cli-debug-info-provider.ts b/packages/astro/src/cli/info/infra/cli-debug-info-provider.ts new file mode 100644 index 000000000000..ee03146adef4 --- /dev/null +++ b/packages/astro/src/cli/info/infra/cli-debug-info-provider.ts @@ -0,0 +1,66 @@ +import type { AstroConfig } from '../../../types/public/index.js'; +import type { AstroVersionProvider, OperatingSystemProvider } from '../../definitions.js'; +import type { DebugInfoProvider, NodeVersionProvider, PackageManager } from '../definitions.js'; +import type { DebugInfo } from '../domain/debug-info.js'; + +interface Options { + config: Pick; + astroVersionProvider: AstroVersionProvider; + packageManager: PackageManager; + operatingSystemProvider: OperatingSystemProvider; + nodeVersionProvider: NodeVersionProvider; +} + +function withVersion(name: string, version: string | undefined): string { + let result = name; + if (version) { + result += ` (${version})`; + } + return result; +} + +export function createCliDebugInfoProvider({ + config, + astroVersionProvider, + packageManager, + operatingSystemProvider, + nodeVersionProvider, +}: Options): DebugInfoProvider { + return { + async get() { + const debugInfo: DebugInfo = [ + ['Astro', `v${astroVersionProvider.getVersion()}`], + ['Node', nodeVersionProvider.get()], + ['System', operatingSystemProvider.getDisplayName()], + ['Package Manager', packageManager.getName()], + ['Output', config.output], + ]; + + const viteVersion = await packageManager.getPackageVersion('vite'); + + if (viteVersion) { + debugInfo.splice(1, 0, ['Vite', viteVersion]); + } + + debugInfo.push([ + 'Adapter', + config.adapter + ? withVersion( + config.adapter.name, + await packageManager.getPackageVersion(config.adapter.name), + ) + : 'none', + ]); + + const integrations = await Promise.all( + config.integrations.map(async ({ name }) => + withVersion(name, await packageManager.getPackageVersion(name)), + ), + ); + + debugInfo.push(['Integrations', integrations.length > 0 ? integrations : 'none']); + + return debugInfo; + }, + }; +} diff --git a/packages/astro/src/cli/info/infra/dev-debug-info-provider.ts b/packages/astro/src/cli/info/infra/dev-debug-info-provider.ts new file mode 100644 index 000000000000..dafd84e0091b --- /dev/null +++ b/packages/astro/src/cli/info/infra/dev-debug-info-provider.ts @@ -0,0 +1,42 @@ +import type { AstroConfig } from '../../../types/public/index.js'; +import type { AstroVersionProvider, OperatingSystemProvider } from '../../definitions.js'; +import type { DebugInfoProvider, NodeVersionProvider, PackageManager } from '../definitions.js'; +import type { DebugInfo } from '../domain/debug-info.js'; + +interface Options { + config: Pick; + astroVersionProvider: AstroVersionProvider; + packageManager: PackageManager; + operatingSystemProvider: OperatingSystemProvider; + nodeVersionProvider: NodeVersionProvider; +} + +/** + * Returns debug info without any package versions, to avoid slowing down the dev server + */ +export function createDevDebugInfoProvider({ + config, + astroVersionProvider, + packageManager, + operatingSystemProvider, + nodeVersionProvider, +}: Options): DebugInfoProvider { + return { + async get() { + const debugInfo: DebugInfo = [ + ['Astro', `v${astroVersionProvider.getVersion()}`], + ['Node', nodeVersionProvider.get()], + ['System', operatingSystemProvider.getDisplayName()], + ['Package Manager', packageManager.getName()], + ['Output', config.output], + ['Adapter', config.adapter?.name ?? 'none'], + ]; + + const integrations = config.integrations.map((integration) => integration.name); + + debugInfo.push(['Integrations', integrations.length > 0 ? integrations : 'none']); + + return debugInfo; + }, + }; +} diff --git a/packages/astro/src/cli/info/infra/noop-package-manager.ts b/packages/astro/src/cli/info/infra/noop-package-manager.ts new file mode 100644 index 000000000000..a4aa444251e9 --- /dev/null +++ b/packages/astro/src/cli/info/infra/noop-package-manager.ts @@ -0,0 +1,12 @@ +import type { PackageManager } from '../definitions.js'; + +export function createNoopPackageManager(): PackageManager { + return { + getName() { + return 'unknown'; + }, + async getPackageVersion() { + return undefined; + }, + }; +} diff --git a/packages/astro/src/cli/info/infra/npm-package-manager.ts b/packages/astro/src/cli/info/infra/npm-package-manager.ts new file mode 100644 index 000000000000..9c2ae1817ec3 --- /dev/null +++ b/packages/astro/src/cli/info/infra/npm-package-manager.ts @@ -0,0 +1,45 @@ +import type { CommandExecutor } from '../../definitions.js'; +import type { PackageManager } from '../definitions.js'; + +interface BareNpmLikeVersionOutput { + version: string; + dependencies: Record; +} + +interface Options { + commandExecutor: CommandExecutor; +} + +export function createNpmPackageManager({ commandExecutor }: Options): PackageManager { + return { + getName() { + return 'npm'; + }, + async getPackageVersion(name) { + try { + // https://docs.npmjs.com/cli/v9/commands/npm-ls + const { stdout } = await commandExecutor.execute( + 'npm', + ['ls', name, '--json', '--depth=1'], + { + shell: true, + }, + ); + const parsedNpmOutput = JSON.parse(stdout) as BareNpmLikeVersionOutput; + + if (!parsedNpmOutput.dependencies) { + return undefined; + } + + if (parsedNpmOutput.dependencies[name]) { + return `v${parsedNpmOutput.dependencies[name].version}`; + } + + const astro = parsedNpmOutput.dependencies.astro; + return astro ? `v${astro.dependencies[name].version}` : undefined; + } catch { + return undefined; + } + }, + }; +} diff --git a/packages/astro/src/cli/info/infra/pnpm-package-manager.ts b/packages/astro/src/cli/info/infra/pnpm-package-manager.ts new file mode 100644 index 000000000000..fc5a6901179c --- /dev/null +++ b/packages/astro/src/cli/info/infra/pnpm-package-manager.ts @@ -0,0 +1,50 @@ +import type { CommandExecutor } from '../../definitions.js'; +import type { PackageManager } from '../definitions.js'; + +function formatPnpmVersionOutput(versionOutput: string): string { + return versionOutput.startsWith('link:') ? 'Local' : `v${versionOutput}`; +} + +interface BareNpmLikeVersionOutput { + version: string; + dependencies: Record; +} + +interface Options { + commandExecutor: CommandExecutor; +} + +export function createPnpmPackageManager({ commandExecutor }: Options): PackageManager { + return { + getName() { + return 'pnpm'; + }, + async getPackageVersion(name) { + try { + // https://pnpm.io/cli/why + const { stdout } = await commandExecutor.execute('pnpm', ['why', name, '--json'], { + shell: true, + }); + + const parsedOutput = JSON.parse(stdout) as Array; + + const deps = parsedOutput[0].dependencies; + + if (parsedOutput.length === 0 || !deps) { + return undefined; + } + + const userProvidedDependency = deps[name]; + + if (userProvidedDependency) { + return formatPnpmVersionOutput(userProvidedDependency.version); + } + + const astroDependency = deps.astro?.dependencies[name]; + return astroDependency ? formatPnpmVersionOutput(astroDependency.version) : undefined; + } catch { + return undefined; + } + }, + }; +} diff --git a/packages/astro/src/cli/info/infra/process-node-version-provider.ts b/packages/astro/src/cli/info/infra/process-node-version-provider.ts new file mode 100644 index 000000000000..df877b1f3617 --- /dev/null +++ b/packages/astro/src/cli/info/infra/process-node-version-provider.ts @@ -0,0 +1,9 @@ +import type { NodeVersionProvider } from '../definitions.js'; + +export function createProcessNodeVersionProvider(): NodeVersionProvider { + return { + get() { + return process.version; + }, + }; +} diff --git a/packages/astro/src/cli/info/infra/process-package-manager-user-agent-provider.ts b/packages/astro/src/cli/info/infra/process-package-manager-user-agent-provider.ts new file mode 100644 index 000000000000..65d1b846310b --- /dev/null +++ b/packages/astro/src/cli/info/infra/process-package-manager-user-agent-provider.ts @@ -0,0 +1,10 @@ +import type { PackageManagerUserAgentProvider } from '../definitions.js'; + +export function createProcessPackageManagerUserAgentProvider(): PackageManagerUserAgentProvider { + return { + getUserAgent() { + // https://docs.npmjs.com/cli/v8/using-npm/config#user-agent + return process.env.npm_config_user_agent ?? null; + }, + }; +} diff --git a/packages/astro/src/cli/info/infra/prompts-prompt.ts b/packages/astro/src/cli/info/infra/prompts-prompt.ts new file mode 100644 index 000000000000..316ea801d06c --- /dev/null +++ b/packages/astro/src/cli/info/infra/prompts-prompt.ts @@ -0,0 +1,23 @@ +import prompts from 'prompts'; +import type { Prompt } from '../definitions.js'; + +interface Options { + force: boolean; +} + +export function createPromptsPrompt({ force }: Options): Prompt { + return { + async confirm({ message, defaultValue }) { + if (force) { + return true; + } + const { value } = await prompts({ + type: 'confirm', + name: 'value', + message, + initial: defaultValue, + }); + return value; + }, + }; +} diff --git a/packages/astro/src/cli/info/infra/styled-debug-info-formatter.ts b/packages/astro/src/cli/info/infra/styled-debug-info-formatter.ts new file mode 100644 index 000000000000..681a2dcf8617 --- /dev/null +++ b/packages/astro/src/cli/info/infra/styled-debug-info-formatter.ts @@ -0,0 +1,29 @@ +import type { TextStyler } from '../../definitions.js'; +import type { DebugInfoFormatter } from '../definitions.js'; + +const MAX_PADDING = 25; + +interface Options { + textStyler: TextStyler; +} + +export function createStyledDebugInfoFormatter({ textStyler }: Options): DebugInfoFormatter { + return { + format(info) { + let output = ''; + for (const [label, value] of info) { + const padding = MAX_PADDING - label.length; + const [first, ...rest] = Array.isArray(value) ? value : [value]; + let richtext = `\n${textStyler.bold(label)}${' '.repeat(padding)}${textStyler.green(first)}`; + if (rest.length > 0) { + for (const entry of rest) { + richtext += `\n${' '.repeat(MAX_PADDING)}${textStyler.green(entry)}`; + } + } + output += richtext; + } + + return output.trim(); + }, + }; +} diff --git a/packages/astro/src/cli/info/infra/yarn-package-manager.ts b/packages/astro/src/cli/info/infra/yarn-package-manager.ts new file mode 100644 index 000000000000..f4a0eb438786 --- /dev/null +++ b/packages/astro/src/cli/info/infra/yarn-package-manager.ts @@ -0,0 +1,64 @@ +import type { CommandExecutor } from '../../definitions.js'; +import type { PackageManager } from '../definitions.js'; + +interface YarnVersionOutputLine { + children: Record; +} + +function getYarnOutputDepVersion(dependency: string, outputLine: string) { + const parsed = JSON.parse(outputLine) as YarnVersionOutputLine; + + for (const [key, value] of Object.entries(parsed.children)) { + if (key.startsWith(`${dependency}@`)) { + return `v${value.locator.split(':').pop()}`; + } + } +} + +interface Options { + commandExecutor: CommandExecutor; +} + +export function createYarnPackageManager({ commandExecutor }: Options): PackageManager { + return { + getName() { + return 'yarn'; + }, + async getPackageVersion(name) { + try { + // https://yarnpkg.com/cli/why + const { stdout } = await commandExecutor.execute('yarn', ['why', name, '--json'], { + shell: true, + }); + + const hasUserDefinition = stdout.includes('workspace:.'); + + /* output is NDJSON: one line contains a json object. For example: + + {"type":"step","data":{"message":"Why do we have the module \"hookable\"?","current":1,"total":4}} + {"type":"step","data":{"message":"Initialising dependency graph","current":2,"total":4}} + {"type":"activityStart","data":{"id":0}} + {"type":"activityTick","data":{"id":0,"name":"hookable@^5.5.3"}} + {"type":"activityEnd","data":{"id":0}} + {"type":"step","data":{"message":"Finding dependency","current":3,"total":4}} + {"type":"step","data":{"message":"Calculating file sizes","current":4,"total":4}} + {"type":"info","data":"\r=> Found \"hookable@5.5.3\""} + {"type":"info","data":"Has been hoisted to \"hookable\""} + {"type":"info","data":"This module exists because it's specified in \"dependencies\"."} + {"type":"info","data":"Disk size without dependencies: \"52KB\""} + {"type":"info","data":"Disk size with unique dependencies: \"52KB\""} + {"type":"info","data":"Disk size with transitive dependencies: \"52KB\""} + {"type":"info","data":"Number of shared dependencies: 0"} + */ + for (const line of stdout.split('\n')) { + if (hasUserDefinition && line.includes('workspace:.')) + return getYarnOutputDepVersion(name, line); + if (!hasUserDefinition && line.includes('astro@')) + return getYarnOutputDepVersion(name, line); + } + } catch { + return undefined; + } + }, + }; +} diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index e8aa77f998c3..7d95a11b2d7c 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -1,5 +1,4 @@ import { fileURLToPath } from 'node:url'; -import { getInfoOutput } from '../cli/info/index.js'; import type { HeadElements, TryRewriteResult } from '../core/base-pipeline.js'; import { ASTRO_VERSION } from '../core/constants.js'; import { enhanceViteSSRError } from '../core/errors/dev/index.js'; @@ -42,6 +41,7 @@ export class DevPipeline extends Pipeline { readonly logger: Logger, readonly manifest: SSRManifest, readonly settings: AstroSettings, + readonly getDebugInfo: () => Promise, readonly config = settings.config, readonly defaultRoutes = createDefaultRoutes(manifest), ) { @@ -60,9 +60,10 @@ export class DevPipeline extends Pipeline { logger, manifest, settings, - }: Pick, + getDebugInfo, + }: Pick, ) { - const pipeline = new DevPipeline(loader, logger, manifest, settings); + const pipeline = new DevPipeline(loader, logger, manifest, settings, getDebugInfo); pipeline.routesList = manifestData; return pipeline; } @@ -95,7 +96,11 @@ export class DevPipeline extends Pipeline { root: fileURLToPath(settings.config.root), version: ASTRO_VERSION, latestAstroVersion: settings.latestAstroVersion, - debugInfo: await getInfoOutput({ userConfig: settings.config, print: false }), + // TODO: Currently the debug info is always fetched, which slows things down. + // We should look into not loading it if the dev toolbar is disabled. And when + // enabled, it would nice to request the debug info through import.meta.hot + // when the button is click to defer execution as much as possible + debugInfo: await this.getDebugInfo(), }; // Additional data for the dev overlay diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index 447e7369d916..3d5b896413bd 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -7,6 +7,15 @@ import { IncomingMessage } from 'node:http'; import { fileURLToPath } from 'node:url'; import type * as vite from 'vite'; import { normalizePath } from 'vite'; +import { getPackageManager } from '../cli/info/core/get-package-manager.js'; +import { createDevDebugInfoProvider } from '../cli/info/infra/dev-debug-info-provider.js'; +import { createProcessNodeVersionProvider } from '../cli/info/infra/process-node-version-provider.js'; +import { createProcessPackageManagerUserAgentProvider } from '../cli/info/infra/process-package-manager-user-agent-provider.js'; +import { createStyledDebugInfoFormatter } from '../cli/info/infra/styled-debug-info-formatter.js'; +import { createBuildTimeAstroVersionProvider } from '../cli/infra/build-time-astro-version-provider.js'; +import { createPassthroughTextStyler } from '../cli/infra/passthrough-text-styler.js'; +import { createProcessOperatingSystemProvider } from '../cli/infra/process-operating-system-provider.js'; +import { createTinyexecCommandExecutor } from '../cli/infra/tinyexec-command-executor.js'; import type { SSRManifest, SSRManifestCSP, SSRManifestI18n } from '../core/app/types.js'; import { getAlgorithm, @@ -54,8 +63,12 @@ export default function createVitePluginAstroServer({ routesList, manifest, }: AstroPluginOptions): vite.Plugin { + let debugInfo: string | null = null; return { name: 'astro:server', + buildEnd() { + debugInfo = null; + }, async configureServer(viteServer) { const loader = createViteLoader(viteServer); const pipeline = DevPipeline.create(routesList, { @@ -63,6 +76,27 @@ export default function createVitePluginAstroServer({ logger, manifest, settings, + async getDebugInfo() { + if (!debugInfo) { + // TODO: do not import from CLI. Currently the code is located under src/cli/infra + // but some will have to be moved to src/infra + const debugInfoProvider = createDevDebugInfoProvider({ + config: settings.config, + astroVersionProvider: createBuildTimeAstroVersionProvider(), + operatingSystemProvider: createProcessOperatingSystemProvider(), + packageManager: await getPackageManager({ + packageManagerUserAgentProvider: createProcessPackageManagerUserAgentProvider(), + commandExecutor: createTinyexecCommandExecutor(), + }), + nodeVersionProvider: createProcessNodeVersionProvider(), + }); + const debugInfoFormatter = createStyledDebugInfoFormatter({ + textStyler: createPassthroughTextStyler(), + }); + debugInfo = debugInfoFormatter.format(await debugInfoProvider.get()); + } + return debugInfo; + }, }); const controller = createController({ loader }); const localStorage = new AsyncLocalStorage(); diff --git a/packages/astro/test/cli.test.js b/packages/astro/test/cli.test.js index f8669ac8bef0..d3a0a5fd5e70 100644 --- a/packages/astro/test/cli.test.js +++ b/packages/astro/test/cli.test.js @@ -7,9 +7,52 @@ import { Writable } from 'node:stream'; import { describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; import { stripVTControlCharacters } from 'node:util'; -import { readFromClipboard } from '../dist/cli/info/index.js'; import { cli, cliServerLogSetup, loadFixture, parseCliDevStart } from './test-utils.js'; +/** + * Throws an error if no command is found for the current OS. + * @returns {string} + */ +function readFromClipboard() { + const system = process.platform; + let command = ''; + let args = []; + + if (system === 'darwin') { + command = 'pbpaste'; + } else if (system === 'win32') { + command = 'powershell'; + args = ['-command', 'Get-Clipboard']; + } else { + const unixCommands = [ + ['xclip', ['-sel', 'clipboard', '-o']], + ['wl-paste', []], + ]; + for (const [unixCommand, unixArgs] of unixCommands) { + try { + const output = spawnSync('which', [unixCommand], { encoding: 'utf8' }); + if (output.stdout.trim()) { + command = unixCommand; + args = unixArgs; + break; + } + } catch { + continue; + } + } + } + + if (!command) { + throw new Error('Clipboard read command not found!'); + } + + const result = spawnSync(command, args, { encoding: 'utf8' }); + if (result.error) { + throw result.error; + } + return result.stdout.trim(); +} + describe('astro cli', () => { const cliServerLogSetupWithFixture = (flags, cmd) => { const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url); diff --git a/packages/astro/test/units/cli/info.test.js b/packages/astro/test/units/cli/info.test.js new file mode 100644 index 000000000000..aedb4cd61a97 --- /dev/null +++ b/packages/astro/test/units/cli/info.test.js @@ -0,0 +1,588 @@ +// @ts-check +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { getPackageManager } from '../../../dist/cli/info/core/get-package-manager.js'; +import { infoCommand } from '../../../dist/cli/info/core/info.js'; +import { createCliClipboard } from '../../../dist/cli/info/infra/cli-clipboard.js'; +import { createCliDebugInfoProvider } from '../../../dist/cli/info/infra/cli-debug-info-provider.js'; +import { createDevDebugInfoProvider } from '../../../dist/cli/info/infra/dev-debug-info-provider.js'; +import { createProcessNodeVersionProvider } from '../../../dist/cli/info/infra/process-node-version-provider.js'; +import { createProcessPackageManagerUserAgentProvider } from '../../../dist/cli/info/infra/process-package-manager-user-agent-provider.js'; +import { createSpyLogger } from '../test-utils.js'; +import { + createFakeAstroVersionProvider, + createFakeDebugInfoProvider, + createFakeNodeVersionProvider, + createFakeOperatingSystemProvider, + createFakePackageManagerUserAgentProvider, + createFakePrompt, + createPassthroughCommandRunner, + createSpyClipboard, + createSpyCommandExecutor, +} from './utils.js'; + +describe('CLI info', () => { + describe('core', () => { + describe('infoCommand', () => { + it('logs pretty debug info', async () => { + const { logger, logs } = createSpyLogger(); + const runner = createPassthroughCommandRunner(); + const debugInfoProvider = createFakeDebugInfoProvider([['foo', 'bar']]); + const { clipboard } = createSpyClipboard(); + + await runner.run(infoCommand, { + debugInfoProvider, + getDebugInfoFormatter: ({ pretty }) => ({ + format: (debugInfo) => `${pretty}-${JSON.stringify(debugInfo)}`, + }), + clipboard, + logger, + }); + + assert.equal(logs[0].type, 'info'); + assert.equal(logs[0].message, 'true-[["foo","bar"]]'); + }); + + it('copies raw debug info', async () => { + const { logger } = createSpyLogger(); + const runner = createPassthroughCommandRunner(); + const debugInfoProvider = createFakeDebugInfoProvider([['foo', 'bar']]); + const { clipboard, texts } = createSpyClipboard(); + + await runner.run(infoCommand, { + debugInfoProvider, + getDebugInfoFormatter: ({ pretty }) => ({ + format: (debugInfo) => `${pretty}-${JSON.stringify(debugInfo)}`, + }), + clipboard, + logger, + }); + + assert.deepStrictEqual(texts, ['false-[["foo","bar"]]']); + }); + }); + + describe('getPackageManager()', () => { + it('returns noop if there is no user agent', async () => { + const packageManagerUserAgentProvider = createFakePackageManagerUserAgentProvider(null); + const { commandExecutor, inputs } = createSpyCommandExecutor(); + + const packageManager = await getPackageManager({ + packageManagerUserAgentProvider, + commandExecutor, + }); + + assert.deepStrictEqual(inputs, []); + assert.equal(packageManager.getName(), 'unknown'); + }); + + it('handles pnpm', async () => { + const packageManagerUserAgentProvider = createFakePackageManagerUserAgentProvider( + 'pnpm/7.18.1 node/v16.17.0 linux x64', + ); + const { commandExecutor, inputs } = createSpyCommandExecutor(); + + const packageManager = await getPackageManager({ + packageManagerUserAgentProvider, + commandExecutor, + }); + + assert.deepStrictEqual(inputs, []); + assert.equal(packageManager.getName(), 'pnpm'); + }); + + it('handles npm', async () => { + const packageManagerUserAgentProvider = createFakePackageManagerUserAgentProvider( + 'npm/8.19.2 node/v16.17.0 linux x64', + ); + const { commandExecutor, inputs } = createSpyCommandExecutor(); + + const packageManager = await getPackageManager({ + packageManagerUserAgentProvider, + commandExecutor, + }); + + assert.deepStrictEqual(inputs, []); + assert.equal(packageManager.getName(), 'npm'); + }); + + it('handles yarn', async () => { + const packageManagerUserAgentProvider = createFakePackageManagerUserAgentProvider( + 'yarn/1.22.19 npm/? node/v16.17.0 linux x64', + ); + const { commandExecutor, inputs } = createSpyCommandExecutor(); + + const packageManager = await getPackageManager({ + packageManagerUserAgentProvider, + commandExecutor, + }); + + assert.deepStrictEqual(inputs, []); + assert.equal(packageManager.getName(), 'yarn'); + }); + + it('handles bun', async () => { + const packageManagerUserAgentProvider = + createFakePackageManagerUserAgentProvider('bun/0.5.9 linux x64'); + const { commandExecutor, inputs } = createSpyCommandExecutor(); + + const packageManager = await getPackageManager({ + packageManagerUserAgentProvider, + commandExecutor, + }); + + assert.deepStrictEqual(inputs, []); + assert.equal(packageManager.getName(), 'bun'); + }); + + it('returns a noop for unknown user agents', async () => { + const packageManagerUserAgentProvider = createFakePackageManagerUserAgentProvider( + 'npminstall/5.0.0 npm/8.19.2 node/v16.17.0 linux x64', + ); + const { commandExecutor, inputs } = createSpyCommandExecutor(); + + const packageManager = await getPackageManager({ + packageManagerUserAgentProvider, + commandExecutor, + }); + + assert.deepStrictEqual(inputs, []); + assert.equal(packageManager.getName(), 'unknown'); + }); + }); + }); + + describe('infra', () => { + describe('createCliClipboard()', () => { + it('aborts early if no copy command can be found', async () => { + const { commandExecutor, inputs } = createSpyCommandExecutor({ fail: true }); + const { logger, logs } = createSpyLogger(); + const operatingSystemProvider = createFakeOperatingSystemProvider('aix'); + const prompt = createFakePrompt(true); + + const clipboard = createCliClipboard({ + commandExecutor, + logger, + operatingSystemProvider, + prompt, + }); + await clipboard.copy('foo bar'); + + assert.equal(inputs.length, 2); + assert.equal(logs[0].type, 'warn'); + assert.equal(logs[0].message, 'Clipboard command not found!'); + assert.equal(logs[1].type, 'info'); + assert.equal(logs[1].message, 'Please manually copy the text above.'); + }); + + it('aborts if user does not confirm', async () => { + const { commandExecutor, inputs } = createSpyCommandExecutor(); + const { logger, logs } = createSpyLogger(); + const operatingSystemProvider = createFakeOperatingSystemProvider('win32'); + const prompt = createFakePrompt(false); + + const clipboard = createCliClipboard({ + commandExecutor, + logger, + operatingSystemProvider, + prompt, + }); + const text = Date.now().toString(); + await clipboard.copy(text); + + assert.equal(logs.length, 0); + assert.equal(inputs.length, 0); + }); + + it('copies correctly', async () => { + const { commandExecutor, inputs } = createSpyCommandExecutor(); + const { logger, logs } = createSpyLogger(); + const operatingSystemProvider = createFakeOperatingSystemProvider('win32'); + const prompt = createFakePrompt(true); + + const clipboard = createCliClipboard({ + commandExecutor, + logger, + operatingSystemProvider, + prompt, + }); + const text = Date.now().toString(); + await clipboard.copy(text); + + assert.equal(logs[0].type, 'info'); + assert.equal(logs[0].message, 'Copied to clipboard!'); + assert.equal(inputs.length, 1); + assert.deepStrictEqual(inputs[0], { + command: 'clip', + args: undefined, + input: text, + }); + }); + }); + + describe('createCliDebugInfoProvider()', () => { + it('returns basic infos', async () => { + const astroVersionProvider = createFakeAstroVersionProvider('5.5.5'); + const operatingSystemProvider = createFakeOperatingSystemProvider('win32'); + const nodeVersionProvider = createFakeNodeVersionProvider('v10.1.7'); + + const debugInfoProvider = createCliDebugInfoProvider({ + config: { + output: 'static', + adapter: undefined, + integrations: [], + }, + astroVersionProvider, + operatingSystemProvider, + packageManager: { + getName: () => 'pnpm', + getPackageVersion: async () => { + return undefined; + }, + }, + nodeVersionProvider, + }); + const debugInfo = await debugInfoProvider.get(); + + assert.deepStrictEqual(debugInfo, [ + ['Astro', 'v5.5.5'], + ['Node', 'v10.1.7'], + ['System', 'win32'], + ['Package Manager', 'pnpm'], + ['Output', 'static'], + ['Adapter', 'none'], + ['Integrations', 'none'], + ]); + }); + + it('handles the vite version', async () => { + const astroVersionProvider = createFakeAstroVersionProvider('5.5.5'); + const operatingSystemProvider = createFakeOperatingSystemProvider('win32'); + const nodeVersionProvider = createFakeNodeVersionProvider('v10.1.7'); + + const debugInfoProvider = createCliDebugInfoProvider({ + config: { + output: 'static', + adapter: undefined, + integrations: [], + }, + astroVersionProvider, + operatingSystemProvider, + packageManager: { + getName: () => 'pnpm', + getPackageVersion: async (name) => { + if (name === 'vite') { + return 'v1.2.3'; + } + return undefined; + }, + }, + nodeVersionProvider, + }); + const debugInfo = await debugInfoProvider.get(); + + assert.deepStrictEqual(debugInfo, [ + ['Astro', 'v5.5.5'], + ['Vite', 'v1.2.3'], + ['Node', 'v10.1.7'], + ['System', 'win32'], + ['Package Manager', 'pnpm'], + ['Output', 'static'], + ['Adapter', 'none'], + ['Integrations', 'none'], + ]); + }); + + it('handles the adapter with no version', async () => { + const astroVersionProvider = createFakeAstroVersionProvider('5.5.5'); + const operatingSystemProvider = createFakeOperatingSystemProvider('win32'); + const nodeVersionProvider = createFakeNodeVersionProvider('v10.1.7'); + + const debugInfoProvider = createCliDebugInfoProvider({ + config: { + output: 'static', + adapter: { + name: '@astrojs/node', + hooks: {}, + }, + integrations: [], + }, + astroVersionProvider, + operatingSystemProvider, + packageManager: { + getName: () => 'pnpm', + getPackageVersion: async () => { + return undefined; + }, + }, + nodeVersionProvider, + }); + const debugInfo = await debugInfoProvider.get(); + + assert.deepStrictEqual(debugInfo, [ + ['Astro', 'v5.5.5'], + ['Node', 'v10.1.7'], + ['System', 'win32'], + ['Package Manager', 'pnpm'], + ['Output', 'static'], + ['Adapter', '@astrojs/node'], + ['Integrations', 'none'], + ]); + }); + + it('handles the adapter version', async () => { + const astroVersionProvider = createFakeAstroVersionProvider('5.5.5'); + const operatingSystemProvider = createFakeOperatingSystemProvider('win32'); + const nodeVersionProvider = createFakeNodeVersionProvider('v10.1.7'); + + const debugInfoProvider = createCliDebugInfoProvider({ + config: { + output: 'static', + adapter: { + name: '@astrojs/node', + hooks: {}, + }, + integrations: [], + }, + astroVersionProvider, + operatingSystemProvider, + packageManager: { + getName: () => 'pnpm', + getPackageVersion: async (name) => { + if (name === '@astrojs/node') { + return 'v6.5.4'; + } + return undefined; + }, + }, + nodeVersionProvider, + }); + const debugInfo = await debugInfoProvider.get(); + + assert.deepStrictEqual(debugInfo, [ + ['Astro', 'v5.5.5'], + ['Node', 'v10.1.7'], + ['System', 'win32'], + ['Package Manager', 'pnpm'], + ['Output', 'static'], + ['Adapter', '@astrojs/node (v6.5.4)'], + ['Integrations', 'none'], + ]); + }); + + it('handles integrations', async () => { + const astroVersionProvider = createFakeAstroVersionProvider('5.5.5'); + const operatingSystemProvider = createFakeOperatingSystemProvider('win32'); + const nodeVersionProvider = createFakeNodeVersionProvider('v10.1.7'); + + const debugInfoProvider = createCliDebugInfoProvider({ + config: { + output: 'static', + adapter: undefined, + integrations: [ + { + name: 'foo', + hooks: {}, + }, + { + name: 'bar', + hooks: {}, + }, + ], + }, + astroVersionProvider, + operatingSystemProvider, + packageManager: { + getName: () => 'pnpm', + getPackageVersion: async (name) => { + if (name === 'bar') { + return 'v6.6.6'; + } + return undefined; + }, + }, + nodeVersionProvider, + }); + const debugInfo = await debugInfoProvider.get(); + + assert.deepStrictEqual(debugInfo, [ + ['Astro', 'v5.5.5'], + ['Node', 'v10.1.7'], + ['System', 'win32'], + ['Package Manager', 'pnpm'], + ['Output', 'static'], + ['Adapter', 'none'], + ['Integrations', ['foo', 'bar (v6.6.6)']], + ]); + }); + }); + + describe('createDevDebugInfoProvider', () => { + it('works', async () => { + const astroVersionProvider = createFakeAstroVersionProvider('5.5.5'); + const operatingSystemProvider = createFakeOperatingSystemProvider('win32'); + const nodeVersionProvider = createFakeNodeVersionProvider('v10.1.7'); + + const debugInfoProvider = createDevDebugInfoProvider({ + config: { + output: 'static', + adapter: undefined, + integrations: [], + }, + astroVersionProvider, + operatingSystemProvider, + packageManager: { + getName: () => 'pnpm', + getPackageVersion: async () => { + return undefined; + }, + }, + nodeVersionProvider, + }); + const debugInfo = await debugInfoProvider.get(); + + assert.deepStrictEqual(debugInfo, [ + ['Astro', 'v5.5.5'], + ['Node', 'v10.1.7'], + ['System', 'win32'], + ['Package Manager', 'pnpm'], + ['Output', 'static'], + ['Adapter', 'none'], + ['Integrations', 'none'], + ]); + }); + + it('handles the adapter', async () => { + const astroVersionProvider = createFakeAstroVersionProvider('5.5.5'); + const operatingSystemProvider = createFakeOperatingSystemProvider('win32'); + const nodeVersionProvider = createFakeNodeVersionProvider('v10.1.7'); + + const debugInfoProvider = createDevDebugInfoProvider({ + config: { + output: 'static', + adapter: { + name: '@astrojs/node', + hooks: {}, + }, + integrations: [], + }, + astroVersionProvider, + operatingSystemProvider, + packageManager: { + getName: () => 'pnpm', + getPackageVersion: async () => { + return undefined; + }, + }, + nodeVersionProvider, + }); + const debugInfo = await debugInfoProvider.get(); + + assert.deepStrictEqual(debugInfo, [ + ['Astro', 'v5.5.5'], + ['Node', 'v10.1.7'], + ['System', 'win32'], + ['Package Manager', 'pnpm'], + ['Output', 'static'], + ['Adapter', '@astrojs/node'], + ['Integrations', 'none'], + ]); + }); + + it('handles integrations', async () => { + const astroVersionProvider = createFakeAstroVersionProvider('5.5.5'); + const operatingSystemProvider = createFakeOperatingSystemProvider('win32'); + const nodeVersionProvider = createFakeNodeVersionProvider('v10.1.7'); + + const debugInfoProvider = createDevDebugInfoProvider({ + config: { + output: 'static', + adapter: undefined, + integrations: [ + { + name: 'foo', + hooks: {}, + }, + { + name: 'bar', + hooks: {}, + }, + ], + }, + astroVersionProvider, + operatingSystemProvider, + packageManager: { + getName: () => 'pnpm', + getPackageVersion: async () => { + return undefined; + }, + }, + nodeVersionProvider, + }); + const debugInfo = await debugInfoProvider.get(); + + assert.deepStrictEqual(debugInfo, [ + ['Astro', 'v5.5.5'], + ['Node', 'v10.1.7'], + ['System', 'win32'], + ['Package Manager', 'pnpm'], + ['Output', 'static'], + ['Adapter', 'none'], + ['Integrations', ['foo', 'bar']], + ]); + }); + + it('never retrieves versions', async () => { + const astroVersionProvider = createFakeAstroVersionProvider('5.5.5'); + const operatingSystemProvider = createFakeOperatingSystemProvider('win32'); + const nodeVersionProvider = createFakeNodeVersionProvider('v10.1.7'); + + let called = false; + + const debugInfoProvider = createDevDebugInfoProvider({ + config: { + output: 'static', + adapter: { + name: '@astrojs/node', + hooks: {}, + }, + integrations: [ + { + name: 'foo', + hooks: {}, + }, + { + name: 'bar', + hooks: {}, + }, + ], + }, + astroVersionProvider, + operatingSystemProvider, + packageManager: { + getName: () => 'pnpm', + getPackageVersion: async () => { + called = true; + return undefined; + }, + }, + nodeVersionProvider, + }); + await debugInfoProvider.get(); + + assert.equal(called, false); + }); + }); + + it('createProcessPackageManagerUserAgentProvider()', () => { + assert.equal( + createProcessPackageManagerUserAgentProvider().getUserAgent(), + process.env.npm_config_user_agent ?? null, + ); + }); + + it('createProcessNodeVersionProvider()', () => { + assert.equal(createProcessNodeVersionProvider().get(), process.version); + }); + }); +}); diff --git a/packages/astro/test/units/cli/utils.js b/packages/astro/test/units/cli/utils.js index 284d8ba96f1c..d4b40e33959c 100644 --- a/packages/astro/test/units/cli/utils.js +++ b/packages/astro/test/units/cli/utils.js @@ -83,14 +83,23 @@ export function createFakeOperatingSystemProvider(platform) { }; } -export function createSpyCommandExecutor() { - /** @type {Array<{ command: string; args?: Array }>} */ +/** + * + * @param {object} options + * @param {boolean} [options.fail=false] Forces execute() to throw an error. This is useful to test error handling + * @returns + */ +export function createSpyCommandExecutor({ fail = false } = {}) { + /** @type {Array<{ command: string; args: Array | undefined; input: string | undefined }>} */ const inputs = []; /** @type {import("../../../dist/cli/definitions.js").CommandExecutor} */ const commandExecutor = { - async execute(command, args) { - inputs.push({ command, args }); + async execute(command, args, options) { + inputs.push({ command, args, input: options?.input }); + if (fail) { + throw new Error('Command execution failed'); + } return { stdout: '', }; @@ -99,3 +108,66 @@ export function createSpyCommandExecutor() { return { inputs, commandExecutor }; } + +/** + * @param {import("../../../dist/cli/info/domain/debug-info.js").DebugInfo} debugInfo + * @returns {import("../../../dist/cli/info/definitions.js").DebugInfoProvider} + */ +export function createFakeDebugInfoProvider(debugInfo) { + return { + async get() { + return debugInfo; + }, + }; +} + +export function createSpyClipboard() { + /** @type {Array} */ + const texts = []; + + /** @type {import("../../../dist/cli/info/definitions.js").Clipboard} */ + const clipboard = { + async copy(text) { + texts.push(text); + }, + }; + + return { texts, clipboard }; +} + +/** + * @param {string | null} userAgent + * @returns {import("../../../dist/cli/info/definitions.js").PackageManagerUserAgentProvider} + */ +export function createFakePackageManagerUserAgentProvider(userAgent) { + return { + getUserAgent() { + return userAgent; + }, + }; +} + +/** + * @param {boolean} confirmed + * @returns {import("../../../dist/cli/info/definitions.js").Prompt} + * */ +export function createFakePrompt(confirmed) { + return { + async confirm() { + return confirmed; + }, + }; +} + +/** + * + * @param {`v${string}`} version + * @returns {import("../../../dist/cli/info/definitions.js").NodeVersionProvider} + */ +export function createFakeNodeVersionProvider(version) { + return { + get() { + return version; + }, + }; +} diff --git a/packages/astro/test/units/routing/route-matching.test.js b/packages/astro/test/units/routing/route-matching.test.js index 9e878d2d1d9f..f6438a723db8 100644 --- a/packages/astro/test/units/routing/route-matching.test.js +++ b/packages/astro/test/units/routing/route-matching.test.js @@ -143,7 +143,13 @@ describe('Route matching', () => { const loader = createViteLoader(container.viteServer); const manifest = createDevelopmentManifest(container.settings); - pipeline = DevPipeline.create(undefined, { loader, logger: defaultLogger, manifest, settings }); + pipeline = DevPipeline.create(undefined, { + loader, + logger: defaultLogger, + manifest, + settings, + getDebugInfo: async () => '', + }); manifestData = await createRoutesList( { cwd: fixture.path, diff --git a/packages/astro/test/units/vite-plugin-astro-server/request.test.js b/packages/astro/test/units/vite-plugin-astro-server/request.test.js index 5a35fb907a4e..ef83d3fb8c8d 100644 --- a/packages/astro/test/units/vite-plugin-astro-server/request.test.js +++ b/packages/astro/test/units/vite-plugin-astro-server/request.test.js @@ -27,7 +27,13 @@ async function createDevPipeline(overrides = {}, root) { }, defaultLogger, ); - return DevPipeline.create(routesList, { loader, logger: defaultLogger, manifest, settings }); + return DevPipeline.create(routesList, { + loader, + logger: defaultLogger, + manifest, + settings, + getDebugInfo: async () => '', + }); } describe('vite-plugin-astro-server', () => { diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json index a30b9cb398b7..8fd0317bd994 100644 --- a/packages/integrations/markdoc/package.json +++ b/packages/integrations/markdoc/package.json @@ -59,7 +59,7 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test --timeout 60000 \"test/**/*.test.js\"" + "test": "astro-scripts test --timeout 100000 \"test/**/*.test.js\"" }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", diff --git a/packages/language-tools/vscode/languages/astro.code-snippets b/packages/language-tools/vscode/languages/astro.code-snippets index 1004bf35bcac..3ebeb15b49bf 100644 --- a/packages/language-tools/vscode/languages/astro.code-snippets +++ b/packages/language-tools/vscode/languages/astro.code-snippets @@ -1,47 +1,47 @@ { - "page_html": { - "prefix": "page_html", - "isFileTemplate": true, - "scope": "astro", - "body": [ - "---", - "$1", - "---", - "", - "", - "\t", - "\t\t", - "\t\t", - "\t\t", - "\t\t${2:Document}", - "\t", - "\t", - "\t\t$0", - "\t", - "", - ], - "description": "Page with full HTML", - }, - "page_layout": { - "prefix": "page_layout", - "isFileTemplate": true, - "scope": "astro", - "body": [ - "---", - "import ${1:Layout} from \"../layouts/$1.astro\"", - "---", - "", - "<$1>", - "\t$0", - "", - ], - "description": "Page from Layout", - }, - "component": { - "prefix": "component", - "isFileTemplate": true, - "scope": "astro", - "body": ["---", "$1", "---", "", "$0"], - "description": "Component", - }, + "page_html": { + "prefix": "page_html", + "isFileTemplate": true, + "scope": "astro", + "body": [ + "---", + "$1", + "---", + "", + "", + "\t", + "\t\t", + "\t\t", + "\t\t", + "\t\t${2:Document}", + "\t", + "\t", + "\t\t$0", + "\t", + "" + ], + "description": "Page with full HTML" + }, + "page_layout": { + "prefix": "page_layout", + "isFileTemplate": true, + "scope": "astro", + "body": [ + "---", + "import ${1:Layout} from \"../layouts/$1.astro\"", + "---", + "", + "<$1>", + "\t$0", + "" + ], + "description": "Page from Layout" + }, + "component": { + "prefix": "component", + "isFileTemplate": true, + "scope": "astro", + "body": ["---", "$1", "---", "", "$0"], + "description": "Component" + } }