From 5f39205c683461409850c5e97091afdc4cdf2faa Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Wed, 5 Feb 2020 00:49:34 -0500 Subject: [PATCH 1/3] Check next.config.js settings --- packages/next/build/index.ts | 4 +- packages/next/export/index.ts | 4 +- packages/next/next-server/server/config.ts | 2 +- packages/next/server/next-dev-server.ts | 10 +-- packages/next/telemetry/events/version.ts | 96 ++++++++++++++++++---- 5 files changed, 91 insertions(+), 25 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 4d4307882bf0c25..0b09309d19a6f3a 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -47,8 +47,8 @@ import loadConfig, { import { eventBuildCompleted, eventBuildOptimize, + eventCliSession, eventNextPlugins, - eventVersion, } from '../telemetry/events' import { Telemetry } from '../telemetry/storage' import { CompilerResult, runCompiler } from './compiler' @@ -157,7 +157,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 40f0c33ca68852a..4df58650fbfe76e 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' const mkdirp = promisify(mkdirpModule) @@ -104,7 +104,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 248b38f80acbe17..ccac75e6ecd7f07 100644 --- a/packages/next/server/next-dev-server.ts +++ b/packages/next/server/next-dev-server.ts @@ -1,4 +1,5 @@ import AmpHtmlValidator from 'amphtml-validator' +import findUp from 'find-up' import fs from 'fs' import { IncomingMessage, ServerResponse } from 'http' import { join, relative } from 'path' @@ -6,9 +7,9 @@ import React from 'react' import { UrlWithParsedQuery } from 'url' import { promisify } from 'util' import Watchpack from 'watchpack' -import findUp from 'find-up' import { ampValidation } from '../build/output/index' import * as Log from '../build/output/log' +import checkCustomRoutes from '../lib/check-custom-routes' import { PUBLIC_DIR_MIDDLEWARE_CONFLICT } from '../lib/constants' import { findPagesDir } from '../lib/find-pages-dir' import { verifyTypeScriptSetup } from '../lib/verifyTypeScriptSetup' @@ -21,13 +22,12 @@ import { } from '../next-server/lib/router/utils' import Server, { ServerConstructor } from '../next-server/server/next-server' import { normalizePagePath } from '../next-server/server/normalize-page-path' -import Router, { route, Params } from '../next-server/server/router' -import { eventVersion } from '../telemetry/events' +import Router, { Params, route } from '../next-server/server/router' +import { eventCliSession } from '../telemetry/events' import { Telemetry } from '../telemetry/storage' import ErrorDebug from './error-debug' import HotReloader from './hot-reloader' import { findPageFile } from './lib/find-page-file' -import checkCustomRoutes from '../lib/check-custom-routes' if (typeof React.Suspense === 'undefined') { throw new Error( @@ -230,7 +230,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..b9146bf2846b5fb 100644 --- a/packages/next/telemetry/events/version.ts +++ b/packages/next/telemetry/events/version.ts @@ -1,3 +1,13 @@ +import Babel from '@babel/core' +import findUp from 'find-up' +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,83 @@ 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 res = Babel.loadPartialConfig({ cwd: dir }) as any + return res.hasFilesystemConfig() + } 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 }] } From fe879d0362bab1d43ebe62ff7f5d574239565e59 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Fri, 14 Feb 2020 11:12:52 -0500 Subject: [PATCH 2/3] Add tests --- packages/next/telemetry/events/version.ts | 15 +- test/integration/telemetry/.babelrc.default | 3 + test/integration/telemetry/.babelrc.plugin | 4 + test/integration/telemetry/.babelrc.preset | 3 + test/integration/telemetry/next.config.target | 10 ++ .../integration/telemetry/next.config.webpack | 5 + test/integration/telemetry/test/index.test.js | 148 ++++++++++++++++++ 7 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 test/integration/telemetry/.babelrc.default create mode 100644 test/integration/telemetry/.babelrc.plugin create mode 100644 test/integration/telemetry/.babelrc.preset create mode 100644 test/integration/telemetry/next.config.target create mode 100644 test/integration/telemetry/next.config.webpack diff --git a/packages/next/telemetry/events/version.ts b/packages/next/telemetry/events/version.ts index b9146bf2846b5fb..a4b9ef4803a0296 100644 --- a/packages/next/telemetry/events/version.ts +++ b/packages/next/telemetry/events/version.ts @@ -1,5 +1,5 @@ -import Babel from '@babel/core' import findUp from 'find-up' +import path from 'path' import { CONFIG_FILE, PHASE_DEVELOPMENT_SERVER, @@ -25,8 +25,17 @@ type EventCliSessionStarted = { function hasBabelConfig(dir: string): boolean { try { - const res = Babel.loadPartialConfig({ cwd: dir }) as any - return res.hasFilesystemConfig() + 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 } 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/test/index.test.js b/test/integration/telemetry/test/index.test.js index 7b2a9e4c7b42a7c..2cdb42bc049f46e 100644 --- a/test/integration/telemetry/test/index.test.js +++ b/test/integration/telemetry/test/index.test.js @@ -138,6 +138,154 @@ 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: 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, From 3f84389b0589fedec40b3228211b1e6ba44a3c3f Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Fri, 14 Feb 2020 11:21:00 -0500 Subject: [PATCH 3/3] test package.json case too --- test/integration/telemetry/package.babel | 10 +++++++ test/integration/telemetry/test/index.test.js | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 test/integration/telemetry/package.babel 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 2cdb42bc049f46e..f059b50af04bed7 100644 --- a/test/integration/telemetry/test/index.test.js +++ b/test/integration/telemetry/test/index.test.js @@ -208,6 +208,34 @@ describe('Telemetry CLI', () => { 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'),