diff --git a/packages/astro/src/cli/create-key/core/create-key.ts b/packages/astro/src/cli/create-key/core/create-key.ts new file mode 100644 index 000000000000..1759e587f45a --- /dev/null +++ b/packages/astro/src/cli/create-key/core/create-key.ts @@ -0,0 +1,29 @@ +import type { Logger } from '../../../core/logger/core.js'; +import type { HelpDisplay, KeyGenerator } from '../definitions.js'; + +interface CreateKeyOptions { + logger: Logger; + keyGenerator: KeyGenerator; + helpDisplay: HelpDisplay; +} + +export async function createKey({ logger, keyGenerator, helpDisplay }: CreateKeyOptions) { + if (helpDisplay.shouldFire()) { + return helpDisplay.show({ + commandName: 'astro create-key', + tables: { + Flags: [['--help (-h)', 'See all available flags.']], + }, + description: 'Generates a key to encrypt props passed to Server islands.', + }); + } + + const key = await keyGenerator.generate(); + + logger.info( + 'crypto', + `Generated a key to encrypt props passed to Server islands. To reuse the same key across builds, set this value as ASTRO_KEY in an environment variable on your build server. + +ASTRO_KEY=${key}`, + ); +} diff --git a/packages/astro/src/cli/create-key/definitions.ts b/packages/astro/src/cli/create-key/definitions.ts new file mode 100644 index 000000000000..3bfc74887fd2 --- /dev/null +++ b/packages/astro/src/cli/create-key/definitions.ts @@ -0,0 +1,23 @@ +import type { HelpPayload } from './domain/help-payload.js'; + +export interface KeyGenerator { + generate: () => Promise; +} + +export interface HelpDisplay { + shouldFire: () => boolean; + show: (payload: HelpPayload) => void; +} + +export interface TextStyler { + bgWhite: (msg: string) => string; + black: (msg: string) => string; + dim: (msg: string) => string; + green: (msg: string) => string; + bold: (msg: string) => string; + bgGreen: (msg: string) => string; +} + +export interface AstroVersionProvider { + getVersion: () => string; +} diff --git a/packages/astro/src/cli/create-key/domain/help-payload.ts b/packages/astro/src/cli/create-key/domain/help-payload.ts new file mode 100644 index 000000000000..2ed6e72cd678 --- /dev/null +++ b/packages/astro/src/cli/create-key/domain/help-payload.ts @@ -0,0 +1,7 @@ +export interface HelpPayload { + commandName: string; + headline?: string; + usage?: string; + tables?: Record; + description?: string; +} diff --git a/packages/astro/src/cli/create-key/index.ts b/packages/astro/src/cli/create-key/index.ts deleted file mode 100644 index bc03c53572d0..000000000000 --- a/packages/astro/src/cli/create-key/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { createNodeLogger } from '../../core/config/logging.js'; -import { createKey as createCryptoKey, encodeKey } from '../../core/encryption.js'; -import { type Flags, flagsToAstroInlineConfig } from '../flags.js'; - -interface CreateKeyOptions { - flags: Flags; -} - -export async function createKey({ flags }: CreateKeyOptions): Promise<0 | 1> { - try { - const inlineConfig = flagsToAstroInlineConfig(flags); - const logger = createNodeLogger(inlineConfig); - - const keyPromise = createCryptoKey(); - const key = await keyPromise; - const encoded = await encodeKey(key); - - logger.info( - 'crypto', - `Generated a key to encrypt props passed to Server islands. To reuse the same key across builds, set this value as ASTRO_KEY in an environment variable on your build server. - -ASTRO_KEY=${encoded}`, - ); - } catch (err: unknown) { - if (err != null) { - console.error(err.toString()); - } - return 1; - } - - return 0; -} diff --git a/packages/astro/src/cli/create-key/infra/build-time-astro-version-provider.ts b/packages/astro/src/cli/create-key/infra/build-time-astro-version-provider.ts new file mode 100644 index 000000000000..dea44640ccce --- /dev/null +++ b/packages/astro/src/cli/create-key/infra/build-time-astro-version-provider.ts @@ -0,0 +1,9 @@ +import type { AstroVersionProvider } from '../definitions.js'; + +export function createBuildTimeAstroVersionProvider(): AstroVersionProvider { + return { + getVersion() { + return process.env.PACKAGE_VERSION ?? ''; + }, + }; +} diff --git a/packages/astro/src/cli/create-key/infra/crypto-key-generator.ts b/packages/astro/src/cli/create-key/infra/crypto-key-generator.ts new file mode 100644 index 000000000000..97d8f415d9b8 --- /dev/null +++ b/packages/astro/src/cli/create-key/infra/crypto-key-generator.ts @@ -0,0 +1,12 @@ +import { createKey, encodeKey } from '../../../core/encryption.js'; +import type { KeyGenerator } from '../definitions.js'; + +export function createCryptoKeyGenerator(): KeyGenerator { + return { + async generate() { + const key = await createKey(); + const encoded = await encodeKey(key); + return encoded; + }, + }; +} diff --git a/packages/astro/src/cli/create-key/infra/kleur-text-styler.ts b/packages/astro/src/cli/create-key/infra/kleur-text-styler.ts new file mode 100644 index 000000000000..4988f2fc22a6 --- /dev/null +++ b/packages/astro/src/cli/create-key/infra/kleur-text-styler.ts @@ -0,0 +1,6 @@ +import * as colors from 'kleur/colors'; +import type { TextStyler } from '../definitions.js'; + +export function createKleurTextStyler(): TextStyler { + return colors; +} diff --git a/packages/astro/src/cli/create-key/infra/logger-help-display.ts b/packages/astro/src/cli/create-key/infra/logger-help-display.ts new file mode 100644 index 000000000000..4de05b5ec1d9 --- /dev/null +++ b/packages/astro/src/cli/create-key/infra/logger-help-display.ts @@ -0,0 +1,76 @@ +import type { Logger } from '../../../core/logger/core.js'; +import type { Flags } from '../../flags.js'; +import type { AstroVersionProvider, HelpDisplay, TextStyler } from '../definitions.js'; + +interface LoggerHelpDisplayOptions { + logger: Logger; + textStyler: TextStyler; + astroVersionProvider: AstroVersionProvider; + // TODO: find something better + flags: Flags; +} + +export function createLoggerHelpDisplay({ + logger, + flags, + textStyler, + astroVersionProvider, +}: LoggerHelpDisplayOptions): HelpDisplay { + return { + shouldFire() { + return !!(flags.help || flags.h); + }, + show({ commandName, description, headline, tables, usage }) { + const linebreak = () => ''; + const title = (label: string) => ` ${textStyler.bgWhite(textStyler.black(` ${label} `))}`; + const table = (rows: [string, string][], { padding }: { padding: number }) => { + const split = process.stdout.columns < 60; + let raw = ''; + + for (const row of rows) { + if (split) { + raw += ` ${row[0]}\n `; + } else { + raw += `${`${row[0]}`.padStart(padding)}`; + } + raw += ' ' + textStyler.dim(row[1]) + '\n'; + } + + return raw.slice(0, -1); // remove latest \n + }; + + let message = []; + + if (headline) { + message.push( + linebreak(), + ` ${textStyler.bgGreen(textStyler.black(` ${commandName} `))} ${textStyler.green( + `v${astroVersionProvider.getVersion()}`, + )} ${headline}`, + ); + } + + if (usage) { + message.push(linebreak(), ` ${textStyler.green(commandName)} ${textStyler.bold(usage)}`); + } + + if (tables) { + function calculateTablePadding(rows: [string, string][]) { + return rows.reduce((val, [first]) => Math.max(val, first.length), 0) + 2; + } + + const tableEntries = Object.entries(tables); + const padding = Math.max(...tableEntries.map(([, rows]) => calculateTablePadding(rows))); + for (const [tableTitle, tableRows] of tableEntries) { + message.push(linebreak(), title(tableTitle), table(tableRows, { padding })); + } + } + + if (description) { + message.push(linebreak(), `${description}`); + } + + logger.info('SKIP_FORMAT', message.join('\n') + '\n'); + }, + }; +} diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index 94c08d7b0cb7..e585849d3e9e 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -109,9 +109,33 @@ async function runCommand(cmd: string, flags: yargs.Arguments) { return; } case 'create-key': { - const { createKey } = await import('./create-key/index.js'); - const exitCode = await createKey({ flags }); - return process.exit(exitCode); + const [ + { createLoggerFromFlags }, + { createCryptoKeyGenerator }, + { createKleurTextStyler }, + { createBuildTimeAstroVersionProvider }, + { createLoggerHelpDisplay }, + { createKey }, + ] = await Promise.all([ + import('./flags.js'), + import('./create-key/infra/crypto-key-generator.js'), + import('./create-key/infra/kleur-text-styler.js'), + import('./create-key/infra/build-time-astro-version-provider.js'), + import('./create-key/infra/logger-help-display.js'), + import('./create-key/core/create-key.js'), + ]); + const logger = createLoggerFromFlags(flags); + const keyGenerator = createCryptoKeyGenerator(); + const textStyler = createKleurTextStyler(); + const astroVersionProvider = createBuildTimeAstroVersionProvider(); + const helpDisplay = createLoggerHelpDisplay({ + logger, + flags, + textStyler, + astroVersionProvider, + }); + await createKey({ logger, keyGenerator, helpDisplay }); + return; } case 'docs': { const { docs } = await import('./docs/index.js'); diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts index caa00ac139ea..7a2746de02da 100644 --- a/packages/astro/src/core/messages.ts +++ b/packages/astro/src/core/messages.ts @@ -333,6 +333,7 @@ export function formatErrorMessage(err: ErrorWithMetadata, showFullStacktrace: b return output.join('\n'); } +/** @deprecated Migrate to HelpDisplay */ export function printHelp({ commandName, headline, diff --git a/packages/astro/test/units/assets/fonts/orchestrate.test.js b/packages/astro/test/units/assets/fonts/orchestrate.test.js index aeb785198148..3ff3abb2087c 100644 --- a/packages/astro/test/units/assets/fonts/orchestrate.test.js +++ b/packages/astro/test/units/assets/fonts/orchestrate.test.js @@ -20,9 +20,8 @@ import { createBuildUrlProxyHashResolver } from '../../../../dist/assets/fonts/i import { createDevUrlResolver } from '../../../../dist/assets/fonts/implementations/url-resolver.js'; import { orchestrate } from '../../../../dist/assets/fonts/orchestrate.js'; import { defineAstroFontProvider } from '../../../../dist/assets/fonts/providers/index.js'; -import { defaultLogger } from '../../test-utils.js'; +import { createSpyLogger, defaultLogger } from '../../test-utils.js'; import { - createSpyLogger, createSpyStorage, fakeFontMetricsResolver, fakeHasher, diff --git a/packages/astro/test/units/assets/fonts/utils.js b/packages/astro/test/units/assets/fonts/utils.js index a3211e293e18..9b1b8293929d 100644 --- a/packages/astro/test/units/assets/fonts/utils.js +++ b/packages/astro/test/units/assets/fonts/utils.js @@ -83,31 +83,6 @@ export const fakeFontMetricsResolver = { }, }; -export function createSpyLogger() { - /** @type {Array<{ type: string; label: string | null; message: string }>} */ - const logs = []; - - /** @type {import('../../../../dist/core/logger/core').Logger} */ - const logger = { - debug: (label, ...messages) => { - logs.push(...messages.map((message) => ({ type: 'debug', label, message }))); - }, - error: (label, message) => { - logs.push({ type: 'error', label, message }); - }, - info: (label, message) => { - logs.push({ type: 'info', label, message }); - }, - warn: (label, message) => { - logs.push({ type: 'warn', label, message }); - }, - }; - return { - logs, - logger, - }; -} - /** * @param {string} input */ diff --git a/packages/astro/test/units/cli/create-key.test.js b/packages/astro/test/units/cli/create-key.test.js new file mode 100644 index 000000000000..a6fc1643b261 --- /dev/null +++ b/packages/astro/test/units/cli/create-key.test.js @@ -0,0 +1,213 @@ +// @ts-check +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { createKey } from '../../../dist/cli/create-key/core/create-key.js'; +import { createBuildTimeAstroVersionProvider } from '../../../dist/cli/create-key/infra/build-time-astro-version-provider.js'; +import { createLoggerHelpDisplay } from '../../../dist/cli/create-key/infra/logger-help-display.js'; +import packageJson from '../../../package.json' with { type: 'json' }; +import { createSpyLogger } from '../test-utils.js'; + +describe('CLI create-key', () => { + describe('core', () => { + describe('createKey()', () => { + it('logs the generated key', async () => { + const { logger, logs } = createSpyLogger(); + /** @type {Array} */ + const payloads = []; + + await createKey({ + logger, + keyGenerator: { + generate: async () => 'FOO', + }, + helpDisplay: { + shouldFire: () => false, + show: (payload) => { + payloads.push(payload); + }, + }, + }); + + assert.deepStrictEqual(logs, [ + { + type: 'info', + label: 'crypto', + message: + 'Generated a key to encrypt props passed to Server islands. To reuse the same key across builds, set this value as ASTRO_KEY in an environment variable on your build server.\n\nASTRO_KEY=FOO', + }, + ]); + assert.deepStrictEqual(payloads, []); + }); + + it('logs the help', async () => { + const { logger, logs } = createSpyLogger(); + /** @type {Array} */ + const payloads = []; + + await createKey({ + logger, + keyGenerator: { + generate: async () => 'FOO', + }, + helpDisplay: { + shouldFire: () => true, + show: (payload) => { + payloads.push(payload); + }, + }, + }); + + assert.deepStrictEqual(logs, []); + assert.deepStrictEqual(payloads, [ + { + commandName: 'astro create-key', + tables: { + Flags: [['--help (-h)', 'See all available flags.']], + }, + description: 'Generates a key to encrypt props passed to Server islands.', + }, + ]); + }); + }); + }); + + describe('infra', () => { + describe('createBuildTimeAstroVersionProvider()', () => { + it('returns the value from the build', () => { + const astroVersionProvider = createBuildTimeAstroVersionProvider(); + + assert.equal(astroVersionProvider.getVersion(), packageJson.version); + }); + }); + + describe('createLoggerHelpDisplay()', () => { + describe('shouldFire()', () => { + it('returns false if no relevant flag is enabled', () => { + const { logger, logs } = createSpyLogger(); + const helpDisplay = createLoggerHelpDisplay({ + logger, + astroVersionProvider: { + getVersion: () => '1.0.0', + }, + flags: { + _: [], + }, + textStyler: { + bgWhite: (msg) => msg, + black: (msg) => msg, + dim: (msg) => msg, + green: (msg) => msg, + bold: (msg) => msg, + bgGreen: (msg) => msg, + }, + }); + + assert.equal(helpDisplay.shouldFire(), false); + assert.deepStrictEqual(logs, []); + }); + + it('returns true if help flag is enabled', () => { + const { logger, logs } = createSpyLogger(); + const helpDisplay = createLoggerHelpDisplay({ + logger, + astroVersionProvider: { + getVersion: () => '1.0.0', + }, + flags: { + _: [], + help: true, + }, + textStyler: { + bgWhite: (msg) => msg, + black: (msg) => msg, + dim: (msg) => msg, + green: (msg) => msg, + bold: (msg) => msg, + bgGreen: (msg) => msg, + }, + }); + + assert.equal(helpDisplay.shouldFire(), true); + assert.deepStrictEqual(logs, []); + }); + + it('returns true if h flag is enabled', () => { + const { logger, logs } = createSpyLogger(); + const helpDisplay = createLoggerHelpDisplay({ + logger, + astroVersionProvider: { + getVersion: () => '1.0.0', + }, + flags: { + _: [], + h: true, + }, + textStyler: { + bgWhite: (msg) => msg, + black: (msg) => msg, + dim: (msg) => msg, + green: (msg) => msg, + bold: (msg) => msg, + bgGreen: (msg) => msg, + }, + }); + + assert.equal(helpDisplay.shouldFire(), true); + assert.deepStrictEqual(logs, []); + }); + }); + + describe('show()', () => { + it('works', () => { + const { logger, logs } = createSpyLogger(); + const helpDisplay = createLoggerHelpDisplay({ + logger, + astroVersionProvider: { + getVersion: () => '1.0.0', + }, + flags: { + _: [], + }, + textStyler: { + bgWhite: (msg) => msg, + black: (msg) => msg, + dim: (msg) => msg, + green: (msg) => msg, + bold: (msg) => msg, + bgGreen: (msg) => msg, + }, + }); + + helpDisplay.show({ + commandName: 'astro preview', + usage: '[...flags]', + tables: { + Flags: [ + ['--port', `Specify which port to run on. Defaults to 4321.`], + ['--host', `Listen on all addresses, including LAN and public addresses.`], + ], + }, + description: 'Starts a local server to serve your static dist/ directory.', + }); + + assert.deepStrictEqual(logs, [ + { + type: 'info', + label: 'SKIP_FORMAT', + message: + ` + astro preview [...flags] + + Flags + --port Specify which port to run on. Defaults to 4321. + --host Listen on all addresses, including LAN and public addresses. + +Starts a local server to serve your static dist/ directory. +`, + }, + ]); + }); + }); + }); + }); +}); diff --git a/packages/astro/test/units/test-utils.js b/packages/astro/test/units/test-utils.js index 6132a2a4354d..bc64a0234b90 100644 --- a/packages/astro/test/units/test-utils.js +++ b/packages/astro/test/units/test-utils.js @@ -7,7 +7,7 @@ import { getDefaultClientDirectives } from '../../dist/core/client-directive/ind import { resolveConfig } from '../../dist/core/config/index.js'; import { createBaseSettings } from '../../dist/core/config/settings.js'; import { createContainer } from '../../dist/core/dev/container.js'; -import { Logger } from '../../dist/core/logger/core.js'; +import { AstroIntegrationLogger, Logger } from '../../dist/core/logger/core.js'; import { nodeLogDestination } from '../../dist/core/logger/node.js'; import { NOOP_MIDDLEWARE_FN } from '../../dist/core/middleware/noop-middleware.js'; import { Pipeline } from '../../dist/core/render/index.js'; @@ -167,3 +167,39 @@ export async function runInContainer(options = {}, callback) { await container.close(); } } + +export function createSpyLogger() { + /** @type {Array<{ type: string; label: string | null; message: string }>} */ + const logs = []; + + /** @type {import('../../dist/core/logger/core').Logger} */ + const logger = { + debug: (label, ...messages) => { + logs.push(...messages.map((message) => ({ type: 'debug', label, message }))); + }, + error: (label, message) => { + logs.push({ type: 'error', label, message }); + }, + info: (label, message) => { + logs.push({ type: 'info', label, message }); + }, + warn: (label, message) => { + logs.push({ type: 'warn', label, message }); + }, + options: { + dest: { + write: () => true, + }, + level: 'silent', + }, + level: () => 'silent', + forkIntegrationLogger(label) { + return new AstroIntegrationLogger(this.options, label); + }, + }; + + return { + logs, + logger, + }; +}