From 450e7130b082d8982f3e46c0e3e4dd7fad2d8048 Mon Sep 17 00:00:00 2001 From: Kevin Jennison Date: Sat, 23 Jan 2021 09:49:14 -0500 Subject: [PATCH 1/5] Add example: Firebase authentication + SSR (#15202) Co-authored-by: Luis Alvarez D. --- .../.env.local.example | 9 --- .../with-firebase-authentication/README.md | 39 +---------- .../components/FirebaseAuth.js | 42 ------------ .../with-firebase-authentication/package.json | 20 ------ .../pages/api/getFood.js | 17 ----- .../pages/auth.js | 14 ---- .../pages/example.js | 17 ----- .../pages/index.js | 63 ------------------ .../utils/auth/firebaseAdmin.js | 24 ------- .../utils/auth/initFirebase.js | 15 ----- .../utils/auth/mapUserData.js | 9 --- .../utils/auth/useUser.js | 65 ------------------- .../utils/auth/userCookies.js | 19 ------ 13 files changed, 2 insertions(+), 351 deletions(-) delete mode 100644 examples/with-firebase-authentication/.env.local.example delete mode 100644 examples/with-firebase-authentication/components/FirebaseAuth.js delete mode 100644 examples/with-firebase-authentication/package.json delete mode 100644 examples/with-firebase-authentication/pages/api/getFood.js delete mode 100644 examples/with-firebase-authentication/pages/auth.js delete mode 100644 examples/with-firebase-authentication/pages/example.js delete mode 100644 examples/with-firebase-authentication/pages/index.js delete mode 100644 examples/with-firebase-authentication/utils/auth/firebaseAdmin.js delete mode 100644 examples/with-firebase-authentication/utils/auth/initFirebase.js delete mode 100644 examples/with-firebase-authentication/utils/auth/mapUserData.js delete mode 100644 examples/with-firebase-authentication/utils/auth/useUser.js delete mode 100644 examples/with-firebase-authentication/utils/auth/userCookies.js diff --git a/examples/with-firebase-authentication/.env.local.example b/examples/with-firebase-authentication/.env.local.example deleted file mode 100644 index 283b89ef8c95f84..000000000000000 --- a/examples/with-firebase-authentication/.env.local.example +++ /dev/null @@ -1,9 +0,0 @@ -# Update these with your Firebase app's values. -FIREBASE_CLIENT_EMAIL=my-example-app-email@example.com -NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY=MyExampleAppAPIKey123 -NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=my-example-app.firebaseapp.com -NEXT_PUBLIC_FIREBASE_DATABASE_URL=https://my-example-app.firebaseio.com -NEXT_PUBLIC_FIREBASE_PROJECT_ID=my-example-app-id - -# Your Firebase private key. -FIREBASE_PRIVATE_KEY=some-key-here diff --git a/examples/with-firebase-authentication/README.md b/examples/with-firebase-authentication/README.md index 37897745b53d60d..20af17b2fdaff78 100644 --- a/examples/with-firebase-authentication/README.md +++ b/examples/with-firebase-authentication/README.md @@ -1,38 +1,3 @@ -# Example: Firebase authentication with a serverless API +# With Firebase Authentication -This example includes Firebase authentication and serverless [API routes](https://nextjs.org/docs/api-routes/introduction). - -## How to use - -Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example: - -```bash -npx create-next-app --example with-firebase-authentication with-firebase-authentication-app -# or -yarn create next-app --example with-firebase-authentication with-firebase-authentication-app -``` - -## Configuration - -Set up Firebase: - -- Create a project at the [Firebase console](https://console.firebase.google.com/). -- Copy the contents of `.env.local.example` into a new file called `.env.local` -- Get your account credentials from the Firebase console at _Project settings > Service accounts_, where you can click on _Generate new private key_ and download the credentials as a json file. It will contain keys such as `project_id`, `client_email` and `private_key`. Set them as environment variables in the `.env.local` file at the root of this project. -- Get your authentication credentials from the Firebase console under _Project settings > General> Your apps_ Add a new web app if you don't already have one. Under _Firebase SDK snippet_ choose _Config_ to get the configuration as JSON. It will include keys like `apiKey` and `authDomain`. Set the appropriate environment variables in the `.env.local` file at the root of this project. -- Go to **Develop**, click on **Realtime Database** and create a database if you don't already have one. Under _data_ get `databaseUrl`(e.g. `https://[dbname].firebaseio.com/`). Set the appropriate environment variables in the `.env.local` file at the root of this project. -- Go to **Develop**, click on **Authentication** and in the **Sign-in method** tab enable authentication for the app. - -Install it and run: - -```bash -npm install -npm run dev -# or -yarn -yarn dev -``` - -Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). - -After deploying, copy the deployment URL and navigate to your Firebase project's Authentication tab. Scroll down in the page to "Authorized domains" and add that URL to the list. +Check out the [next-firebase-auth](https://github.com/gladly-team/next-firebase-auth) package and their [official example](https://github.com/gladly-team/next-firebase-auth/tree/main/example) for a powerful implementation of Firebase authentication. diff --git a/examples/with-firebase-authentication/components/FirebaseAuth.js b/examples/with-firebase-authentication/components/FirebaseAuth.js deleted file mode 100644 index f9e8f295a000e9d..000000000000000 --- a/examples/with-firebase-authentication/components/FirebaseAuth.js +++ /dev/null @@ -1,42 +0,0 @@ -import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth' -import firebase from 'firebase/app' -import 'firebase/auth' -import initFirebase from '../utils/auth/initFirebase' -import { setUserCookie } from '../utils/auth/userCookies' -import { mapUserData } from '../utils/auth/mapUserData' - -// Init the Firebase app. -initFirebase() - -const firebaseAuthConfig = { - signInFlow: 'popup', - // Auth providers - // https://github.com/firebase/firebaseui-web#configure-oauth-providers - signInOptions: [ - { - provider: firebase.auth.EmailAuthProvider.PROVIDER_ID, - requireDisplayName: false, - }, - ], - signInSuccessUrl: '/', - credentialHelper: 'none', - callbacks: { - signInSuccessWithAuthResult: async ({ user }, redirectUrl) => { - const userData = await mapUserData(user) - setUserCookie(userData) - }, - }, -} - -const FirebaseAuth = () => { - return ( -
- -
- ) -} - -export default FirebaseAuth diff --git a/examples/with-firebase-authentication/package.json b/examples/with-firebase-authentication/package.json deleted file mode 100644 index 8fa14c38cd77db5..000000000000000 --- a/examples/with-firebase-authentication/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "with-firebase-authentication", - "version": "1.0.0", - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start" - }, - "dependencies": { - "firebase": "^7.15.5", - "firebase-admin": "^8.12.1", - "js-cookie": "2.2.1", - "next": "latest", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-firebaseui": "4.1.0", - "swr": "0.2.3" - }, - "license": "MIT" -} diff --git a/examples/with-firebase-authentication/pages/api/getFood.js b/examples/with-firebase-authentication/pages/api/getFood.js deleted file mode 100644 index 188f4e3140f1163..000000000000000 --- a/examples/with-firebase-authentication/pages/api/getFood.js +++ /dev/null @@ -1,17 +0,0 @@ -import { verifyIdToken } from '../../utils/auth/firebaseAdmin' -const favoriteFoods = ['pizza', 'burger', 'chips', 'tortilla'] - -const getFood = async (req, res) => { - const token = req.headers.token - - try { - await verifyIdToken(token) - return res.status(200).json({ - food: favoriteFoods[Math.floor(Math.random() * favoriteFoods.length)], - }) - } catch (error) { - return res.status(401).send('You are unauthorised') - } -} - -export default getFood diff --git a/examples/with-firebase-authentication/pages/auth.js b/examples/with-firebase-authentication/pages/auth.js deleted file mode 100644 index 431eaf691e108e5..000000000000000 --- a/examples/with-firebase-authentication/pages/auth.js +++ /dev/null @@ -1,14 +0,0 @@ -import FirebaseAuth from '../components/FirebaseAuth' - -const Auth = () => { - return ( -
-

Sign in

-
- -
-
- ) -} - -export default Auth diff --git a/examples/with-firebase-authentication/pages/example.js b/examples/with-firebase-authentication/pages/example.js deleted file mode 100644 index 6f179479fd3656a..000000000000000 --- a/examples/with-firebase-authentication/pages/example.js +++ /dev/null @@ -1,17 +0,0 @@ -import Link from 'next/link' - -const Example = (props) => { - return ( -
-

- This page is static because it does not fetch any data or include the - authed user info. -

- - Home - -
- ) -} - -export default Example diff --git a/examples/with-firebase-authentication/pages/index.js b/examples/with-firebase-authentication/pages/index.js deleted file mode 100644 index 0434faa8a443db7..000000000000000 --- a/examples/with-firebase-authentication/pages/index.js +++ /dev/null @@ -1,63 +0,0 @@ -import useSWR from 'swr' -import Link from 'next/link' -import { useUser } from '../utils/auth/useUser' - -const fetcher = (url, token) => - fetch(url, { - method: 'GET', - headers: new Headers({ 'Content-Type': 'application/json', token }), - credentials: 'same-origin', - }).then((res) => res.json()) - -const Index = () => { - const { user, logout } = useUser() - const { data, error } = useSWR( - user ? ['/api/getFood', user.token] : null, - fetcher - ) - if (!user) { - return ( - <> -

Hi there!

-

- You are not signed in.{' '} - - Sign in - -

- - ) - } - - return ( -
-
-

You're signed in. Email: {user.email}

-

logout()} - > - Log out -

-
-
- - Another example page - -
- {error &&
Failed to fetch food!
} - {data && !error ? ( -
Your favorite food is {data.food}.
- ) : ( -
Loading...
- )} -
- ) -} - -export default Index diff --git a/examples/with-firebase-authentication/utils/auth/firebaseAdmin.js b/examples/with-firebase-authentication/utils/auth/firebaseAdmin.js deleted file mode 100644 index b53ba7b9fb68952..000000000000000 --- a/examples/with-firebase-authentication/utils/auth/firebaseAdmin.js +++ /dev/null @@ -1,24 +0,0 @@ -import * as admin from 'firebase-admin' - -export const verifyIdToken = (token) => { - const firebasePrivateKey = process.env.FIREBASE_PRIVATE_KEY - - if (!admin.apps.length) { - admin.initializeApp({ - credential: admin.credential.cert({ - projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, - clientEmail: process.env.FIREBASE_CLIENT_EMAIL, - // https://stackoverflow.com/a/41044630/1332513 - privateKey: firebasePrivateKey.replace(/\\n/g, '\n'), - }), - databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, - }) - } - - return admin - .auth() - .verifyIdToken(token) - .catch((error) => { - throw error - }) -} diff --git a/examples/with-firebase-authentication/utils/auth/initFirebase.js b/examples/with-firebase-authentication/utils/auth/initFirebase.js deleted file mode 100644 index e0c5eca65c6dbe8..000000000000000 --- a/examples/with-firebase-authentication/utils/auth/initFirebase.js +++ /dev/null @@ -1,15 +0,0 @@ -import firebase from 'firebase/app' -import 'firebase/auth' - -const config = { - apiKey: process.env.NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY, - authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, - databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, - projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, -} - -export default function initFirebase() { - if (!firebase.apps.length) { - firebase.initializeApp(config) - } -} diff --git a/examples/with-firebase-authentication/utils/auth/mapUserData.js b/examples/with-firebase-authentication/utils/auth/mapUserData.js deleted file mode 100644 index ff29018c4760936..000000000000000 --- a/examples/with-firebase-authentication/utils/auth/mapUserData.js +++ /dev/null @@ -1,9 +0,0 @@ -export const mapUserData = async (user) => { - const { uid, email } = user - const token = await user.getIdToken(true) - return { - id: uid, - email, - token, - } -} diff --git a/examples/with-firebase-authentication/utils/auth/useUser.js b/examples/with-firebase-authentication/utils/auth/useUser.js deleted file mode 100644 index 9a3158821777541..000000000000000 --- a/examples/with-firebase-authentication/utils/auth/useUser.js +++ /dev/null @@ -1,65 +0,0 @@ -import { useEffect, useState } from 'react' -import { useRouter } from 'next/router' -import firebase from 'firebase/app' -import 'firebase/auth' -import initFirebase from '../auth/initFirebase' -import { - removeUserCookie, - setUserCookie, - getUserFromCookie, -} from './userCookies' -import { mapUserData } from './mapUserData' - -initFirebase() - -const useUser = () => { - const [user, setUser] = useState() - const router = useRouter() - - const logout = async () => { - return firebase - .auth() - .signOut() - .then(() => { - // Sign-out successful. - router.push('/auth') - }) - .catch((e) => { - console.error(e) - }) - } - - useEffect(() => { - // Firebase updates the id token every hour, this - // makes sure the react state and the cookie are - // both kept up to date - const cancelAuthListener = firebase - .auth() - .onIdTokenChanged(async (user) => { - if (user) { - const userData = await mapUserData(user) - setUserCookie(userData) - setUser(userData) - } else { - removeUserCookie() - setUser() - } - }) - - const userFromCookie = getUserFromCookie() - if (!userFromCookie) { - router.push('/') - return - } - setUser(userFromCookie) - - return () => { - cancelAuthListener() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - return { user, logout } -} - -export { useUser } diff --git a/examples/with-firebase-authentication/utils/auth/userCookies.js b/examples/with-firebase-authentication/utils/auth/userCookies.js deleted file mode 100644 index 776f02ff463101a..000000000000000 --- a/examples/with-firebase-authentication/utils/auth/userCookies.js +++ /dev/null @@ -1,19 +0,0 @@ -import cookies from 'js-cookie' - -export const getUserFromCookie = () => { - const cookie = cookies.get('auth') - if (!cookie) { - return - } - return JSON.parse(cookie) -} - -export const setUserCookie = (user) => { - cookies.set('auth', user, { - // firebase id tokens expire in one hour - // set cookie expiry to match - expires: 1 / 24, - }) -} - -export const removeUserCookie = () => cookies.remove('auth') From d68d21c3291d4126008e0d3d8035924e82bcc1ba Mon Sep 17 00:00:00 2001 From: Darsh Patel Date: Sat, 23 Jan 2021 20:45:13 +0530 Subject: [PATCH 2/5] Test: serverless target should set correct revalidation (`cache-control`) header (#15512) --- .../next.config.js | 5 ++ .../pages/revalidate.js | 16 +++++ .../serverless-trace-revalidate/server.js | 21 +++++++ .../test/index.test.js | 58 +++++++++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 test/integration/serverless-trace-revalidate/next.config.js create mode 100644 test/integration/serverless-trace-revalidate/pages/revalidate.js create mode 100644 test/integration/serverless-trace-revalidate/server.js create mode 100644 test/integration/serverless-trace-revalidate/test/index.test.js diff --git a/test/integration/serverless-trace-revalidate/next.config.js b/test/integration/serverless-trace-revalidate/next.config.js new file mode 100644 index 000000000000000..d15379d6767aacd --- /dev/null +++ b/test/integration/serverless-trace-revalidate/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + target: 'experimental-serverless-trace', + // make sure error isn't thrown from empty publicRuntimeConfig + publicRuntimeConfig: {}, +} diff --git a/test/integration/serverless-trace-revalidate/pages/revalidate.js b/test/integration/serverless-trace-revalidate/pages/revalidate.js new file mode 100644 index 000000000000000..f810f624ed2bcec --- /dev/null +++ b/test/integration/serverless-trace-revalidate/pages/revalidate.js @@ -0,0 +1,16 @@ +export const getStaticProps = () => { + return { + props: { + hello: 'hello world', + random: Math.random(), + }, + revalidate: 10, + } +} + +export default ({ hello, random }) => ( + <> +

{hello}

+

{random}

+ +) diff --git a/test/integration/serverless-trace-revalidate/server.js b/test/integration/serverless-trace-revalidate/server.js new file mode 100644 index 000000000000000..848c88de5ac4fba --- /dev/null +++ b/test/integration/serverless-trace-revalidate/server.js @@ -0,0 +1,21 @@ +const path = require('path') +const http = require('http') + +const server = http.createServer((req, res) => { + const pagePath = (page) => path.join('.next/serverless/pages/', page) + const render = (page) => { + require(`./${pagePath(page)}`).render(req, res) + } + + switch (req.url) { + case '/revalidate': { + return render('/revalidate') + } + default: { + return res.end('404') + } + } +}) +server.listen(process.env.PORT, () => { + console.log('ready on', process.env.PORT) +}) diff --git a/test/integration/serverless-trace-revalidate/test/index.test.js b/test/integration/serverless-trace-revalidate/test/index.test.js new file mode 100644 index 000000000000000..340bd296dfe26ab --- /dev/null +++ b/test/integration/serverless-trace-revalidate/test/index.test.js @@ -0,0 +1,58 @@ +/* eslint-env jest */ + +import fs from 'fs-extra' +import { join } from 'path' +import { + nextBuild, + findPort, + killApp, + initNextServerScript, +} from 'next-test-utils' + +const appDir = join(__dirname, '../') +jest.setTimeout(1000 * 60 * 2) + +let appPort +let app +let buildId + +const nextStart = async (appDir, appPort) => { + const scriptPath = join(appDir, 'server.js') + const env = Object.assign({ ...process.env }, { PORT: `${appPort}` }) + + return initNextServerScript( + scriptPath, + /ready on/i, + env, + /ReferenceError: options is not defined/ + ) +} + +describe('Serverless Trace', () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + }) + afterAll(() => killApp(app)) + + it('should have revalidate page in prerender-manifest with correct interval', async () => { + const data = await fs.readJSON( + join(appDir, '.next/prerender-manifest.json') + ) + expect(data.routes['/revalidate']).toEqual({ + initialRevalidateSeconds: 10, + dataRoute: `/_next/data/${buildId}/revalidate.json`, + srcRoute: null, + }) + }) + + it('should set correct Cache-Control header', async () => { + const url = `http://localhost:${appPort}/revalidate` + const res = await fetch(url) + expect(res.headers.get('Cache-Control')).toMatch( + 's-maxage=10, stale-while-revalidate' + ) + }) +}) From caf95e1e357818734250cafd72f298e1fc8917b3 Mon Sep 17 00:00:00 2001 From: Mohsen Azimi Date: Sat, 23 Jan 2021 10:40:30 -0500 Subject: [PATCH 3/5] Add a VSCode Launch configuration for debugging examples (#16940) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .vscode/launch.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index e7cd5522232f3ec..3a6858c2fd0dce6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -42,6 +42,16 @@ "port": 9229, "skipFiles": ["/**"], "outFiles": ["${workspaceFolder}/packages/next/dist/**/*"] + }, + { + "name": "Launch this example", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "yarn", + "runtimeArgs": ["run", "debug", "dev", "${fileDirname}"], + "skipFiles": ["/**"], + "port": 9229 } ] } From 5baedf55670b0a8d64a01211f84aa0a9694b298c Mon Sep 17 00:00:00 2001 From: "Dylan R. Johnston" Date: Sun, 24 Jan 2021 00:28:33 +0800 Subject: [PATCH 4/5] Update data-fetching.md to use async file loading (#17518) Co-authored-by: Tim Neutkens --- docs/basic-features/data-fetching.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/basic-features/data-fetching.md b/docs/basic-features/data-fetching.md index 362e8e0bfea39bf..f82f7cac5e1100e 100644 --- a/docs/basic-features/data-fetching.md +++ b/docs/basic-features/data-fetching.md @@ -289,7 +289,7 @@ Since Next.js compiles your code into a separate directory you can't use `__dirn Instead you can use `process.cwd()` which gives you the directory where Next.js is being executed. ```jsx -import fs from 'fs' +import { promises as fs } from 'fs' import path from 'path' // posts will be populated at build time by getStaticProps() @@ -311,11 +311,11 @@ function Blog({ posts }) { // direct database queries. See the "Technical details" section. export async function getStaticProps() { const postsDirectory = path.join(process.cwd(), 'posts') - const filenames = fs.readdirSync(postsDirectory) + const filenames = await fs.readdir(postsDirectory) - const posts = filenames.map((filename) => { + const posts = filenames.map(async (filename) => { const filePath = path.join(postsDirectory, filename) - const fileContents = fs.readFileSync(filePath, 'utf8') + const fileContents = await fs.readFile(filePath, 'utf8') // Generally you would parse/transform the contents // For example you can transform markdown to HTML here @@ -329,7 +329,7 @@ export async function getStaticProps() { // will receive `posts` as a prop at build time return { props: { - posts, + posts: await Promise.all(posts), }, } } From 783b9d86ffcf879950cf4e40e97d39951cdedf62 Mon Sep 17 00:00:00 2001 From: Calmon Ribeiro Date: Sun, 24 Jan 2021 10:24:36 -0300 Subject: [PATCH 5/5] fix(next): respect extends in tsconfig with exclude and include (#16619) --- .../typescript/writeConfigurationDefaults.ts | 18 +++---- .../tsconfig-verifier/test/index.test.js | 54 +++++++++++++++++++ 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/packages/next/lib/typescript/writeConfigurationDefaults.ts b/packages/next/lib/typescript/writeConfigurationDefaults.ts index 88021f3ce90772c..ffdf748e7d4b4cb 100644 --- a/packages/next/lib/typescript/writeConfigurationDefaults.ts +++ b/packages/next/lib/typescript/writeConfigurationDefaults.ts @@ -95,16 +95,16 @@ export async function writeConfigurationDefaults( } const desiredCompilerOptions = getDesiredCompilerOptions(ts) - const effectiveConfiguration = await getTypeScriptConfiguration( - ts, - tsConfigPath - ) + const { + options: tsOptions, + raw: rawConfig, + } = await getTypeScriptConfiguration(ts, tsConfigPath) const userTsConfigContent = await fs.readFile(tsConfigPath, { encoding: 'utf8', }) const userTsConfig = CommentJson.parse(userTsConfigContent) - if (userTsConfig.compilerOptions == null) { + if (userTsConfig.compilerOptions == null && !('extends' in rawConfig)) { userTsConfig.compilerOptions = {} isFirstTimeSetup = true } @@ -114,14 +114,14 @@ export async function writeConfigurationDefaults( for (const optionKey of Object.keys(desiredCompilerOptions)) { const check = desiredCompilerOptions[optionKey] if ('suggested' in check) { - if (!(optionKey in effectiveConfiguration.options)) { + if (!(optionKey in tsOptions)) { userTsConfig.compilerOptions[optionKey] = check.suggested suggestedActions.push( chalk.cyan(optionKey) + ' was set to ' + chalk.bold(check.suggested) ) } } else if ('value' in check) { - const ev = effectiveConfiguration.options[optionKey] + const ev = tsOptions[optionKey] if ( !('parsedValues' in check ? check.parsedValues?.includes(ev) @@ -143,7 +143,7 @@ export async function writeConfigurationDefaults( } } - if (userTsConfig.include == null) { + if (!('include' in rawConfig)) { userTsConfig.include = ['next-env.d.ts', '**/*.ts', '**/*.tsx'] suggestedActions.push( chalk.cyan('include') + @@ -152,7 +152,7 @@ export async function writeConfigurationDefaults( ) } - if (userTsConfig.exclude == null) { + if (!('exclude' in rawConfig)) { userTsConfig.exclude = ['node_modules'] suggestedActions.push( chalk.cyan('exclude') + ' was set to ' + chalk.bold(`['node_modules']`) diff --git a/test/integration/tsconfig-verifier/test/index.test.js b/test/integration/tsconfig-verifier/test/index.test.js index 0d974ae7402f4c9..96ef52a9d4fae8a 100644 --- a/test/integration/tsconfig-verifier/test/index.test.js +++ b/test/integration/tsconfig-verifier/test/index.test.js @@ -10,13 +10,16 @@ jest.setTimeout(1000 * 60 * 5) describe('tsconfig.json verifier', () => { const appDir = path.join(__dirname, '../') const tsConfig = path.join(appDir, 'tsconfig.json') + const tsConfigBase = path.join(appDir, 'tsconfig.base.json') beforeEach(async () => { await remove(tsConfig) + await remove(tsConfigBase) }) afterEach(async () => { await remove(tsConfig) + await remove(tsConfigBase) }) it('Creates a default tsconfig.json when one is missing', async () => { @@ -258,4 +261,55 @@ describe('tsconfig.json verifier', () => { " `) }) + + it('allows you to extend another configuration file', async () => { + expect(await exists(tsConfig)).toBe(false) + expect(await exists(tsConfigBase)).toBe(false) + + await writeFile( + tsConfigBase, + ` + { + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] + } + ` + ) + await new Promise((resolve) => setTimeout(resolve, 500)) + + await writeFile(tsConfig, `{ "extends": "./tsconfig.base.json" }`) + await new Promise((resolve) => setTimeout(resolve, 500)) + + const { code } = await nextBuild(appDir) + expect(code).toBe(0) + + expect(await readFile(tsConfig, 'utf8')).toMatchInlineSnapshot( + `"{ \\"extends\\": \\"./tsconfig.base.json\\" }"` + ) + }) })