From 5e9711b9995056340255e735a5fd13eb37e4bbd1 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Wed, 15 Nov 2023 20:02:32 -0600 Subject: [PATCH 01/19] feat: add new preferences module --- packages/astro/package.json | 2 + packages/astro/src/@types/astro.ts | 2 + packages/astro/src/preferences/README.md | 21 ++++++ packages/astro/src/preferences/defaults.ts | 8 +++ packages/astro/src/preferences/index.ts | 84 ++++++++++++++++++++++ packages/astro/src/preferences/store.ts | 57 +++++++++++++++ 6 files changed, 174 insertions(+) create mode 100644 packages/astro/src/preferences/README.md create mode 100644 packages/astro/src/preferences/defaults.ts create mode 100644 packages/astro/src/preferences/index.ts create mode 100644 packages/astro/src/preferences/store.ts diff --git a/packages/astro/package.json b/packages/astro/package.json index 6b076fc7da46..f0bcb6229cd6 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -134,6 +134,8 @@ "deterministic-object-hash": "^2.0.1", "devalue": "^4.3.2", "diff": "^5.1.0", + "dlv": "^1.1.3", + "dset": "^3.1.2", "es-module-lexer": "^1.4.1", "esbuild": "^0.19.6", "estree-walker": "^3.0.3", diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 24b7b195f910..0acea76e4ad6 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -35,6 +35,7 @@ import type { import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server/index.js'; import type { OmitIndexSignature, Simplify } from '../type-utils.js'; import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js'; +import type { AstroPreferences } from '../preferences/index.js'; export { type AstroIntegrationLogger }; @@ -1678,6 +1679,7 @@ export interface AstroAdapterFeatures { export interface AstroSettings { config: AstroConfig; adapter: AstroAdapter | undefined; + preferences: AstroPreferences; injectedRoutes: InjectedRoute[]; resolvedInjectedRoutes: ResolvedInjectedRoute[]; pageExtensions: string[]; diff --git a/packages/astro/src/preferences/README.md b/packages/astro/src/preferences/README.md new file mode 100644 index 000000000000..a690010297f8 --- /dev/null +++ b/packages/astro/src/preferences/README.md @@ -0,0 +1,21 @@ +# Preferences + +The preferences module implements global and local user preferences for controlling certain Astro behavior. Whereas the `astro.config.mjs` file controls project-specific behavior for every user of a project, preferences are user-specific. + +The design of Preferences is inspired by [Visual Studio Code's Settings](https://code.visualstudio.com/docs/getstarted/settings), which are layered in a similar way. + +## `AstroPreferences` + +The `AstroPreferences` interface exposes both a `get` and `set` function. + +### Reading a preference + +`preferences.get("dot.separated.value")` will read a preference value from multiple sources if needed. Local project preferences are read from `.astro/settings.json`, if it exists. Next, global user preferences are read from `//astro/settings.json`. If neither of those are found, the default preferences defined in [`./defaults.ts`](./defaults.ts) will apply. + +### Writing a preference + +`preferences.set("dot.separated.value", true)` will store a preference value. By default, preferences are stored locally in a project. In order to set a global user preference, you can pass the `target: "global"` option (`preferences.set("dot.separated.value", true, { target: 'global' })`). + +## Relation to Telemetry + +This module evolved from the existing `@astrojs/telemetry` package, but has been generalized for user-facing `astro` preferences. At some point, we'll need to merge the logic in `@astrojs/telemetry` and the logic in this module so that all preferences are stored in the same location. diff --git a/packages/astro/src/preferences/defaults.ts b/packages/astro/src/preferences/defaults.ts new file mode 100644 index 000000000000..74ae7b2dc6cc --- /dev/null +++ b/packages/astro/src/preferences/defaults.ts @@ -0,0 +1,8 @@ +export const DEFAULT_PREFERENCES = { + devOverlay: { + /** Specifies whether the user has the Dev Overlay enabled */ + enabled: true, + }, +} + +export type Preferences = typeof DEFAULT_PREFERENCES; diff --git a/packages/astro/src/preferences/index.ts b/packages/astro/src/preferences/index.ts new file mode 100644 index 000000000000..e49eb45d4e87 --- /dev/null +++ b/packages/astro/src/preferences/index.ts @@ -0,0 +1,84 @@ +import type { AstroConfig } from '../@types/astro.js'; + +import { fileURLToPath } from 'node:url'; +import os from 'node:os'; +import process from 'node:process'; +import path from 'node:path'; + +import dget from 'dlv'; +import { DEFAULT_PREFERENCES, type Preferences } from './defaults.js'; +import { PreferenceStore } from './store.js'; + +type DotKeys = T extends object ? { [K in keyof T]: + `${Exclude}${DotKeys extends never ? "" : `.${DotKeys}`}` +}[keyof T] : never + +export type GetDotKey< + T extends Record, + K extends string +> = K extends `${infer U}.${infer Rest}` ? GetDotKey : T[K] + +export interface PreferenceOptions { + location?: 'global' | 'project'; +} + +export interface AstroPreferences { + get>(key: Key, opts?: PreferenceOptions): GetDotKey; + set>(key: Key, value: GetDotKey, opts?: PreferenceOptions): void; +} + +export function isValidKey(key: string): key is DotKeys { + return dget(DEFAULT_PREFERENCES, key) !== undefined; +} +export function coerce(key: string, value: string | number) { + const type = typeof dget(DEFAULT_PREFERENCES, key); + switch (type) { + case 'string': return value; + case 'number': return Number(value); + case 'boolean': { + if (value === 'true' || value === 1) return true; + if (value === 'false' || value === 0) return false; + } + } + throw new Error(`Unable to convert "${value}" to a ${type} for "${key}".`) +} + +export default function createPreferences(config: AstroConfig): AstroPreferences { + const global = new PreferenceStore(getGlobalPreferenceDir()); + const project = new PreferenceStore(fileURLToPath(new URL('./.astro/', config.root))); + const stores = { global, project }; + + return { + get(key, { location } = {}) { + if (!location) return project.get(key) ?? global.get(key) ?? dget(DEFAULT_PREFERENCES, key); + return stores[location].get(key); + }, + set(key, value, { location = 'project' } = {}) { + stores[location].set(key, value); + } + } +} + + +// Adapted from https://github.com/sindresorhus/env-paths +export function getGlobalPreferenceDir() { + const name = 'astro'; + const homedir = os.homedir(); + const macos = () => path.join(homedir, 'Library', 'Preferences', name); + const win = () => { + const { APPDATA = path.join(homedir, 'AppData', 'Roaming') } = process.env; + return path.join(APPDATA, name, 'Config'); + }; + const linux = () => { + const { XDG_CONFIG_HOME = path.join(homedir, '.config') } = process.env; + return path.join(XDG_CONFIG_HOME, name); + }; + switch (process.platform) { + case 'darwin': + return macos(); + case 'win32': + return win(); + default: + return linux(); + } +} diff --git a/packages/astro/src/preferences/store.ts b/packages/astro/src/preferences/store.ts new file mode 100644 index 000000000000..d6cec4fd0cb2 --- /dev/null +++ b/packages/astro/src/preferences/store.ts @@ -0,0 +1,57 @@ +import dget from 'dlv'; +// @ts-expect-error `dset` is mispackaged: https://publint.dev/dset@3.1.2 +import { dset } from 'dset'; +import fs from 'node:fs'; +import path from 'node:path'; + +export class PreferenceStore { + private file: string; + + constructor(private dir: string, filename = 'settings.json') { + this.file = path.join(this.dir, filename); + } + + private _store?: Record; + private get store(): Record { + if (this._store) return this._store; + if (fs.existsSync(this.file)) { + try { + this._store = JSON.parse(fs.readFileSync(this.file).toString()); + } catch {} + } + if (!this._store) { + this._store = {}; + this.write(); + } + return this._store; + } + private set store(value: Record) { + this._store = value; + this.write(); + } + write() { + if (!this._store || Object.keys(this._store).length === 0) return; + fs.mkdirSync(this.dir, { recursive: true }); + fs.writeFileSync(this.file, JSON.stringify(this.store, null, '\t')); + } + clear(): void { + this.store = {}; + fs.rmSync(this.file, { recursive: true }); + } + delete(key: string): boolean { + dset(this.store, key, undefined); + this.write(); + return true; + } + get(key: string): any { + return dget(this.store, key); + } + has(key: string): boolean { + return typeof this.get(key) !== 'undefined'; + } + set(key: string, value: any): void { + if (this.get(key) === value) return; + dset(this.store, key, value); + this.write(); + } +} From 2f62d1d3b1d53af49b6a2725411e7838b901b788 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Wed, 15 Nov 2023 20:02:53 -0600 Subject: [PATCH 02/19] feat: wire up preferences to devOverlay --- packages/astro/src/vite-plugin-astro-server/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index a58f38a02b53..c9e610bab8b1 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -384,7 +384,7 @@ async function getScriptsAndStyles({ pipeline, filePath }: GetScriptsAndStylesPa children: '', }); - if (settings.config.devOverlay.enabled) { + if (settings.config.devOverlay.enabled && settings.preferences.get('devOverlay.enabled')) { scripts.add({ props: { type: 'module', From 72c8a43766fd01158889aec54901811cbae87be3 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Wed, 15 Nov 2023 20:03:23 -0600 Subject: [PATCH 03/19] fix: restart dev server on preference changes --- packages/astro/src/core/dev/restart.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/core/dev/restart.ts b/packages/astro/src/core/dev/restart.ts index 16c1b1b2ce5b..49cd185d8a27 100644 --- a/packages/astro/src/core/dev/restart.ts +++ b/packages/astro/src/core/dev/restart.ts @@ -29,6 +29,9 @@ async function createRestartedContainer( return newContainer; } +const configRE = new RegExp(`.*astro\.config\.((mjs)|(cjs)|(js)|(ts))$`); +const preferencesRE = new RegExp(`.*\.astro\/settings\.json$`); + export function shouldRestartContainer( { settings, inlineConfig, restartInFlight }: Container, changedFile: string @@ -43,9 +46,9 @@ export function shouldRestartContainer( } // Otherwise, watch for any astro.config.* file changes in project root else { - const exp = new RegExp(`.*astro\.config\.((mjs)|(cjs)|(js)|(ts))$`); const normalizedChangedFile = vite.normalizePath(changedFile); - shouldRestart = exp.test(normalizedChangedFile); + shouldRestart = configRE.test(normalizedChangedFile) || preferencesRE.test(normalizedChangedFile); + } if (!shouldRestart && settings.watchFiles.length > 0) { From 2167cfa8a39422bb85aefe934bd457bf9bdb01ab Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Wed, 15 Nov 2023 20:03:42 -0600 Subject: [PATCH 04/19] feat(cli): add `astro config` command --- packages/astro/src/cli/config/index.ts | 50 ++++++++++++++++++++++ packages/astro/src/cli/index.ts | 11 ++++- packages/astro/src/core/config/settings.ts | 3 ++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 packages/astro/src/cli/config/index.ts diff --git a/packages/astro/src/cli/config/index.ts b/packages/astro/src/cli/config/index.ts new file mode 100644 index 000000000000..fecdb899854d --- /dev/null +++ b/packages/astro/src/cli/config/index.ts @@ -0,0 +1,50 @@ +import type yargs from 'yargs-parser'; + +import { cyan } from 'kleur/colors'; +import { fileURLToPath } from 'node:url'; + +import { printHelp } from '../../core/messages.js'; +import { flagsToAstroInlineConfig } from '../flags.js'; +import { resolveConfig } from '../../core/config/config.js'; +import { createSettings } from '../../core/config/settings.js'; +import { isValidKey, coerce } from '../../preferences/index.js'; + +interface PreferencesOptions { + flags: yargs.Arguments; +} + +export async function config(key: string, value: string | undefined, { flags }: PreferencesOptions): Promise { + if (flags?.help || flags?.h) { + printHelp({ + commandName: 'astro config', + usage: '[key] [:value]', + tables: { + Flags: [ + ['--global', 'Set configuration globally.'], + ['--help (-h)', 'See all available flags.'], + ], + }, + description: `Starts a local server to serve your static dist/ directory. Check ${cyan( + 'https://docs.astro.build/en/reference/cli-reference/#astro-preview' + )} for more information.`, + }); + return 0; + } + + const inlineConfig = flagsToAstroInlineConfig(flags); + const { astroConfig } = await resolveConfig(inlineConfig ?? {}, 'dev'); + const settings = await createSettings(astroConfig, fileURLToPath(astroConfig.root)); + const location = flags.global ? 'global' : undefined; + if (!isValidKey(key)) { + throw new Error(`Unsupported config key "${key}"!`); + } + + if (value === undefined) { + // eslint-disable-next-line no-console + console.log(settings.preferences.get(key, { location })); + } else { + settings.preferences.set(key, coerce(key, value) as any, { location }); + } + + return 0; +} diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index 0421258a51ae..c8143cb8a7a6 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -14,6 +14,7 @@ type CLICommand = | 'sync' | 'check' | 'info' + | 'config' | 'telemetry'; /** Display --help flag */ @@ -33,6 +34,7 @@ async function printAstroHelp() { ['info', 'List info about your current Astro setup.'], ['preview', 'Preview your build locally.'], ['sync', 'Generate content collection types.'], + ['config', 'Configure user preferences.'], ['telemetry', 'Configure telemetry settings.'], ], 'Global Flags': [ @@ -64,6 +66,7 @@ function resolveCommand(flags: yargs.Arguments): CLICommand { 'add', 'sync', 'telemetry', + 'config', 'dev', 'build', 'preview', @@ -114,6 +117,12 @@ async function runCommand(cmd: string, flags: yargs.Arguments) { const exitCode = await sync({ flags }); return process.exit(exitCode); } + case 'config': { + const { config } = await import('./config/index.js'); + const [key, value] = flags._.slice(3); + const exitCode = await config(key, value, { flags }); + return process.exit(exitCode); + } } // In verbose/debug mode, we log the debug logs asap before any potential errors could appear @@ -177,7 +186,7 @@ async function runCommand(cmd: string, flags: yargs.Arguments) { /** The primary CLI action */ export async function cli(args: string[]) { - const flags = yargs(args); + const flags = yargs(args, { boolean: ['global'], alias: { g: 'global' } }); const cmd = resolveCommand(flags); try { await runCommand(cmd, flags); diff --git a/packages/astro/src/core/config/settings.ts b/packages/astro/src/core/config/settings.ts index fca392c97672..29df00eafc1e 100644 --- a/packages/astro/src/core/config/settings.ts +++ b/packages/astro/src/core/config/settings.ts @@ -10,11 +10,14 @@ import { formatYAMLException, isYAMLException } from '../errors/utils.js'; import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../constants.js'; import { AstroTimer } from './timer.js'; import { loadTSConfig } from './tsconfig.js'; +import createPreferences from '../../preferences/index.js'; export function createBaseSettings(config: AstroConfig): AstroSettings { const { contentDir } = getContentPaths(config); + const preferences = createPreferences(config); return { config, + preferences, tsConfig: undefined, tsConfigPath: undefined, adapter: undefined, From 6bd38600f3fc59cda982dced1c1875239ded9b38 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Thu, 16 Nov 2023 08:03:20 -0600 Subject: [PATCH 05/19] chore: update README --- packages/astro/src/preferences/README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/preferences/README.md b/packages/astro/src/preferences/README.md index a690010297f8..4234ebac1b67 100644 --- a/packages/astro/src/preferences/README.md +++ b/packages/astro/src/preferences/README.md @@ -2,7 +2,7 @@ The preferences module implements global and local user preferences for controlling certain Astro behavior. Whereas the `astro.config.mjs` file controls project-specific behavior for every user of a project, preferences are user-specific. -The design of Preferences is inspired by [Visual Studio Code's Settings](https://code.visualstudio.com/docs/getstarted/settings), which are layered in a similar way. +The design of Preferences is inspired by [Git](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration) and [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/settings). Both systems implement similar layering approaches with project-specific and global settings. ## `AstroPreferences` @@ -12,9 +12,21 @@ The `AstroPreferences` interface exposes both a `get` and `set` function. `preferences.get("dot.separated.value")` will read a preference value from multiple sources if needed. Local project preferences are read from `.astro/settings.json`, if it exists. Next, global user preferences are read from `//astro/settings.json`. If neither of those are found, the default preferences defined in [`./defaults.ts`](./defaults.ts) will apply. +In order to read a preference from a specific location, you can pass the `location: "global" | "project"` option. + +```js +await preferences.get('dot.separated.value', { location: 'global' }); +``` + ### Writing a preference -`preferences.set("dot.separated.value", true)` will store a preference value. By default, preferences are stored locally in a project. In order to set a global user preference, you can pass the `target: "global"` option (`preferences.set("dot.separated.value", true, { target: 'global' })`). +`preferences.set("dot.separated.value", true)` will store a preference value. By default, preferences are stored locally in a project. + +In order to set a global user preference, you can pass the `location: "global"` option. + +```js +await preferences.set('dot.separated.value', 'value', { location: 'global' }); +``` ## Relation to Telemetry From ece26a7434169b48e3c976113a820050ad605023 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Thu, 16 Nov 2023 08:36:38 -0600 Subject: [PATCH 06/19] chore: add changeset --- .changeset/gentle-cobras-wash.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .changeset/gentle-cobras-wash.md diff --git a/.changeset/gentle-cobras-wash.md b/.changeset/gentle-cobras-wash.md new file mode 100644 index 000000000000..5a15a3c56f89 --- /dev/null +++ b/.changeset/gentle-cobras-wash.md @@ -0,0 +1,17 @@ +--- +'astro': minor +--- + +Adds the `astro config` command to manage user preferences. User preferences are specific to individual Astro users, unlike the `astro.config.mjs` file which changes behavior for everyone working on a project. + +User preferences are scoped to the current project by default, stored in a local `.astro/settings.json` file. Using the `--global` flag, user preferences can also be applied to every Astro project on the current machine. Global user preferences are stored in an operating system-specific location. + +```sh +# Disable the dev overlay for the current user in the current project +npm run astro config devOverlay.enabled false +# Disable the dev overlay for the current user in all Astro projects on this machine +npm run astro config --global devOverlay.enabled false + +# Check if the dev overlay is enabled for the current user +npm run astro config devOverlay.enabled +``` From 539b1a0bb057be5501975aaa5804199bb8294013 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Thu, 16 Nov 2023 08:53:07 -0600 Subject: [PATCH 07/19] fix: cast flags to string --- packages/astro/src/cli/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index c8143cb8a7a6..8dc718e8115f 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -119,7 +119,7 @@ async function runCommand(cmd: string, flags: yargs.Arguments) { } case 'config': { const { config } = await import('./config/index.js'); - const [key, value] = flags._.slice(3); + const [key, value] = flags._.slice(3).map(v => v.toString()); const exitCode = await config(key, value, { flags }); return process.exit(exitCode); } From ae45b38bc0ce3d3cb4e325333c330b8b561a28e1 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 17 Nov 2023 16:31:29 -0600 Subject: [PATCH 08/19] refactor: rename CLI command to preferences --- packages/astro/src/cli/config/index.ts | 50 ------- packages/astro/src/cli/index.ts | 14 +- packages/astro/src/cli/preferences/index.ts | 151 ++++++++++++++++++++ packages/astro/src/core/messages.ts | 24 ++++ packages/astro/src/preferences/index.ts | 19 ++- packages/astro/src/preferences/store.ts | 3 + 6 files changed, 198 insertions(+), 63 deletions(-) delete mode 100644 packages/astro/src/cli/config/index.ts create mode 100644 packages/astro/src/cli/preferences/index.ts diff --git a/packages/astro/src/cli/config/index.ts b/packages/astro/src/cli/config/index.ts deleted file mode 100644 index fecdb899854d..000000000000 --- a/packages/astro/src/cli/config/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type yargs from 'yargs-parser'; - -import { cyan } from 'kleur/colors'; -import { fileURLToPath } from 'node:url'; - -import { printHelp } from '../../core/messages.js'; -import { flagsToAstroInlineConfig } from '../flags.js'; -import { resolveConfig } from '../../core/config/config.js'; -import { createSettings } from '../../core/config/settings.js'; -import { isValidKey, coerce } from '../../preferences/index.js'; - -interface PreferencesOptions { - flags: yargs.Arguments; -} - -export async function config(key: string, value: string | undefined, { flags }: PreferencesOptions): Promise { - if (flags?.help || flags?.h) { - printHelp({ - commandName: 'astro config', - usage: '[key] [:value]', - tables: { - Flags: [ - ['--global', 'Set configuration globally.'], - ['--help (-h)', 'See all available flags.'], - ], - }, - description: `Starts a local server to serve your static dist/ directory. Check ${cyan( - 'https://docs.astro.build/en/reference/cli-reference/#astro-preview' - )} for more information.`, - }); - return 0; - } - - const inlineConfig = flagsToAstroInlineConfig(flags); - const { astroConfig } = await resolveConfig(inlineConfig ?? {}, 'dev'); - const settings = await createSettings(astroConfig, fileURLToPath(astroConfig.root)); - const location = flags.global ? 'global' : undefined; - if (!isValidKey(key)) { - throw new Error(`Unsupported config key "${key}"!`); - } - - if (value === undefined) { - // eslint-disable-next-line no-console - console.log(settings.preferences.get(key, { location })); - } else { - settings.preferences.set(key, coerce(key, value) as any, { location }); - } - - return 0; -} diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index 8dc718e8115f..83dd960c63fb 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -14,7 +14,7 @@ type CLICommand = | 'sync' | 'check' | 'info' - | 'config' + | 'preferences' | 'telemetry'; /** Display --help flag */ @@ -34,7 +34,7 @@ async function printAstroHelp() { ['info', 'List info about your current Astro setup.'], ['preview', 'Preview your build locally.'], ['sync', 'Generate content collection types.'], - ['config', 'Configure user preferences.'], + ['preferences', 'Configure user preferences.'], ['telemetry', 'Configure telemetry settings.'], ], 'Global Flags': [ @@ -66,7 +66,7 @@ function resolveCommand(flags: yargs.Arguments): CLICommand { 'add', 'sync', 'telemetry', - 'config', + 'preferences', 'dev', 'build', 'preview', @@ -117,10 +117,10 @@ async function runCommand(cmd: string, flags: yargs.Arguments) { const exitCode = await sync({ flags }); return process.exit(exitCode); } - case 'config': { - const { config } = await import('./config/index.js'); - const [key, value] = flags._.slice(3).map(v => v.toString()); - const exitCode = await config(key, value, { flags }); + case 'preferences': { + const { preferences } = await import('./preferences/index.js'); + const [subcommand, key, value] = flags._.slice(3).map(v => v.toString()); + const exitCode = await preferences(subcommand, key, value, { flags }); return process.exit(exitCode); } } diff --git a/packages/astro/src/cli/preferences/index.ts b/packages/astro/src/cli/preferences/index.ts new file mode 100644 index 000000000000..5bd51b8ec45a --- /dev/null +++ b/packages/astro/src/cli/preferences/index.ts @@ -0,0 +1,151 @@ +/* eslint-disable no-console */ +import type yargs from 'yargs-parser'; +import type { AstroSettings } from '../../@types/astro.js'; + +import { cyan } from 'kleur/colors'; +import { fileURLToPath } from 'node:url'; + +import * as msg from '../../core/messages.js'; +import { flagsToAstroInlineConfig } from '../flags.js'; +import { resolveConfig } from '../../core/config/config.js'; +import { createSettings } from '../../core/config/settings.js'; +import { isValidKey, coerce, type PreferenceKey } from '../../preferences/index.js'; +import { error, log } from '../../core/logger/core.js'; +import { nodeLogOptions } from '../../core/logger/node.js'; +import { DEFAULT_PREFERENCES, type Preferences } from '../../preferences/defaults.js'; +import dlv from 'dlv'; + + +interface PreferencesOptions { + flags: yargs.Arguments; +} + +const PREFERENCES_SUBCOMMANDS = ['get', 'set', 'enable', 'disable', 'delete', 'reset', 'list'] as const; +export type Subcommand = typeof PREFERENCES_SUBCOMMANDS[number]; + +function isValidSubcommand(subcommand: string): subcommand is Subcommand { + return PREFERENCES_SUBCOMMANDS.includes(subcommand as Subcommand); +} + +export async function preferences(subcommand: string, key: string, value: string | undefined, { flags }: PreferencesOptions): Promise { + if (!isValidSubcommand(subcommand) || flags?.help || flags?.h) { + printHelp({ + commandName: 'astro preferences', + usage: 'set [key] [:value]', + tables: { + Flags: [ + ['--global', 'Change setting value globally.'], + ['--help (-h)', 'See all available flags.'], + ], + }, + description: `Starts a local server to serve your static dist/ directory. Check ${cyan( + 'https://docs.astro.build/en/reference/cli-reference/#astro-preview' + )} for more information.`, + }); + return 0; + } + + const inlineConfig = flagsToAstroInlineConfig(flags); + const { astroConfig } = await resolveConfig(inlineConfig ?? {}, 'dev'); + const settings = await createSettings(astroConfig, fileURLToPath(astroConfig.root)); + const opts: SubcommandOptions = { + location: flags.global ? 'global' : undefined, + json: flags.json + } + + if (subcommand === 'list') { + return listPreferences(settings, opts); + } + + if (subcommand === 'enable' || subcommand === 'disable') { + key = `${key}.enabled` as PreferenceKey; + } + + if (!isValidKey(key)) { + error(nodeLogOptions, 'preferences', `Unknown preference "${key}"\n`); + return 1; + } + + if (subcommand === 'set' && value === undefined) { + const type = typeof dlv(DEFAULT_PREFERENCES, key); + // TODO: better error message + error(nodeLogOptions, 'preferences', `Please provide a ${type} value for "${key}"\n`); + return 1; + } + + switch (subcommand) { + case 'get': return getPreference(settings, key, opts); + case 'set': return setPreference(settings, key, value, opts); + case 'reset': + case 'delete': return resetPreference(settings, key, opts); + case 'enable': return enablePreference(settings, key, opts); + case 'disable': return disablePreference(settings, key, opts); + } +} + +interface SubcommandOptions { + location?: 'global' | 'project'; + json?: boolean; +} + +// Default `location` to "project" to avoid reading default preferencesa +async function getPreference(settings: AstroSettings, key: PreferenceKey, { location = 'project' }: SubcommandOptions) { + try { + const value = await settings.preferences.get(key, { location }); + // TODO: guard against printing objects + if (value !== undefined) { + console.log(msg.preferenceGet(key, value)); + } else { + const defaultValue = await settings.preferences.get(key); + console.log(msg.preferenceDefault(key, defaultValue)); + } + return 0; + } catch {} + return 1; +} + +async function setPreference(settings: AstroSettings, key: PreferenceKey, value: unknown, { location }: SubcommandOptions) { + try { + await settings.preferences.set(key, value as any, { location }); + console.log(msg.preferenceSet(key, value)) + return 0; + } catch {} + return 1; +} + +async function enablePreference(settings: AstroSettings, key: PreferenceKey, { location }: SubcommandOptions) { + try { + await settings.preferences.set(key, true, { location }); + console.log(msg.preferenceEnabled(key.replace('.enabled', ''))) + return 0; + } catch {} + return 1; +} + +async function disablePreference(settings: AstroSettings, key: PreferenceKey, { location }: SubcommandOptions) { + try { + await settings.preferences.set(key, false, { location }); + console.log(msg.preferenceDisabled(key.replace('.enabled', ''))) + return 0; + } catch {} + return 1; +} + +async function resetPreference(settings: AstroSettings, key: PreferenceKey, { location }: SubcommandOptions) { + try { + await settings.preferences.set(key, undefined as any, { location }); + console.log(msg.preferenceReset(key)) + return 0; + } catch {} + return 1; +} + +async function listPreferences(settings: AstroSettings, { location, json }: SubcommandOptions) { + const store = await settings.preferences.getAll({ location }); + if (json) { + console.log(JSON.stringify(store, null, 2)); + } + // TODO: pretty print + console.log(JSON.stringify(store, null, 2)); + return 0; +} diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts index 1c37909622b4..10336ab7a0c4 100644 --- a/packages/astro/src/core/messages.ts +++ b/packages/astro/src/core/messages.ts @@ -110,6 +110,30 @@ export function telemetryEnabled() { ].join('\n'); } +export function preferenceEnabled(name: string) { + return `${green('◉')} ${name} is now ${bgGreen(black(' enabled '))}\n`; +} + +export function preferenceSet(name: string, value: any) { + return `${green('◉')} ${name} has been set to ${bgGreen(black(` ${JSON.stringify(value)} `))}\n`; +} + +export function preferenceGet(name: string, value: any) { + return `${green('◉')} ${name} is set to ${bgGreen(black(` ${JSON.stringify(value)} `))}\n`; +} + +export function preferenceDefault(name: string, value: any) { + return `${yellow('◯')} ${name} has not been set. It defaults to ${bgYellow(black(` ${JSON.stringify(value)} `))}\n`; +} + +export function preferenceDisabled(name: string) { + return `${yellow('◯')} ${name} is now ${bgYellow(black(' disabled '))}\n`; +} + +export function preferenceReset(name: string) { + return `${cyan('◆')} ${name} has been reset ${bgCyan(black(' reset '))}\n`; +} + export function telemetryDisabled() { return [ green('▶ Anonymous telemetry ') + bgGreen(' disabled '), diff --git a/packages/astro/src/preferences/index.ts b/packages/astro/src/preferences/index.ts index e49eb45d4e87..90612bfb3773 100644 --- a/packages/astro/src/preferences/index.ts +++ b/packages/astro/src/preferences/index.ts @@ -22,12 +22,15 @@ export interface PreferenceOptions { location?: 'global' | 'project'; } +export type PreferenceKey = DotKeys; + export interface AstroPreferences { - get>(key: Key, opts?: PreferenceOptions): GetDotKey; - set>(key: Key, value: GetDotKey, opts?: PreferenceOptions): void; + get(key: Key, opts?: PreferenceOptions): Promise>; + set(key: Key, value: GetDotKey, opts?: PreferenceOptions): Promise; + getAll(opts?: PreferenceOptions): Promise>; } -export function isValidKey(key: string): key is DotKeys { +export function isValidKey(key: string): key is PreferenceKey { return dget(DEFAULT_PREFERENCES, key) !== undefined; } export function coerce(key: string, value: string | number) { @@ -49,13 +52,17 @@ export default function createPreferences(config: AstroConfig): AstroPreferences const stores = { global, project }; return { - get(key, { location } = {}) { + async get(key, { location } = {}) { if (!location) return project.get(key) ?? global.get(key) ?? dget(DEFAULT_PREFERENCES, key); return stores[location].get(key); }, - set(key, value, { location = 'project' } = {}) { + async set(key, value, { location = 'project' } = {}) { stores[location].set(key, value); - } + }, + async getAll({ location } = {}) { + if (!location) return Object.assign({}, stores['global'].getAll(), stores['project'].getAll()); + return stores[location].getAll(); + }, } } diff --git a/packages/astro/src/preferences/store.ts b/packages/astro/src/preferences/store.ts index d6cec4fd0cb2..d3d0e99ba7af 100644 --- a/packages/astro/src/preferences/store.ts +++ b/packages/astro/src/preferences/store.ts @@ -54,4 +54,7 @@ export class PreferenceStore { dset(this.store, key, value); this.write(); } + getAll(): Record { + return this.store; + } } From d943fbf072d5f36b13d96b3a4254b6797f67885b Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 20 Nov 2023 09:44:44 -0600 Subject: [PATCH 09/19] chore: fix build issues --- packages/astro/src/cli/preferences/index.ts | 8 ++++---- packages/astro/src/vite-plugin-astro-server/route.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/astro/src/cli/preferences/index.ts b/packages/astro/src/cli/preferences/index.ts index 5bd51b8ec45a..4cc82cc46dcb 100644 --- a/packages/astro/src/cli/preferences/index.ts +++ b/packages/astro/src/cli/preferences/index.ts @@ -9,10 +9,10 @@ import * as msg from '../../core/messages.js'; import { flagsToAstroInlineConfig } from '../flags.js'; import { resolveConfig } from '../../core/config/config.js'; import { createSettings } from '../../core/config/settings.js'; -import { isValidKey, coerce, type PreferenceKey } from '../../preferences/index.js'; -import { error, log } from '../../core/logger/core.js'; +import { isValidKey, type PreferenceKey } from '../../preferences/index.js'; +import { error } from '../../core/logger/core.js'; import { nodeLogOptions } from '../../core/logger/node.js'; -import { DEFAULT_PREFERENCES, type Preferences } from '../../preferences/defaults.js'; +import { DEFAULT_PREFERENCES } from '../../preferences/defaults.js'; import dlv from 'dlv'; @@ -29,7 +29,7 @@ function isValidSubcommand(subcommand: string): subcommand is Subcommand { export async function preferences(subcommand: string, key: string, value: string | undefined, { flags }: PreferencesOptions): Promise { if (!isValidSubcommand(subcommand) || flags?.help || flags?.h) { - printHelp({ + msg.printHelp({ commandName: 'astro preferences', usage: 'set [key] [:value]', tables: { diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index c9e610bab8b1..a8f9efa049b5 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -384,7 +384,7 @@ async function getScriptsAndStyles({ pipeline, filePath }: GetScriptsAndStylesPa children: '', }); - if (settings.config.devOverlay.enabled && settings.preferences.get('devOverlay.enabled')) { + if (settings.config.devOverlay.enabled && await settings.preferences.get('devOverlay.enabled')) { scripts.add({ props: { type: 'module', From f6aedaa0df412b13a0873af69d6dfa37eca93363 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 27 Nov 2023 16:14:58 -0600 Subject: [PATCH 10/19] chore: update dependencies --- packages/astro/package.json | 3 ++- packages/astro/src/preferences/store.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/astro/package.json b/packages/astro/package.json index f0bcb6229cd6..140848faab10 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -135,7 +135,7 @@ "devalue": "^4.3.2", "diff": "^5.1.0", "dlv": "^1.1.3", - "dset": "^3.1.2", + "dset": "^3.1.3", "es-module-lexer": "^1.4.1", "esbuild": "^0.19.6", "estree-walker": "^3.0.3", @@ -187,6 +187,7 @@ "@types/cookie": "^0.5.4", "@types/debug": "^4.1.12", "@types/diff": "^5.0.8", + "@types/dlv": "^1.1.4", "@types/dom-view-transitions": "^1.0.4", "@types/estree": "^1.0.5", "@types/hast": "^3.0.3", diff --git a/packages/astro/src/preferences/store.ts b/packages/astro/src/preferences/store.ts index d3d0e99ba7af..4dabbba1cebb 100644 --- a/packages/astro/src/preferences/store.ts +++ b/packages/astro/src/preferences/store.ts @@ -1,5 +1,4 @@ import dget from 'dlv'; -// @ts-expect-error `dset` is mispackaged: https://publint.dev/dset@3.1.2 import { dset } from 'dset'; import fs from 'node:fs'; import path from 'node:path'; From 5dcabe1e67b3ced66d936005dad611465386fe1f Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 27 Nov 2023 16:22:20 -0600 Subject: [PATCH 11/19] chore: update changelog --- .changeset/gentle-cobras-wash.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/gentle-cobras-wash.md b/.changeset/gentle-cobras-wash.md index 5a15a3c56f89..1a9245524aae 100644 --- a/.changeset/gentle-cobras-wash.md +++ b/.changeset/gentle-cobras-wash.md @@ -2,16 +2,16 @@ 'astro': minor --- -Adds the `astro config` command to manage user preferences. User preferences are specific to individual Astro users, unlike the `astro.config.mjs` file which changes behavior for everyone working on a project. +Adds the `astro preferences` command to manage user preferences. User preferences are specific to individual Astro users, unlike the `astro.config.mjs` file which changes behavior for everyone working on a project. User preferences are scoped to the current project by default, stored in a local `.astro/settings.json` file. Using the `--global` flag, user preferences can also be applied to every Astro project on the current machine. Global user preferences are stored in an operating system-specific location. ```sh # Disable the dev overlay for the current user in the current project -npm run astro config devOverlay.enabled false +npm run astro preferences disable devOverlay # Disable the dev overlay for the current user in all Astro projects on this machine -npm run astro config --global devOverlay.enabled false +npm run astro preferences --global disable devOverlay # Check if the dev overlay is enabled for the current user -npm run astro config devOverlay.enabled +npm run astro preferences list devOverlay ``` From b19ba59279be0fba49842b592b79d23e06e2766e Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 27 Nov 2023 16:51:21 -0600 Subject: [PATCH 12/19] fix: use Logger in `preferences` module --- packages/astro/src/cli/preferences/index.ts | 9 ++++----- packages/astro/src/core/logger/core.ts | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/astro/src/cli/preferences/index.ts b/packages/astro/src/cli/preferences/index.ts index 4cc82cc46dcb..cd53ad3f80dc 100644 --- a/packages/astro/src/cli/preferences/index.ts +++ b/packages/astro/src/cli/preferences/index.ts @@ -6,12 +6,10 @@ import { cyan } from 'kleur/colors'; import { fileURLToPath } from 'node:url'; import * as msg from '../../core/messages.js'; -import { flagsToAstroInlineConfig } from '../flags.js'; +import { createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js'; import { resolveConfig } from '../../core/config/config.js'; import { createSettings } from '../../core/config/settings.js'; import { isValidKey, type PreferenceKey } from '../../preferences/index.js'; -import { error } from '../../core/logger/core.js'; -import { nodeLogOptions } from '../../core/logger/node.js'; import { DEFAULT_PREFERENCES } from '../../preferences/defaults.js'; import dlv from 'dlv'; @@ -46,6 +44,7 @@ export async function preferences(subcommand: string, key: string, value: string } const inlineConfig = flagsToAstroInlineConfig(flags); + const logger = createLoggerFromFlags(flags); const { astroConfig } = await resolveConfig(inlineConfig ?? {}, 'dev'); const settings = await createSettings(astroConfig, fileURLToPath(astroConfig.root)); const opts: SubcommandOptions = { @@ -62,14 +61,14 @@ export async function preferences(subcommand: string, key: string, value: string } if (!isValidKey(key)) { - error(nodeLogOptions, 'preferences', `Unknown preference "${key}"\n`); + logger.error('preferences', `Unknown preference "${key}"\n`); return 1; } if (subcommand === 'set' && value === undefined) { const type = typeof dlv(DEFAULT_PREFERENCES, key); // TODO: better error message - error(nodeLogOptions, 'preferences', `Please provide a ${type} value for "${key}"\n`); + logger.error('preferences', `Please provide a ${type} value for "${key}"\n`); return 1; } diff --git a/packages/astro/src/core/logger/core.ts b/packages/astro/src/core/logger/core.ts index 2c26a55a0d3d..5dab122135af 100644 --- a/packages/astro/src/core/logger/core.ts +++ b/packages/astro/src/core/logger/core.ts @@ -25,6 +25,7 @@ export type LoggerLabel = | 'vite' | 'watch' | 'middleware' + | 'preferences' // SKIP_FORMAT: A special label that tells the logger not to apply any formatting. // Useful for messages that are already formatted, like the server start message. | 'SKIP_FORMAT'; From de46c58d2ce3808bb9d7a9b5d3e2d290cbb0593d Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 27 Nov 2023 16:54:37 -0600 Subject: [PATCH 13/19] fix: missing import --- packages/astro/src/core/messages.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts index 10336ab7a0c4..417c3daa33b5 100644 --- a/packages/astro/src/core/messages.ts +++ b/packages/astro/src/core/messages.ts @@ -3,6 +3,7 @@ import { bgRed, bgWhite, bgYellow, + bgCyan, black, blue, bold, From d7d7862d50e11d255beceea8680ea0fbe8e2cb42 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Wed, 29 Nov 2023 09:47:17 -0600 Subject: [PATCH 14/19] chore: update lockfile --- pnpm-lock.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ede4937053a3..78b5b87dd369 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -550,6 +550,12 @@ importers: diff: specifier: ^5.1.0 version: 5.1.0 + dlv: + specifier: ^1.1.3 + version: 1.1.3 + dset: + specifier: ^3.1.3 + version: 3.1.3 es-module-lexer: specifier: ^1.4.1 version: 1.4.1 @@ -693,6 +699,9 @@ importers: '@types/diff': specifier: ^5.0.8 version: 5.0.8 + '@types/dlv': + specifier: ^1.1.4 + version: 1.1.4 '@types/dom-view-transitions': specifier: ^1.0.4 version: 1.0.4 From 7f8ce9d199b00c9cdded2a394d734439ba6b62fb Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Wed, 29 Nov 2023 11:43:58 -0600 Subject: [PATCH 15/19] chore: pretty print preferences --- packages/astro/package.json | 1 + packages/astro/src/cli/preferences/index.ts | 59 +++++++++++++++++++-- pnpm-lock.yaml | 8 +++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/packages/astro/package.json b/packages/astro/package.json index 140848faab10..ee0ac59bae8d 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -141,6 +141,7 @@ "estree-walker": "^3.0.3", "execa": "^8.0.1", "fast-glob": "^3.3.2", + "flattie": "^1.1.0", "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "html-escaper": "^3.0.3", diff --git a/packages/astro/src/cli/preferences/index.ts b/packages/astro/src/cli/preferences/index.ts index cd53ad3f80dc..a9f3788b8792 100644 --- a/packages/astro/src/cli/preferences/index.ts +++ b/packages/astro/src/cli/preferences/index.ts @@ -2,7 +2,7 @@ import type yargs from 'yargs-parser'; import type { AstroSettings } from '../../@types/astro.js'; -import { cyan } from 'kleur/colors'; +import { cyan, bold } from 'kleur/colors'; import { fileURLToPath } from 'node:url'; import * as msg from '../../core/messages.js'; @@ -12,7 +12,9 @@ import { createSettings } from '../../core/config/settings.js'; import { isValidKey, type PreferenceKey } from '../../preferences/index.js'; import { DEFAULT_PREFERENCES } from '../../preferences/defaults.js'; import dlv from 'dlv'; - +// @ts-expect-error flattie types are mispackaged +import { flattie } from 'flattie'; +import { format, formatWithOptions } from 'node:util'; interface PreferencesOptions { flags: yargs.Arguments; @@ -139,12 +141,61 @@ async function resetPreference(settings: AstroSettings, key: PreferenceKey, { lo return 1; } + async function listPreferences(settings: AstroSettings, { location, json }: SubcommandOptions) { const store = await settings.preferences.getAll({ location }); if (json) { console.log(JSON.stringify(store, null, 2)); + return; } - // TODO: pretty print - console.log(JSON.stringify(store, null, 2)); + const flattened = flattie(store); + const table = formatTable(flattened, ['Preference', 'Value']); + console.log(table); return 0; } + +const chars = { + h: '─', + hThick: '━', + hThickCross: '┿', + v: '│', + vRight: '├', + vRightThick: '┝', + vLeft: '┤', + vLeftThick: '┥', + hTop: '┴', + hBottom: '┬', + topLeft: '╭', + topRight: '╮', + bottomLeft: '╰', + bottomRight: '╯', +} + +function formatTable(object: Record, columnLabels: [string, string]) { + const [colA, colB] = columnLabels; + const colALength = [colA, ...Object.keys(object)].reduce(longest, 0) + 3; + const colBLength = [colB, ...Object.values(object)].reduce(longest, 0) + 3; + function formatRow(i: number, a: string, b: string | number | boolean, style: (value: string | number | boolean) => string = (v) => v.toString()): string { + return `${chars.v} ${style(a)} ${space(colALength - a.length - 2)} ${chars.v} ${style(b)} ${space(colBLength - b.toString().length - 3)} ${chars.v}` + } + const top = `${chars.topLeft}${chars.h.repeat(colALength + 1)}${chars.hBottom}${chars.h.repeat(colBLength)}${chars.topRight}` + const bottom = `${chars.bottomLeft}${chars.h.repeat(colALength + 1)}${chars.hTop}${chars.h.repeat(colBLength)}${chars.bottomRight}` + const divider = `${chars.vRightThick}${chars.hThick.repeat(colALength + 1)}${chars.hThickCross}${chars.hThick.repeat(colBLength)}${chars.vLeftThick}` + const rows: string[] = [top, formatRow(-1, colA, colB, bold), divider]; + let i = 0; + for (const [key, value] of Object.entries(object)) { + rows.push(formatRow(i, key, value, (v) => formatWithOptions({ colors: true }, v))); + i++; + } + rows.push(bottom); + return rows.join('\n'); +} + +function space(len: number) { + return ' '.repeat(len); +} + +const longest = (a: number, b: string | number | boolean) => { + const { length: len } = b.toString(); + return a > len ? a : len; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78b5b87dd369..214bf4b33f17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -571,6 +571,9 @@ importers: fast-glob: specifier: ^3.3.2 version: 3.3.2 + flattie: + specifier: ^1.1.0 + version: 1.1.0 github-slugger: specifier: ^2.0.0 version: 2.0.0 @@ -12226,6 +12229,11 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true + /flattie@1.1.0: + resolution: {integrity: sha512-xU99gDEnciIwJdGcBmNHnzTJ/w5AT+VFJOu6sTB6WM8diOYNA3Sa+K1DiEBQ7XH4QikQq3iFW1U+jRVcotQnBw==} + engines: {node: '>=8'} + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: From 8a92500189ba240ade1975049936016030c988c1 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Wed, 29 Nov 2023 11:48:19 -0600 Subject: [PATCH 16/19] chore: appease typescript --- packages/astro/src/cli/preferences/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/cli/preferences/index.ts b/packages/astro/src/cli/preferences/index.ts index a9f3788b8792..e86013b67d82 100644 --- a/packages/astro/src/cli/preferences/index.ts +++ b/packages/astro/src/cli/preferences/index.ts @@ -14,7 +14,7 @@ import { DEFAULT_PREFERENCES } from '../../preferences/defaults.js'; import dlv from 'dlv'; // @ts-expect-error flattie types are mispackaged import { flattie } from 'flattie'; -import { format, formatWithOptions } from 'node:util'; +import { formatWithOptions } from 'node:util'; interface PreferencesOptions { flags: yargs.Arguments; @@ -146,7 +146,7 @@ async function listPreferences(settings: AstroSettings, { location, json }: Subc const store = await settings.preferences.getAll({ location }); if (json) { console.log(JSON.stringify(store, null, 2)); - return; + return 0; } const flattened = flattie(store); const table = formatTable(flattened, ['Preference', 'Value']); From d09ac0f86c5542ee8bd3179777ab776178fd5f06 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Wed, 29 Nov 2023 11:49:50 -0600 Subject: [PATCH 17/19] fix: reset typo --- packages/astro/src/core/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts index 417c3daa33b5..c2f145d4512e 100644 --- a/packages/astro/src/core/messages.ts +++ b/packages/astro/src/core/messages.ts @@ -132,7 +132,7 @@ export function preferenceDisabled(name: string) { } export function preferenceReset(name: string) { - return `${cyan('◆')} ${name} has been reset ${bgCyan(black(' reset '))}\n`; + return `${cyan('◆')} ${name} has been ${bgCyan(black(' reset '))}\n`; } export function telemetryDisabled() { From 9f74d112a116240f709dc05ab357affa13013b23 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Wed, 29 Nov 2023 12:24:07 -0600 Subject: [PATCH 18/19] chore(preferences): finalize TODOs --- packages/astro/src/cli/preferences/index.ts | 66 ++++++++++++++------- packages/astro/src/core/messages.ts | 4 ++ packages/astro/src/preferences/index.ts | 4 +- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/packages/astro/src/cli/preferences/index.ts b/packages/astro/src/cli/preferences/index.ts index e86013b67d82..5e1161015287 100644 --- a/packages/astro/src/cli/preferences/index.ts +++ b/packages/astro/src/cli/preferences/index.ts @@ -2,19 +2,20 @@ import type yargs from 'yargs-parser'; import type { AstroSettings } from '../../@types/astro.js'; -import { cyan, bold } from 'kleur/colors'; +import { bold } from 'kleur/colors'; import { fileURLToPath } from 'node:url'; import * as msg from '../../core/messages.js'; import { createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js'; import { resolveConfig } from '../../core/config/config.js'; import { createSettings } from '../../core/config/settings.js'; -import { isValidKey, type PreferenceKey } from '../../preferences/index.js'; +import { coerce, isValidKey, type PreferenceKey } from '../../preferences/index.js'; import { DEFAULT_PREFERENCES } from '../../preferences/defaults.js'; import dlv from 'dlv'; // @ts-expect-error flattie types are mispackaged import { flattie } from 'flattie'; import { formatWithOptions } from 'node:util'; +import { collectErrorMetadata } from '../../core/errors/dev/utils.js'; interface PreferencesOptions { flags: yargs.Arguments; @@ -31,16 +32,21 @@ export async function preferences(subcommand: string, key: string, value: string if (!isValidSubcommand(subcommand) || flags?.help || flags?.h) { msg.printHelp({ commandName: 'astro preferences', - usage: 'set [key] [:value]', + usage: '[command]', tables: { + Commands: [ + ['list', 'Pretty print all current preferences'], + ['list --json', 'Log all current preferences as a JSON object'], + ['get [key]', 'Log current preference value'], + ['set [key] [value]', 'Update preference value'], + ['reset [key]', 'Reset preference value to default'], + ['enable [key]', 'Set a boolean preference to true'], + ['disable [key]', 'Set a boolean preference to false'], + ], Flags: [ - ['--global', 'Change setting value globally.'], - ['--help (-h)', 'See all available flags.'], + ['--global', 'Scope command to global preferences (all Astro projects) rather than the current project'], ], }, - description: `Starts a local server to serve your static dist/ directory. Check ${cyan( - 'https://docs.astro.build/en/reference/cli-reference/#astro-preview' - )} for more information.`, }); return 0; } @@ -69,8 +75,7 @@ export async function preferences(subcommand: string, key: string, value: string if (subcommand === 'set' && value === undefined) { const type = typeof dlv(DEFAULT_PREFERENCES, key); - // TODO: better error message - logger.error('preferences', `Please provide a ${type} value for "${key}"\n`); + console.error(msg.formatErrorMessage(collectErrorMetadata(new Error(`Please provide a ${type} value for "${key}"`)))); return 1; } @@ -92,14 +97,21 @@ interface SubcommandOptions { // Default `location` to "project" to avoid reading default preferencesa async function getPreference(settings: AstroSettings, key: PreferenceKey, { location = 'project' }: SubcommandOptions) { try { - const value = await settings.preferences.get(key, { location }); - // TODO: guard against printing objects - if (value !== undefined) { - console.log(msg.preferenceGet(key, value)); - } else { + let value = await settings.preferences.get(key, { location }); + if (value && typeof value === 'object' && !Array.isArray(value)) { + if (Object.keys(value).length === 0) { + value = dlv(DEFAULT_PREFERENCES, key); + console.log(msg.preferenceDefaultIntro(key)); + } + prettyPrint({ [key]: value }); + return 0; + } + if (value === undefined) { const defaultValue = await settings.preferences.get(key); console.log(msg.preferenceDefault(key, defaultValue)); + return 0; } + console.log(msg.preferenceGet(key, value)); return 0; } catch {} return 1; @@ -107,11 +119,21 @@ async function getPreference(settings: AstroSettings, key: PreferenceKey, { loca async function setPreference(settings: AstroSettings, key: PreferenceKey, value: unknown, { location }: SubcommandOptions) { try { - await settings.preferences.set(key, value as any, { location }); + const defaultType = typeof dlv(DEFAULT_PREFERENCES, key); + if (typeof coerce(key, value) !== defaultType) { + throw new Error(`${key} expects a "${defaultType}" value!`) + } + + await settings.preferences.set(key, coerce(key, value), { location }); console.log(msg.preferenceSet(key, value)) return 0; - } catch {} - return 1; + } catch (e) { + if (e instanceof Error) { + console.error(msg.formatErrorMessage(collectErrorMetadata(e))); + return 1; + } + throw e; + } } async function enablePreference(settings: AstroSettings, key: PreferenceKey, { location }: SubcommandOptions) { @@ -148,10 +170,14 @@ async function listPreferences(settings: AstroSettings, { location, json }: Subc console.log(JSON.stringify(store, null, 2)); return 0; } - const flattened = flattie(store); + prettyPrint(store); + return 0; +} + +function prettyPrint(value: Record) { + const flattened = flattie(value); const table = formatTable(flattened, ['Preference', 'Value']); console.log(table); - return 0; } const chars = { diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts index c2f145d4512e..935ed1bdc455 100644 --- a/packages/astro/src/core/messages.ts +++ b/packages/astro/src/core/messages.ts @@ -123,6 +123,10 @@ export function preferenceGet(name: string, value: any) { return `${green('◉')} ${name} is set to ${bgGreen(black(` ${JSON.stringify(value)} `))}\n`; } +export function preferenceDefaultIntro(name: string) { + return `${yellow('◯')} ${name} has not been set. It defaults to\n`; +} + export function preferenceDefault(name: string, value: any) { return `${yellow('◯')} ${name} has not been set. It defaults to ${bgYellow(black(` ${JSON.stringify(value)} `))}\n`; } diff --git a/packages/astro/src/preferences/index.ts b/packages/astro/src/preferences/index.ts index 90612bfb3773..86faf1cc8552 100644 --- a/packages/astro/src/preferences/index.ts +++ b/packages/astro/src/preferences/index.ts @@ -33,7 +33,7 @@ export interface AstroPreferences { export function isValidKey(key: string): key is PreferenceKey { return dget(DEFAULT_PREFERENCES, key) !== undefined; } -export function coerce(key: string, value: string | number) { +export function coerce(key: string, value: unknown) { const type = typeof dget(DEFAULT_PREFERENCES, key); switch (type) { case 'string': return value; @@ -43,7 +43,7 @@ export function coerce(key: string, value: string | number) { if (value === 'false' || value === 0) return false; } } - throw new Error(`Unable to convert "${value}" to a ${type} for "${key}".`) + return value as any; } export default function createPreferences(config: AstroConfig): AstroPreferences { From fb9e0463643a69303b8c48202249f70694416448 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Wed, 29 Nov 2023 12:29:59 -0600 Subject: [PATCH 19/19] fix: account for new logging --- packages/astro/src/cli/preferences/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/cli/preferences/index.ts b/packages/astro/src/cli/preferences/index.ts index 5e1161015287..2a01bbae2cfb 100644 --- a/packages/astro/src/cli/preferences/index.ts +++ b/packages/astro/src/cli/preferences/index.ts @@ -75,7 +75,7 @@ export async function preferences(subcommand: string, key: string, value: string if (subcommand === 'set' && value === undefined) { const type = typeof dlv(DEFAULT_PREFERENCES, key); - console.error(msg.formatErrorMessage(collectErrorMetadata(new Error(`Please provide a ${type} value for "${key}"`)))); + console.error(msg.formatErrorMessage(collectErrorMetadata(new Error(`Please provide a ${type} value for "${key}"`)), true)); return 1; } @@ -129,7 +129,7 @@ async function setPreference(settings: AstroSettings, key: PreferenceKey, value: return 0; } catch (e) { if (e instanceof Error) { - console.error(msg.formatErrorMessage(collectErrorMetadata(e))); + console.error(msg.formatErrorMessage(collectErrorMetadata(e), true)); return 1; } throw e;