mirror of
https://github.com/withastro/astro.git
synced 2024-12-30 22:03:56 -05:00
Add new preferences module (#9115)
This commit is contained in:
parent
34e96b141a
commit
3b77889b47
15 changed files with 507 additions and 4 deletions
17
.changeset/gentle-cobras-wash.md
Normal file
17
.changeset/gentle-cobras-wash.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
'astro': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
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 preferences disable devOverlay
|
||||||
|
# Disable the dev overlay for the current user in all Astro projects on this machine
|
||||||
|
npm run astro preferences --global disable devOverlay
|
||||||
|
|
||||||
|
# Check if the dev overlay is enabled for the current user
|
||||||
|
npm run astro preferences list devOverlay
|
||||||
|
```
|
|
@ -134,11 +134,14 @@
|
||||||
"deterministic-object-hash": "^2.0.1",
|
"deterministic-object-hash": "^2.0.1",
|
||||||
"devalue": "^4.3.2",
|
"devalue": "^4.3.2",
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
|
"dlv": "^1.1.3",
|
||||||
|
"dset": "^3.1.3",
|
||||||
"es-module-lexer": "^1.4.1",
|
"es-module-lexer": "^1.4.1",
|
||||||
"esbuild": "^0.19.6",
|
"esbuild": "^0.19.6",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"execa": "^8.0.1",
|
"execa": "^8.0.1",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
|
"flattie": "^1.1.0",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"html-escaper": "^3.0.3",
|
"html-escaper": "^3.0.3",
|
||||||
|
@ -185,6 +188,7 @@
|
||||||
"@types/cookie": "^0.5.4",
|
"@types/cookie": "^0.5.4",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/diff": "^5.0.8",
|
"@types/diff": "^5.0.8",
|
||||||
|
"@types/dlv": "^1.1.4",
|
||||||
"@types/dom-view-transitions": "^1.0.4",
|
"@types/dom-view-transitions": "^1.0.4",
|
||||||
"@types/estree": "^1.0.5",
|
"@types/estree": "^1.0.5",
|
||||||
"@types/hast": "^3.0.3",
|
"@types/hast": "^3.0.3",
|
||||||
|
|
|
@ -35,6 +35,7 @@ import type {
|
||||||
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server/index.js';
|
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server/index.js';
|
||||||
import type { OmitIndexSignature, Simplify } from '../type-utils.js';
|
import type { OmitIndexSignature, Simplify } from '../type-utils.js';
|
||||||
import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
|
import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
|
||||||
|
import type { AstroPreferences } from '../preferences/index.js';
|
||||||
|
|
||||||
export { type AstroIntegrationLogger };
|
export { type AstroIntegrationLogger };
|
||||||
|
|
||||||
|
@ -1678,6 +1679,7 @@ export interface AstroAdapterFeatures {
|
||||||
export interface AstroSettings {
|
export interface AstroSettings {
|
||||||
config: AstroConfig;
|
config: AstroConfig;
|
||||||
adapter: AstroAdapter | undefined;
|
adapter: AstroAdapter | undefined;
|
||||||
|
preferences: AstroPreferences;
|
||||||
injectedRoutes: InjectedRoute[];
|
injectedRoutes: InjectedRoute[];
|
||||||
resolvedInjectedRoutes: ResolvedInjectedRoute[];
|
resolvedInjectedRoutes: ResolvedInjectedRoute[];
|
||||||
pageExtensions: string[];
|
pageExtensions: string[];
|
||||||
|
|
|
@ -14,6 +14,7 @@ type CLICommand =
|
||||||
| 'sync'
|
| 'sync'
|
||||||
| 'check'
|
| 'check'
|
||||||
| 'info'
|
| 'info'
|
||||||
|
| 'preferences'
|
||||||
| 'telemetry';
|
| 'telemetry';
|
||||||
|
|
||||||
/** Display --help flag */
|
/** Display --help flag */
|
||||||
|
@ -33,6 +34,7 @@ async function printAstroHelp() {
|
||||||
['info', 'List info about your current Astro setup.'],
|
['info', 'List info about your current Astro setup.'],
|
||||||
['preview', 'Preview your build locally.'],
|
['preview', 'Preview your build locally.'],
|
||||||
['sync', 'Generate content collection types.'],
|
['sync', 'Generate content collection types.'],
|
||||||
|
['preferences', 'Configure user preferences.'],
|
||||||
['telemetry', 'Configure telemetry settings.'],
|
['telemetry', 'Configure telemetry settings.'],
|
||||||
],
|
],
|
||||||
'Global Flags': [
|
'Global Flags': [
|
||||||
|
@ -64,6 +66,7 @@ function resolveCommand(flags: yargs.Arguments): CLICommand {
|
||||||
'add',
|
'add',
|
||||||
'sync',
|
'sync',
|
||||||
'telemetry',
|
'telemetry',
|
||||||
|
'preferences',
|
||||||
'dev',
|
'dev',
|
||||||
'build',
|
'build',
|
||||||
'preview',
|
'preview',
|
||||||
|
@ -114,6 +117,12 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
|
||||||
const exitCode = await sync({ flags });
|
const exitCode = await sync({ flags });
|
||||||
return process.exit(exitCode);
|
return process.exit(exitCode);
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In verbose/debug mode, we log the debug logs asap before any potential errors could appear
|
// 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 */
|
/** The primary CLI action */
|
||||||
export async function cli(args: string[]) {
|
export async function cli(args: string[]) {
|
||||||
const flags = yargs(args);
|
const flags = yargs(args, { boolean: ['global'], alias: { g: 'global' } });
|
||||||
const cmd = resolveCommand(flags);
|
const cmd = resolveCommand(flags);
|
||||||
try {
|
try {
|
||||||
await runCommand(cmd, flags);
|
await runCommand(cmd, flags);
|
||||||
|
|
227
packages/astro/src/cli/preferences/index.ts
Normal file
227
packages/astro/src/cli/preferences/index.ts
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
import type yargs from 'yargs-parser';
|
||||||
|
import type { AstroSettings } from '../../@types/astro.js';
|
||||||
|
|
||||||
|
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 { 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<number> {
|
||||||
|
if (!isValidSubcommand(subcommand) || flags?.help || flags?.h) {
|
||||||
|
msg.printHelp({
|
||||||
|
commandName: 'astro preferences',
|
||||||
|
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', 'Scope command to global preferences (all Astro projects) rather than the current project'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
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)) {
|
||||||
|
logger.error('preferences', `Unknown preference "${key}"\n`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}"`)), true));
|
||||||
|
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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setPreference(settings: AstroSettings, key: PreferenceKey, value: unknown, { location }: SubcommandOptions) {
|
||||||
|
try {
|
||||||
|
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 (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
console.error(msg.formatErrorMessage(collectErrorMetadata(e), true));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
prettyPrint(store);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettyPrint(value: Record<string, string | number | boolean>) {
|
||||||
|
const flattened = flattie(value);
|
||||||
|
const table = formatTable(flattened, ['Preference', 'Value']);
|
||||||
|
console.log(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chars = {
|
||||||
|
h: '─',
|
||||||
|
hThick: '━',
|
||||||
|
hThickCross: '┿',
|
||||||
|
v: '│',
|
||||||
|
vRight: '├',
|
||||||
|
vRightThick: '┝',
|
||||||
|
vLeft: '┤',
|
||||||
|
vLeftThick: '┥',
|
||||||
|
hTop: '┴',
|
||||||
|
hBottom: '┬',
|
||||||
|
topLeft: '╭',
|
||||||
|
topRight: '╮',
|
||||||
|
bottomLeft: '╰',
|
||||||
|
bottomRight: '╯',
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTable(object: Record<string, string | number | boolean>, 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;
|
||||||
|
};
|
|
@ -10,11 +10,14 @@ import { formatYAMLException, isYAMLException } from '../errors/utils.js';
|
||||||
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../constants.js';
|
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../constants.js';
|
||||||
import { AstroTimer } from './timer.js';
|
import { AstroTimer } from './timer.js';
|
||||||
import { loadTSConfig } from './tsconfig.js';
|
import { loadTSConfig } from './tsconfig.js';
|
||||||
|
import createPreferences from '../../preferences/index.js';
|
||||||
|
|
||||||
export function createBaseSettings(config: AstroConfig): AstroSettings {
|
export function createBaseSettings(config: AstroConfig): AstroSettings {
|
||||||
const { contentDir } = getContentPaths(config);
|
const { contentDir } = getContentPaths(config);
|
||||||
|
const preferences = createPreferences(config);
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
|
preferences,
|
||||||
tsConfig: undefined,
|
tsConfig: undefined,
|
||||||
tsConfigPath: undefined,
|
tsConfigPath: undefined,
|
||||||
adapter: undefined,
|
adapter: undefined,
|
||||||
|
|
|
@ -29,6 +29,9 @@ async function createRestartedContainer(
|
||||||
return newContainer;
|
return newContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const configRE = new RegExp(`.*astro\.config\.((mjs)|(cjs)|(js)|(ts))$`);
|
||||||
|
const preferencesRE = new RegExp(`.*\.astro\/settings\.json$`);
|
||||||
|
|
||||||
export function shouldRestartContainer(
|
export function shouldRestartContainer(
|
||||||
{ settings, inlineConfig, restartInFlight }: Container,
|
{ settings, inlineConfig, restartInFlight }: Container,
|
||||||
changedFile: string
|
changedFile: string
|
||||||
|
@ -43,9 +46,9 @@ export function shouldRestartContainer(
|
||||||
}
|
}
|
||||||
// Otherwise, watch for any astro.config.* file changes in project root
|
// Otherwise, watch for any astro.config.* file changes in project root
|
||||||
else {
|
else {
|
||||||
const exp = new RegExp(`.*astro\.config\.((mjs)|(cjs)|(js)|(ts))$`);
|
|
||||||
const normalizedChangedFile = vite.normalizePath(changedFile);
|
const normalizedChangedFile = vite.normalizePath(changedFile);
|
||||||
shouldRestart = exp.test(normalizedChangedFile);
|
shouldRestart = configRE.test(normalizedChangedFile) || preferencesRE.test(normalizedChangedFile);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldRestart && settings.watchFiles.length > 0) {
|
if (!shouldRestart && settings.watchFiles.length > 0) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ export type LoggerLabel =
|
||||||
| 'vite'
|
| 'vite'
|
||||||
| 'watch'
|
| 'watch'
|
||||||
| 'middleware'
|
| 'middleware'
|
||||||
|
| 'preferences'
|
||||||
// SKIP_FORMAT: A special label that tells the logger not to apply any formatting.
|
// 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.
|
// Useful for messages that are already formatted, like the server start message.
|
||||||
| 'SKIP_FORMAT';
|
| 'SKIP_FORMAT';
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
bgRed,
|
bgRed,
|
||||||
bgWhite,
|
bgWhite,
|
||||||
bgYellow,
|
bgYellow,
|
||||||
|
bgCyan,
|
||||||
black,
|
black,
|
||||||
blue,
|
blue,
|
||||||
bold,
|
bold,
|
||||||
|
@ -110,6 +111,34 @@ export function telemetryEnabled() {
|
||||||
].join('\n');
|
].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 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`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preferenceDisabled(name: string) {
|
||||||
|
return `${yellow('◯')} ${name} is now ${bgYellow(black(' disabled '))}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preferenceReset(name: string) {
|
||||||
|
return `${cyan('◆')} ${name} has been ${bgCyan(black(' reset '))}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
export function telemetryDisabled() {
|
export function telemetryDisabled() {
|
||||||
return [
|
return [
|
||||||
green('▶ Anonymous telemetry ') + bgGreen(' disabled '),
|
green('▶ Anonymous telemetry ') + bgGreen(' disabled '),
|
||||||
|
|
33
packages/astro/src/preferences/README.md
Normal file
33
packages/astro/src/preferences/README.md
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# 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 [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`
|
||||||
|
|
||||||
|
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 `<homedir>/<os-specific-preferences-dir>/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 `location: "global"` option.
|
||||||
|
|
||||||
|
```js
|
||||||
|
await preferences.set('dot.separated.value', 'value', { location: '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.
|
8
packages/astro/src/preferences/defaults.ts
Normal file
8
packages/astro/src/preferences/defaults.ts
Normal file
|
@ -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;
|
91
packages/astro/src/preferences/index.ts
Normal file
91
packages/astro/src/preferences/index.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
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> = T extends object ? { [K in keyof T]:
|
||||||
|
`${Exclude<K, symbol>}${DotKeys<T[K]> extends never ? "" : `.${DotKeys<T[K]>}`}`
|
||||||
|
}[keyof T] : never
|
||||||
|
|
||||||
|
export type GetDotKey<
|
||||||
|
T extends Record<string | number, any>,
|
||||||
|
K extends string
|
||||||
|
> = K extends `${infer U}.${infer Rest}` ? GetDotKey<T[U], Rest> : T[K]
|
||||||
|
|
||||||
|
export interface PreferenceOptions {
|
||||||
|
location?: 'global' | 'project';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PreferenceKey = DotKeys<Preferences>;
|
||||||
|
|
||||||
|
export interface AstroPreferences {
|
||||||
|
get<Key extends PreferenceKey>(key: Key, opts?: PreferenceOptions): Promise<GetDotKey<Preferences, Key>>;
|
||||||
|
set<Key extends PreferenceKey>(key: Key, value: GetDotKey<Preferences, Key>, opts?: PreferenceOptions): Promise<void>;
|
||||||
|
getAll(opts?: PreferenceOptions): Promise<Record<string, any>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidKey(key: string): key is PreferenceKey {
|
||||||
|
return dget(DEFAULT_PREFERENCES, key) !== undefined;
|
||||||
|
}
|
||||||
|
export function coerce(key: string, value: unknown) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
async get(key, { location } = {}) {
|
||||||
|
if (!location) return project.get(key) ?? global.get(key) ?? dget(DEFAULT_PREFERENCES, key);
|
||||||
|
return stores[location].get(key);
|
||||||
|
},
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
59
packages/astro/src/preferences/store.ts
Normal file
59
packages/astro/src/preferences/store.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import dget from 'dlv';
|
||||||
|
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<string, any>;
|
||||||
|
private get store(): Record<string, any> {
|
||||||
|
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<string, any>) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
getAll(): Record<string, any> {
|
||||||
|
return this.store;
|
||||||
|
}
|
||||||
|
}
|
|
@ -384,7 +384,7 @@ async function getScriptsAndStyles({ pipeline, filePath }: GetScriptsAndStylesPa
|
||||||
children: '',
|
children: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (settings.config.devOverlay.enabled) {
|
if (settings.config.devOverlay.enabled && await settings.preferences.get('devOverlay.enabled')) {
|
||||||
scripts.add({
|
scripts.add({
|
||||||
props: {
|
props: {
|
||||||
type: 'module',
|
type: 'module',
|
||||||
|
|
|
@ -550,6 +550,12 @@ importers:
|
||||||
diff:
|
diff:
|
||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 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:
|
es-module-lexer:
|
||||||
specifier: ^1.4.1
|
specifier: ^1.4.1
|
||||||
version: 1.4.1
|
version: 1.4.1
|
||||||
|
@ -565,6 +571,9 @@ importers:
|
||||||
fast-glob:
|
fast-glob:
|
||||||
specifier: ^3.3.2
|
specifier: ^3.3.2
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
|
flattie:
|
||||||
|
specifier: ^1.1.0
|
||||||
|
version: 1.1.0
|
||||||
github-slugger:
|
github-slugger:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
|
@ -693,6 +702,9 @@ importers:
|
||||||
'@types/diff':
|
'@types/diff':
|
||||||
specifier: ^5.0.8
|
specifier: ^5.0.8
|
||||||
version: 5.0.8
|
version: 5.0.8
|
||||||
|
'@types/dlv':
|
||||||
|
specifier: ^1.1.4
|
||||||
|
version: 1.1.4
|
||||||
'@types/dom-view-transitions':
|
'@types/dom-view-transitions':
|
||||||
specifier: ^1.0.4
|
specifier: ^1.0.4
|
||||||
version: 1.0.4
|
version: 1.0.4
|
||||||
|
@ -12217,6 +12229,11 @@ packages:
|
||||||
resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==}
|
resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/flattie@1.1.0:
|
||||||
|
resolution: {integrity: sha512-xU99gDEnciIwJdGcBmNHnzTJ/w5AT+VFJOu6sTB6WM8diOYNA3Sa+K1DiEBQ7XH4QikQq3iFW1U+jRVcotQnBw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/for-each@0.3.3:
|
/for-each@0.3.3:
|
||||||
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Loading…
Reference in a new issue