diff --git a/.changeset/sweet-vans-begin.md b/.changeset/sweet-vans-begin.md new file mode 100644 index 000000000..030fb8ba8 --- /dev/null +++ b/.changeset/sweet-vans-begin.md @@ -0,0 +1,6 @@ +--- +'astro': patch +'@astrojs/telemetry': patch +--- + +Update to telemetry to include AstroConfig keys used diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index ca43386da..473b3a6cc 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -17,7 +17,7 @@ import preview from '../core/preview/index.js'; import { check } from './check.js'; import { openInBrowser } from './open.js'; import * as telemetryHandler from './telemetry.js'; -import { loadConfig } from '../core/config.js'; +import { openConfig } from '../core/config.js'; import { printHelp, formatErrorMessage, formatConfigErrorMessage } from '../core/messages.js'; import { createSafeError } from '../core/util.js'; @@ -138,14 +138,16 @@ export async function cli(args: string[]) { } case 'dev': { try { - const config = await loadConfig({ cwd: root, flags, cmd }); + const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd }); + telemetry.record( event.eventCliSession( { astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'dev' }, - config + userConfig, + flags ) ); - await devServer(config, { logging, telemetry }); + await devServer(astroConfig, { logging, telemetry }); return await new Promise(() => {}); // lives forever } catch (err) { return throwAndExit(err); @@ -154,41 +156,44 @@ export async function cli(args: string[]) { case 'build': { try { - const config = await loadConfig({ cwd: root, flags, cmd }); + const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd }); telemetry.record( event.eventCliSession( { astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'build' }, - config + userConfig, + flags ) ); - return await build(config, { logging, telemetry }); + return await build(astroConfig, { logging, telemetry }); } catch (err) { return throwAndExit(err); } } case 'check': { - const config = await loadConfig({ cwd: root, flags, cmd }); + const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd }); telemetry.record( event.eventCliSession( { astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'check' }, - config + userConfig, + flags, ) ); - const ret = await check(config); + const ret = await check(astroConfig); return process.exit(ret); } case 'preview': { try { - const config = await loadConfig({ cwd: root, flags, cmd }); + const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd }); telemetry.record( event.eventCliSession( { astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'preview' }, - config + userConfig, + flags ) ); - const server = await preview(config, { logging, telemetry }); + const server = await preview(astroConfig, { logging, telemetry }); return await server.closed(); // keep alive until the server is closed } catch (err) { return throwAndExit(err); diff --git a/packages/astro/src/core/config.ts b/packages/astro/src/core/config.ts index 8b16ce5a0..918ed9d90 100644 --- a/packages/astro/src/core/config.ts +++ b/packages/astro/src/core/config.ts @@ -397,7 +397,59 @@ export async function resolveConfigURL( } } -/** Attempt to load an `astro.config.mjs` file */ +interface OpenConfigResult { + userConfig: AstroUserConfig; + astroConfig: AstroConfig; + flags: CLIFlags; + root: string; +} + +/** Load a configuration file, returning both the userConfig and astroConfig */ +export async function openConfig(configOptions: LoadConfigOptions): Promise { + const root = configOptions.cwd ? path.resolve(configOptions.cwd) : process.cwd(); + const flags = resolveFlags(configOptions.flags || {}); + let userConfig: AstroUserConfig = {}; + let userConfigPath: string | undefined; + + if (flags?.config) { + userConfigPath = /^\.*\//.test(flags.config) ? flags.config : `./${flags.config}`; + userConfigPath = fileURLToPath( + new URL(userConfigPath, appendForwardSlash(pathToFileURL(root).toString())) + ); + } + + // Automatically load config file using Proload + // If `userConfigPath` is `undefined`, Proload will search for `astro.config.[cm]?[jt]s` + let config; + try { + config = await load('astro', { + mustExist: !!userConfigPath, + cwd: root, + filePath: userConfigPath, + }); + } catch (err) { + if (err instanceof ProloadError && flags.config) { + throw new Error(`Unable to resolve --config "${flags.config}"! Does the file exist?`); + } + throw err; + } + if (config) { + userConfig = config.value; + } + const astroConfig = await resolveConfig(userConfig, root, flags, configOptions.cmd); + + return { + astroConfig, + userConfig, + flags, + root + }; +} + +/** + * Attempt to load an `astro.config.mjs` file + * @deprecated + */ export async function loadConfig(configOptions: LoadConfigOptions): Promise { const root = configOptions.cwd ? path.resolve(configOptions.cwd) : process.cwd(); const flags = resolveFlags(configOptions.flags || {}); diff --git a/packages/telemetry/src/events/session.ts b/packages/telemetry/src/events/session.ts index ee1daca89..abe7704a9 100644 --- a/packages/telemetry/src/events/session.ts +++ b/packages/telemetry/src/events/session.ts @@ -6,25 +6,34 @@ const require = createRequire(import.meta.url); const EVENT_SESSION = 'ASTRO_CLI_SESSION_STARTED'; +// :( We can't import the type because of TurboRepo circular dep limitation +type AstroUserConfig = Record; + interface EventCliSession { astroVersion: string; cliCommand: string; } interface ConfigInfo { - hasViteConfig: boolean; - hasBase: boolean; - viteKeys: string[]; markdownPlugins: string[]; adapter: string | null; integrations: string[]; - experimentalFeatures: string[]; + trailingSlash: undefined | 'always' | 'never' | 'ignore'; + build: undefined | { + format: undefined | 'file' | 'directory' + }; + markdown: undefined | { + mode: undefined | 'md' | 'mdx'; + syntaxHighlight: undefined | 'shiki' | 'prism' | false; + }; } interface EventCliSessionInternal extends EventCliSession { nodeVersion: string; viteVersion: string; config?: ConfigInfo; + configKeys?: string[]; + flags?: string[]; } function getViteVersion() { @@ -35,29 +44,24 @@ function getViteVersion() { return undefined; } -function getExperimentalFeatures(astroConfig?: Record): string[] | undefined { - if (!astroConfig) return undefined; - return Object.entries(astroConfig.experimental || []).reduce((acc, [key, value]) => { - if (value) { - acc.push(key); - } - return acc; - }, [] as string[]); -} - -const secondLevelViteKeys = new Set([ - 'resolve', - 'css', - 'json', - 'server', - 'server.fs', +const multiLevelKeys = new Set([ 'build', - 'preview', - 'optimizeDeps', - 'ssr', - 'worker', + 'markdown', + 'markdown.shikiConfig', + 'server', + 'vite', + 'vite.resolve', + 'vite.css', + 'vite.json', + 'vite.server', + 'vite.server.fs', + 'vite.build', + 'vite.preview', + 'vite.optimizeDeps', + 'vite.ssr', + 'vite.worker', ]); -function viteConfigKeys(obj: Record | undefined, parentKey: string): string[] { +function configKeys(obj: Record | undefined, parentKey: string): string[] { if (!obj) { return []; } @@ -66,8 +70,8 @@ function viteConfigKeys(obj: Record | undefined, parentKey: string) .map(([key, value]) => { if (typeof value === 'object' && !Array.isArray(value)) { const localKey = parentKey ? parentKey + '.' + key : key; - if (secondLevelViteKeys.has(localKey)) { - let keys = viteConfigKeys(value, localKey).map((subkey) => key + '.' + subkey); + if (multiLevelKeys.has(localKey)) { + let keys = configKeys(value, localKey).map((subkey) => key + '.' + subkey); keys.unshift(key); return keys; } @@ -80,29 +84,39 @@ function viteConfigKeys(obj: Record | undefined, parentKey: string) export function eventCliSession( event: EventCliSession, - astroConfig?: Record + userConfig?: AstroUserConfig, + flags?: Record, ): { eventName: string; payload: EventCliSessionInternal }[] { + const configValues = userConfig ? { + markdownPlugins: [ + userConfig?.markdown?.remarkPlugins ?? [], + userConfig?.markdown?.rehypePlugins ?? [], + ].flat(1), + adapter: userConfig?.adapter?.name ?? null, + integrations: userConfig?.integrations?.map((i: any) => i.name) ?? [], + trailingSlash: userConfig?.trailingSlash, + build: userConfig?.build ? { + format: userConfig?.build?.format + } : undefined, + markdown: userConfig?.markdown ? { + mode: userConfig?.markdown?.mode, + syntaxHighlight: userConfig.markdown?.syntaxHighlight + } : undefined, + } : undefined; + + // Filter out yargs default `_` flag which is the cli command + const cliFlags = flags ? Object.keys(flags).filter(name => name != '_'): undefined; + const payload: EventCliSessionInternal = { cliCommand: event.cliCommand, // Versions astroVersion: event.astroVersion, viteVersion: getViteVersion(), nodeVersion: process.version.replace(/^v?/, ''), + configKeys: userConfig ? configKeys(userConfig, '') : undefined, // Config Values - config: astroConfig - ? { - hasViteConfig: Object.keys(astroConfig?.vite).length > 0, - markdownPlugins: [ - astroConfig?.markdown?.remarkPlugins ?? [], - astroConfig?.markdown?.rehypePlugins ?? [], - ].flat(1), - hasBase: astroConfig?.base !== '/', - viteKeys: viteConfigKeys(astroConfig?.vite, ''), - adapter: astroConfig?.adapter?.name ?? null, - integrations: astroConfig?.integrations?.map((i: any) => i.name) ?? [], - experimentalFeatures: getExperimentalFeatures(astroConfig) ?? [], - } - : undefined, + config: configValues, + flags: cliFlags, }; return [{ eventName: EVENT_SESSION, payload }]; } diff --git a/packages/telemetry/test/session-event.test.js b/packages/telemetry/test/session-event.test.js index 354ed2878..c3196787f 100644 --- a/packages/telemetry/test/session-event.test.js +++ b/packages/telemetry/test/session-event.test.js @@ -1,241 +1,423 @@ import { expect } from 'chai'; import * as events from '../dist/events/index.js'; -import { resolveConfig } from '../../astro/dist/core/config.js'; - -async function mockConfig(userConfig) { - return await resolveConfig(userConfig, import.meta.url, {}, 'dev'); -} describe('Session event', () => { - it('top-level keys are captured', async () => { - const config = await mockConfig({ - vite: { - css: { modules: [] }, - base: 'a', - mode: 'b', - define: { - a: 'b', + describe('top-level', () => { + it('All top-level keys added', () => { + const config = { + root: 1, + srcDir: 2, + publicDir: 3, + outDir: 4, + site: 5, + base: 6, + trailingSlash: 7, + experimental: 8, + }; + const expected = Object.keys(config); + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0', }, - publicDir: 'some/dir', - }, + config + ); + expect(payload.configKeys).to.deep.equal(expected); }); - - const [{ payload }] = events.eventCliSession( - { - cliCommand: 'dev', - astroVersion: '0.0.0', - }, - config - ); - expect(payload.config.viteKeys).is.deep.equal([ - 'css', - 'css.modules', - 'base', - 'mode', - 'define', - 'publicDir', - ]); }); - it('vite.resolve keys are captured', async () => { - const config = await mockConfig({ - vite: { - resolve: { - alias: { - a: 'b', - }, - dedupe: ['one', 'two'], + describe('config.build', () => { + it('configKeys includes format', () => { + const config = { + srcDir: 1, + build: { + format: 'file', + } + }; + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0' }, - }, + config + ); + expect(payload.configKeys).to.deep.equal(['srcDir', 'build', 'build.format']); }); - const [{ payload }] = events.eventCliSession( - { - cliCommand: 'dev', - astroVersion: '0.0.0', - }, - config - ); - expect(payload.config.viteKeys).is.deep.equal(['resolve', 'resolve.alias', 'resolve.dedupe']); - }); - - it('vite.css keys are captured', async () => { - const config = await mockConfig({ - vite: { - resolve: { - dedupe: ['one', 'two'], + it('config.build.format', () => { + const config = { + srcDir: 1, + build: { + format: 'file', + } + }; + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0' }, - css: { - modules: [], - postcss: {}, - }, - }, + config + ); + expect(payload.config.build.format).to.equal('file'); }); - - const [{ payload }] = events.eventCliSession( - { - cliCommand: 'dev', - astroVersion: '0.0.0', - }, - config - ); - expect(payload.config.viteKeys).is.deep.equal([ - 'resolve', - 'resolve.dedupe', - 'css', - 'css.modules', - 'css.postcss', - ]); }); - it('vite.server keys are captured', async () => { - const config = await mockConfig({ - vite: { + describe('config.server', () => { + it('configKeys includes server props', () => { + const config = { + srcDir: 1, server: { host: 'example.com', - open: true, - fs: { - strict: true, - allow: ['a', 'b'], - }, + port: 8033 + } + }; + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0' }, - }, + config + ); + expect(payload.configKeys).to.deep.equal(['srcDir', 'server', 'server.host', 'server.port']); }); - - const [{ payload }] = events.eventCliSession( - { - cliCommand: 'dev', - astroVersion: '0.0.0', - }, - config - ); - expect(payload.config.viteKeys).is.deep.equal([ - 'server', - 'server.host', - 'server.open', - 'server.fs', - 'server.fs.strict', - 'server.fs.allow', - ]); }); - it('vite.build keys are captured', async () => { - const config = await mockConfig({ - vite: { - build: { - target: 'one', - outDir: 'some/dir', - cssTarget: { - one: 'two', + describe('config.markdown', () => { + it('configKeys is deep', () => { + const config = { + publicDir: 1, + markdown: { + drafts: true, + mode: 'mdx', + shikiConfig: { + lang: 1, + theme: 2, + wrap: 3 }, + syntaxHighlight: 'shiki', + remarkPlugins: [], + rehypePlugins: [], + } + }; + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0' }, - }, + config + ); + expect(payload.configKeys).to.deep.equal(['publicDir', 'markdown', + 'markdown.drafts', 'markdown.mode', 'markdown.shikiConfig', + 'markdown.shikiConfig.lang', 'markdown.shikiConfig.theme', 'markdown.shikiConfig.wrap', + 'markdown.syntaxHighlight', 'markdown.remarkPlugins', 'markdown.rehypePlugins']); }); - const [{ payload }] = events.eventCliSession( - { - cliCommand: 'dev', - astroVersion: '0.0.0', - }, - config - ); - expect(payload.config.viteKeys).is.deep.equal([ - 'build', - 'build.target', - 'build.outDir', - 'build.cssTarget', - ]); + it('mode', () => { + const config = { + markdown: { + mode: 'mdx', + } + }; + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0' + }, + config + ); + expect(payload.config.markdown.mode).to.equal('mdx'); + }); + + it('syntaxHighlight', () => { + const config = { + markdown: { + syntaxHighlight: 'shiki', + } + }; + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0' + }, + config + ); + expect(payload.config.markdown.syntaxHighlight).to.equal('shiki'); + }); }); - it('vite.preview keys are captured', async () => { - const config = await mockConfig({ - vite: { - preview: { - host: 'example.com', - port: 8080, - another: { + describe('config.vite', () => { + it('top-level keys are captured', async () => { + const config = { + root: 'some/thing', + vite: { + css: { modules: [] }, + base: 'a', + mode: 'b', + define: { a: 'b', }, + publicDir: 'some/dir', }, - }, + }; + + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0', + }, + config + ); + expect(payload.configKeys).is.deep.equal([ + 'root', + 'vite', + 'vite.css', + 'vite.css.modules', + 'vite.base', + 'vite.mode', + 'vite.define', + 'vite.publicDir', + ]); + }); + + it('vite.resolve keys are captured', async () => { + const config = { + vite: { + resolve: { + alias: { + a: 'b', + }, + dedupe: ['one', 'two'], + }, + }, + }; + + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0', + }, + config + ); + expect(payload.configKeys).is.deep.equal(['vite', 'vite.resolve', 'vite.resolve.alias', 'vite.resolve.dedupe']); + }); + + it('vite.css keys are captured', async () => { + const config = { + vite: { + resolve: { + dedupe: ['one', 'two'], + }, + css: { + modules: [], + postcss: {}, + }, + }, + }; + + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0', + }, + config + ); + expect(payload.configKeys).is.deep.equal([ + 'vite', + 'vite.resolve', + 'vite.resolve.dedupe', + 'vite.css', + 'vite.css.modules', + 'vite.css.postcss', + ]); + }); + + it('vite.server keys are captured', async () => { + const config = { + vite: { + server: { + host: 'example.com', + open: true, + fs: { + strict: true, + allow: ['a', 'b'], + }, + }, + }, + }; + + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0', + }, + config + ); + expect(payload.configKeys).is.deep.equal([ + 'vite', + 'vite.server', + 'vite.server.host', + 'vite.server.open', + 'vite.server.fs', + 'vite.server.fs.strict', + 'vite.server.fs.allow', + ]); + }); + + it('vite.build keys are captured', async () => { + const config = { + vite: { + build: { + target: 'one', + outDir: 'some/dir', + cssTarget: { + one: 'two', + }, + }, + }, + }; + + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0', + }, + config + ); + expect(payload.configKeys).is.deep.equal([ + 'vite', + 'vite.build', + 'vite.build.target', + 'vite.build.outDir', + 'vite.build.cssTarget', + ]); + }); + + it('vite.preview keys are captured', async () => { + const config = { + vite: { + preview: { + host: 'example.com', + port: 8080, + another: { + a: 'b', + }, + }, + }, + }; + + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0', + }, + config + ); + expect(payload.configKeys).is.deep.equal([ + 'vite', + 'vite.preview', + 'vite.preview.host', + 'vite.preview.port', + 'vite.preview.another', + ]); + }); + + it('vite.optimizeDeps keys are captured', async () => { + const config = { + vite: { + optimizeDeps: { + entries: ['one', 'two'], + exclude: ['secret', 'name'], + }, + }, + }; + + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0', + }, + config + ); + expect(payload.configKeys).is.deep.equal([ + 'vite', + 'vite.optimizeDeps', + 'vite.optimizeDeps.entries', + 'vite.optimizeDeps.exclude', + ]); + }); + + it('vite.ssr keys are captured', async () => { + const config = { + vite: { + ssr: { + external: ['a'], + target: { one: 'two' }, + }, + }, + }; + + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0', + }, + config + ); + expect(payload.configKeys).is.deep.equal(['vite', 'vite.ssr', 'vite.ssr.external', 'vite.ssr.target']); + }); + + it('vite.worker keys are captured', async () => { + const config = { + vite: { + worker: { + format: { a: 'b' }, + plugins: ['a', 'b'], + }, + }, + }; + + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0', + }, + config + ); + expect(payload.configKeys).is.deep.equal(['vite', 'vite.worker', 'vite.worker.format', 'vite.worker.plugins']); }); - - const [{ payload }] = events.eventCliSession( - { - cliCommand: 'dev', - astroVersion: '0.0.0', - }, - config - ); - expect(payload.config.viteKeys).is.deep.equal([ - 'preview', - 'preview.host', - 'preview.port', - 'preview.another', - ]); }); - it('vite.optimizeDeps keys are captured', async () => { - const config = await mockConfig({ - vite: { - optimizeDeps: { - entries: ['one', 'two'], - exclude: ['secret', 'name'], + describe('flags', () => { + it('includes cli flags in payload', () => { + const config = {}; + const flags = { + root: 'root', + site: 'http://example.com', + host: true, + port: 8080, + config: 'path/to/config.mjs', + experimentalSsr: true, + experimentalIntegrations: true, + drafts: true, + } + const [{ payload }] = events.eventCliSession( + { + cliCommand: 'dev', + astroVersion: '0.0.0', }, - }, - }); - - const [{ payload }] = events.eventCliSession( - { - cliCommand: 'dev', - astroVersion: '0.0.0', - }, - config - ); - expect(payload.config.viteKeys).is.deep.equal([ - 'optimizeDeps', - 'optimizeDeps.entries', - 'optimizeDeps.exclude', - ]); - }); - - it('vite.ssr keys are captured', async () => { - const config = await mockConfig({ - vite: { - ssr: { - external: ['a'], - target: { one: 'two' }, - }, - }, - }); - - const [{ payload }] = events.eventCliSession( - { - cliCommand: 'dev', - astroVersion: '0.0.0', - }, - config - ); - expect(payload.config.viteKeys).is.deep.equal(['ssr', 'ssr.external', 'ssr.target']); - }); - - it('vite.worker keys are captured', async () => { - const config = await mockConfig({ - vite: { - worker: { - format: { a: 'b' }, - plugins: ['a', 'b'], - }, - }, - }); - - const [{ payload }] = events.eventCliSession( - { - cliCommand: 'dev', - astroVersion: '0.0.0', - }, - config - ); - expect(payload.config.viteKeys).is.deep.equal(['worker', 'worker.format', 'worker.plugins']); - }); + config, + flags + ); + expect(payload.flags).to.deep.equal([ + 'root', + 'site', + 'host', + 'port', + 'config', + 'experimentalSsr', + 'experimentalIntegrations', + 'drafts' + ]); + }) + }) });