diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index f95e028f64dd8e7..2548161d4d48f3c 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -50,8 +50,8 @@ import { normalizePagePath } from '../next-server/server/normalize-page-path' import { eventBuildCompleted, eventBuildOptimize, + eventCliSession, eventNextPlugins, - eventVersion, } from '../telemetry/events' import { Telemetry } from '../telemetry/storage' import { CompilerResult, runCompiler } from './compiler' @@ -161,7 +161,7 @@ export default async function build(dir: string, conf = null): Promise { let hasPublicDir = false telemetry.record( - eventVersion({ + eventCliSession(PHASE_PRODUCTION_BUILD, dir, { cliCommand: 'build', isSrcDir: path.relative(dir, pagesDir!).startsWith('src'), hasNowJson: !!(await findUp('now.json', { cwd: dir })), diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 386928105af9922..e43f65e243417f7 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -31,7 +31,7 @@ import { import loadConfig, { isTargetLikeServerless, } from '../next-server/server/config' -import { eventVersion } from '../telemetry/events' +import { eventCliSession } from '../telemetry/events' import { Telemetry } from '../telemetry/storage' import { normalizePagePath } from '../next-server/server/normalize-page-path' @@ -105,7 +105,7 @@ export default async function( if (telemetry) { telemetry.record( - eventVersion({ + eventCliSession(PHASE_EXPORT, distDir, { cliCommand: 'export', isSrcDir: null, hasNowJson: !!(await findUp('now.json', { cwd: dir })), diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts index 4416fbd277dd667..9d675c14d2dede3 100644 --- a/packages/next/next-server/server/config.ts +++ b/packages/next/next-server/server/config.ts @@ -198,7 +198,7 @@ function assignDefaults(userConfig: { [key: string]: any }) { return result } -function normalizeConfig(phase: string, config: any) { +export function normalizeConfig(phase: string, config: any) { if (typeof config === 'function') { config = config(phase, { defaultConfig }) diff --git a/packages/next/server/next-dev-server.ts b/packages/next/server/next-dev-server.ts index 79d231850401094..3df13d0499adfc8 100644 --- a/packages/next/server/next-dev-server.ts +++ b/packages/next/server/next-dev-server.ts @@ -25,7 +25,7 @@ import { __ApiPreviewProps } from '../next-server/server/api-utils' import Server, { ServerConstructor } from '../next-server/server/next-server' import { normalizePagePath } from '../next-server/server/normalize-page-path' import Router, { Params, route } from '../next-server/server/router' -import { eventVersion } from '../telemetry/events' +import { eventCliSession } from '../telemetry/events' import { Telemetry } from '../telemetry/storage' import ErrorDebug from './error-debug' import HotReloader from './hot-reloader' @@ -233,7 +233,7 @@ export default class DevServer extends Server { const telemetry = new Telemetry({ distDir: this.distDir }) telemetry.record( - eventVersion({ + eventCliSession(PHASE_DEVELOPMENT_SERVER, this.distDir, { cliCommand: 'dev', isSrcDir: relative(this.dir, this.pagesDir!).startsWith('src'), hasNowJson: !!(await findUp('now.json', { cwd: this.dir })), diff --git a/packages/next/telemetry/events/version.ts b/packages/next/telemetry/events/version.ts index 8c23ff516676b5a..a4b9ef4803a0296 100644 --- a/packages/next/telemetry/events/version.ts +++ b/packages/next/telemetry/events/version.ts @@ -1,3 +1,13 @@ +import findUp from 'find-up' +import path from 'path' +import { + CONFIG_FILE, + PHASE_DEVELOPMENT_SERVER, + PHASE_EXPORT, + PHASE_PRODUCTION_BUILD, +} from '../../next-server/lib/constants' +import { normalizeConfig } from '../../next-server/server/config' + const EVENT_VERSION = 'NEXT_CLI_SESSION_STARTED' type EventCliSessionStarted = { @@ -7,27 +17,92 @@ type EventCliSessionStarted = { isSrcDir: boolean | null hasNowJson: boolean isCustomServer: boolean | null + hasNextConfig: boolean + buildTarget: string + hasWebpackConfig: boolean + hasBabelConfig: boolean +} + +function hasBabelConfig(dir: string): boolean { + try { + const noopFile = path.join(dir, 'noop.js') + const res = require('@babel/core').loadPartialConfig({ + cwd: dir, + filename: noopFile, + sourceFileName: noopFile, + }) as any + const isForTooling = + res.options?.presets?.every( + (e: any) => e?.file?.request === 'next/babel' + ) && res.options?.plugins?.length === 0 + return res.hasFilesystemConfig() && !isForTooling + } catch { + return false + } +} + +type NextConfigurationPhase = + | typeof PHASE_DEVELOPMENT_SERVER + | typeof PHASE_PRODUCTION_BUILD + | typeof PHASE_EXPORT + +function getNextConfig( + phase: NextConfigurationPhase, + dir: string +): { [key: string]: any } | null { + try { + const configurationPath = findUp.sync(CONFIG_FILE, { + cwd: dir, + }) + + if (configurationPath) { + // This should've already been loaded, and thus should be cached / won't + // be re-evaluated. + const configurationModule = require(configurationPath) + + // Re-normalize the configuration. + return normalizeConfig( + phase, + configurationModule.default || configurationModule + ) + } + } catch { + // ignored + } + return null } -export function eventVersion( - event: Omit +export function eventCliSession( + phase: NextConfigurationPhase, + dir: string, + event: Omit< + EventCliSessionStarted, + | 'nextVersion' + | 'nodeVersion' + | 'hasNextConfig' + | 'buildTarget' + | 'hasWebpackConfig' + | 'hasBabelConfig' + > ): { eventName: string; payload: EventCliSessionStarted }[] { // This should be an invariant, if it fails our build tooling is broken. if (typeof process.env.__NEXT_VERSION !== 'string') { return [] } - return [ - { - eventName: EVENT_VERSION, - payload: { - nextVersion: process.env.__NEXT_VERSION, - nodeVersion: process.version, - cliCommand: event.cliCommand, - isSrcDir: event.isSrcDir, - hasNowJson: event.hasNowJson, - isCustomServer: event.isCustomServer, - } as EventCliSessionStarted, - }, - ] + const userConfiguration = getNextConfig(phase, dir) + + const payload: EventCliSessionStarted = { + nextVersion: process.env.__NEXT_VERSION, + nodeVersion: process.version, + cliCommand: event.cliCommand, + isSrcDir: event.isSrcDir, + hasNowJson: event.hasNowJson, + isCustomServer: event.isCustomServer, + hasNextConfig: !!userConfiguration, + buildTarget: userConfiguration?.target ?? 'default', + hasWebpackConfig: typeof userConfiguration?.webpack === 'function', + hasBabelConfig: hasBabelConfig(dir), + } + return [{ eventName: EVENT_VERSION, payload }] } diff --git a/test/integration/telemetry/.babelrc.default b/test/integration/telemetry/.babelrc.default new file mode 100644 index 000000000000000..1ff94f7ed28e16b --- /dev/null +++ b/test/integration/telemetry/.babelrc.default @@ -0,0 +1,3 @@ +{ + "presets": ["next/babel"] +} diff --git a/test/integration/telemetry/.babelrc.plugin b/test/integration/telemetry/.babelrc.plugin new file mode 100644 index 000000000000000..e79918c689a2919 --- /dev/null +++ b/test/integration/telemetry/.babelrc.plugin @@ -0,0 +1,4 @@ +{ + "presets": ["next/babel"], + "plugins": ["@babel/plugin-proposal-object-rest-spread"] +} diff --git a/test/integration/telemetry/.babelrc.preset b/test/integration/telemetry/.babelrc.preset new file mode 100644 index 000000000000000..f9a98e420859bdd --- /dev/null +++ b/test/integration/telemetry/.babelrc.preset @@ -0,0 +1,3 @@ +{ + "presets": ["next/babel", "@babel/preset-flow"] +} diff --git a/test/integration/telemetry/next.config.target b/test/integration/telemetry/next.config.target new file mode 100644 index 000000000000000..d1a88cac38d9ca1 --- /dev/null +++ b/test/integration/telemetry/next.config.target @@ -0,0 +1,10 @@ +const { PHASE_PRODUCTION_BUILD } = require('next/constants') + +module.exports = phase => { + if (phase === PHASE_PRODUCTION_BUILD) { + return { + target: 'experimental-serverless-trace', + } + } + return {} +} diff --git a/test/integration/telemetry/next.config.webpack b/test/integration/telemetry/next.config.webpack new file mode 100644 index 000000000000000..32facfdb9d19b68 --- /dev/null +++ b/test/integration/telemetry/next.config.webpack @@ -0,0 +1,5 @@ +module.exports = { + webpack(config) { + return config + }, +} diff --git a/test/integration/telemetry/package.babel b/test/integration/telemetry/package.babel new file mode 100644 index 000000000000000..74074a210b716c6 --- /dev/null +++ b/test/integration/telemetry/package.babel @@ -0,0 +1,10 @@ +{ + "babel": { + "presets": [ + "next/babel" + ], + "plugins": [ + "@babel/plugin-proposal-object-rest-spread" + ] + } +} diff --git a/test/integration/telemetry/test/index.test.js b/test/integration/telemetry/test/index.test.js index 7b2a9e4c7b42a7c..f059b50af04bed7 100644 --- a/test/integration/telemetry/test/index.test.js +++ b/test/integration/telemetry/test/index.test.js @@ -138,6 +138,182 @@ describe('Telemetry CLI', () => { expect(event2).toMatch(/hasTestPages.*?true/) }) + it('detects correct cli session defaults', async () => { + const { stderr } = await runNextCommand(['build', appDir], { + stderr: true, + env: { + NEXT_TELEMETRY_DEBUG: 1, + }, + }) + + const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/ + .exec(stderr) + .pop() + + expect(event).toMatch(/"hasNextConfig": false/) + expect(event).toMatch(/"buildTarget": "default"/) + expect(event).toMatch(/"hasWebpackConfig": false/) + expect(event).toMatch(/"hasBabelConfig": false/) + }) + + it('cli session: babel tooling config', async () => { + await fs.rename( + path.join(appDir, '.babelrc.default'), + path.join(appDir, '.babelrc') + ) + const { stderr } = await runNextCommand(['build', appDir], { + stderr: true, + env: { + NEXT_TELEMETRY_DEBUG: 1, + }, + }) + await fs.rename( + path.join(appDir, '.babelrc'), + path.join(appDir, '.babelrc.default') + ) + + const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/ + .exec(stderr) + .pop() + + expect(event).toMatch(/"hasNextConfig": false/) + expect(event).toMatch(/"buildTarget": "default"/) + expect(event).toMatch(/"hasWebpackConfig": false/) + expect(event).toMatch(/"hasBabelConfig": false/) + }) + + it('cli session: custom babel config (plugin)', async () => { + await fs.rename( + path.join(appDir, '.babelrc.plugin'), + path.join(appDir, '.babelrc') + ) + const { stderr } = await runNextCommand(['build', appDir], { + stderr: true, + env: { + NEXT_TELEMETRY_DEBUG: 1, + }, + }) + await fs.rename( + path.join(appDir, '.babelrc'), + path.join(appDir, '.babelrc.plugin') + ) + + const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/ + .exec(stderr) + .pop() + + expect(event).toMatch(/"hasNextConfig": false/) + expect(event).toMatch(/"buildTarget": "default"/) + expect(event).toMatch(/"hasWebpackConfig": false/) + expect(event).toMatch(/"hasBabelConfig": true/) + }) + + it('cli session: package.json custom babel config (plugin)', async () => { + await fs.rename( + path.join(appDir, 'package.babel'), + path.join(appDir, 'package.json') + ) + const { stderr } = await runNextCommand(['build', appDir], { + stderr: true, + env: { + NEXT_TELEMETRY_DEBUG: 1, + }, + }) + await fs.rename( + path.join(appDir, 'package.json'), + path.join(appDir, 'package.babel') + ) + + console.log(stderr) + + const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/ + .exec(stderr) + .pop() + + expect(event).toMatch(/"hasNextConfig": false/) + expect(event).toMatch(/"buildTarget": "default"/) + expect(event).toMatch(/"hasWebpackConfig": false/) + expect(event).toMatch(/"hasBabelConfig": true/) + }) + + it('cli session: custom babel config (preset)', async () => { + await fs.rename( + path.join(appDir, '.babelrc.preset'), + path.join(appDir, '.babelrc') + ) + const { stderr } = await runNextCommand(['build', appDir], { + stderr: true, + env: { + NEXT_TELEMETRY_DEBUG: 1, + }, + }) + await fs.rename( + path.join(appDir, '.babelrc'), + path.join(appDir, '.babelrc.preset') + ) + + const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/ + .exec(stderr) + .pop() + + expect(event).toMatch(/"hasNextConfig": false/) + expect(event).toMatch(/"buildTarget": "default"/) + expect(event).toMatch(/"hasWebpackConfig": false/) + expect(event).toMatch(/"hasBabelConfig": true/) + }) + + it('cli session: next config with target', async () => { + await fs.rename( + path.join(appDir, 'next.config.target'), + path.join(appDir, 'next.config.js') + ) + const { stderr } = await runNextCommand(['build', appDir], { + stderr: true, + env: { + NEXT_TELEMETRY_DEBUG: 1, + }, + }) + await fs.rename( + path.join(appDir, 'next.config.js'), + path.join(appDir, 'next.config.target') + ) + + const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/ + .exec(stderr) + .pop() + + expect(event).toMatch(/"hasNextConfig": true/) + expect(event).toMatch(/"buildTarget": "experimental-serverless-trace"/) + expect(event).toMatch(/"hasWebpackConfig": false/) + expect(event).toMatch(/"hasBabelConfig": false/) + }) + + it('cli session: next config with webpack', async () => { + await fs.rename( + path.join(appDir, 'next.config.webpack'), + path.join(appDir, 'next.config.js') + ) + const { stderr } = await runNextCommand(['build', appDir], { + stderr: true, + env: { + NEXT_TELEMETRY_DEBUG: 1, + }, + }) + await fs.rename( + path.join(appDir, 'next.config.js'), + path.join(appDir, 'next.config.webpack') + ) + + const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/ + .exec(stderr) + .pop() + + expect(event).toMatch(/"hasNextConfig": true/) + expect(event).toMatch(/"buildTarget": "default"/) + expect(event).toMatch(/"hasWebpackConfig": true/) + expect(event).toMatch(/"hasBabelConfig": false/) + }) + it('detect static 404 correctly for `next build`', async () => { const { stderr } = await nextBuild(appDir, [], { stderr: true,