summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/gentle-cobras-wash.md17
-rw-r--r--packages/astro/package.json4
-rw-r--r--packages/astro/src/@types/astro.ts2
-rw-r--r--packages/astro/src/cli/index.ts11
-rw-r--r--packages/astro/src/cli/preferences/index.ts227
-rw-r--r--packages/astro/src/core/config/settings.ts3
-rw-r--r--packages/astro/src/core/dev/restart.ts7
-rw-r--r--packages/astro/src/core/logger/core.ts1
-rw-r--r--packages/astro/src/core/messages.ts29
-rw-r--r--packages/astro/src/preferences/README.md33
-rw-r--r--packages/astro/src/preferences/defaults.ts8
-rw-r--r--packages/astro/src/preferences/index.ts91
-rw-r--r--packages/astro/src/preferences/store.ts59
-rw-r--r--packages/astro/src/vite-plugin-astro-server/route.ts2
-rw-r--r--pnpm-lock.yaml17
15 files changed, 507 insertions, 4 deletions
diff --git a/.changeset/gentle-cobras-wash.md b/.changeset/gentle-cobras-wash.md
new file mode 100644
index 000000000..1a9245524
--- /dev/null
+++ b/.changeset/gentle-cobras-wash.md
@@ -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
+```
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 6b076fc7d..ee0ac59ba 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -134,11 +134,14 @@
"deterministic-object-hash": "^2.0.1",
"devalue": "^4.3.2",
"diff": "^5.1.0",
+ "dlv": "^1.1.3",
+ "dset": "^3.1.3",
"es-module-lexer": "^1.4.1",
"esbuild": "^0.19.6",
"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",
@@ -185,6 +188,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/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 24b7b195f..0acea76e4 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/cli/index.ts b/packages/astro/src/cli/index.ts
index 0421258a5..83dd960c6 100644
--- a/packages/astro/src/cli/index.ts
+++ b/packages/astro/src/cli/index.ts
@@ -14,6 +14,7 @@ type CLICommand =
| 'sync'
| 'check'
| 'info'
+ | 'preferences'
| '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.'],
+ ['preferences', 'Configure user preferences.'],
['telemetry', 'Configure telemetry settings.'],
],
'Global Flags': [
@@ -64,6 +66,7 @@ function resolveCommand(flags: yargs.Arguments): CLICommand {
'add',
'sync',
'telemetry',
+ 'preferences',
'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 '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
@@ -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/cli/preferences/index.ts b/packages/astro/src/cli/preferences/index.ts
new file mode 100644
index 000000000..2a01bbae2
--- /dev/null
+++ b/packages/astro/src/cli/preferences/index.ts
@@ -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;
+};
diff --git a/packages/astro/src/core/config/settings.ts b/packages/astro/src/core/config/settings.ts
index fca392c97..29df00eaf 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,
diff --git a/packages/astro/src/core/dev/restart.ts b/packages/astro/src/core/dev/restart.ts
index 16c1b1b2c..49cd185d8 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) {
diff --git a/packages/astro/src/core/logger/core.ts b/packages/astro/src/core/logger/core.ts
index 2c26a55a0..5dab12213 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';
diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts
index 1c3790962..935ed1bdc 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,
@@ -110,6 +111,34 @@ 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 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() {
return [
green('▶ Anonymous telemetry ') + bgGreen(' disabled '),
diff --git a/packages/astro/src/preferences/README.md b/packages/astro/src/preferences/README.md
new file mode 100644
index 000000000..4234ebac1
--- /dev/null
+++ b/packages/astro/src/preferences/README.md
@@ -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.
diff --git a/packages/astro/src/preferences/defaults.ts b/packages/astro/src/preferences/defaults.ts
new file mode 100644
index 000000000..74ae7b2dc
--- /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 000000000..86faf1cc8
--- /dev/null
+++ b/packages/astro/src/preferences/index.ts
@@ -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();
+ }
+}
diff --git a/packages/astro/src/preferences/store.ts b/packages/astro/src/preferences/store.ts
new file mode 100644
index 000000000..4dabbba1c
--- /dev/null
+++ b/packages/astro/src/preferences/store.ts
@@ -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;
+ }
+}
diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts
index a58f38a02..a8f9efa04 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 && await settings.preferences.get('devOverlay.enabled')) {
scripts.add({
props: {
type: 'module',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ede493705..214bf4b33 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
@@ -565,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
@@ -693,6 +702,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
@@ -12217,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: