Skip to content

Commit

Permalink
Add support for browserslist and legacyBrowsers experimental option (#…
Browse files Browse the repository at this point in the history
…36584)

Implements the first part of #33227

- Applies browserslist to JS transforms when `experimental.browsersListForSwc` is enabled. 
- You don't have to use browserslist, there's also `legacyBrowsers: false` which will be the new default in Next.js 13. See #33227 for which browsers and why. `legacyBrowsers` requires `browsersListForSwc: true` to function until it is the default. 

```js
module.exports = {
  experimental: {
    legacyBrowsers: false,
    browsersListForSwc: true,
  }
}
```

I only implemented the JS part of the RFC, the CSS part should be handled in a follow-up PR.



## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [x] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `yarn lint`


Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
  • Loading branch information
timneutkens and ijjk committed May 17, 2022
1 parent c947abb commit fe3d6b7
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 6 deletions.
8 changes: 8 additions & 0 deletions packages/next/build/swc/options.js
Expand Up @@ -192,6 +192,7 @@ export function getLoaderSWCOptions({
hasReactRefresh,
nextConfig,
jsConfig,
supportedBrowsers,
// This is not passed yet as "paths" resolving is handled by webpack currently.
// resolvedBaseUrl,
}) {
Expand Down Expand Up @@ -242,6 +243,13 @@ export function getLoaderSWCOptions({
isServer,
pagesDir,
isPageFile,
...(supportedBrowsers && supportedBrowsers.length > 0
? {
env: {
targets: supportedBrowsers,
},
}
: {}),
}
}
}
29 changes: 25 additions & 4 deletions packages/next/build/webpack-config.ts
Expand Up @@ -25,6 +25,7 @@ import {
REACT_LOADABLE_MANIFEST,
SERVERLESS_DIRECTORY,
SERVER_DIRECTORY,
MODERN_BROWSERSLIST_TARGET,
} from '../shared/lib/constants'
import { execOnce } from '../shared/lib/utils'
import { NextConfigComplete } from '../server/config-shared'
Expand Down Expand Up @@ -63,16 +64,31 @@ const watchOptions = Object.freeze({

function getSupportedBrowsers(
dir: string,
isDevelopment: boolean
isDevelopment: boolean,
config: NextConfigComplete
): string[] | undefined {
let browsers: any
try {
browsers = browserslist.loadConfig({
const browsersListConfig = browserslist.loadConfig({
path: dir,
env: isDevelopment ? 'development' : 'production',
})
// Running `browserslist` resolves `extends` and other config features into a list of browsers
if (browsersListConfig && browsersListConfig.length > 0) {
browsers = browserslist(browsersListConfig)
}
} catch {}
return browsers

// When user has browserslist use that target
if (browsers && browsers.length > 0) {
return browsers
}

// When user does not have browserslist use the default target
// When `experimental.legacyBrowsers: false` the modern default is used
return config.experimental.legacyBrowsers
? undefined
: MODERN_BROWSERSLIST_TARGET
}

type ExcludesFalse = <T>(x: T | false) => x is T
Expand Down Expand Up @@ -339,7 +355,8 @@ export default async function getBaseWebpackConfig(
dir,
config
)
const supportedBrowsers = await getSupportedBrowsers(dir, dev)
const supportedBrowsers = await getSupportedBrowsers(dir, dev, config)

const hasRewrites =
rewrites.beforeFiles.length > 0 ||
rewrites.afterFiles.length > 0 ||
Expand Down Expand Up @@ -466,6 +483,9 @@ export default async function getBaseWebpackConfig(
fileReading: config.experimental.swcFileReading,
nextConfig: config,
jsConfig,
supportedBrowsers: config.experimental.browsersListForSwc
? supportedBrowsers
: undefined,
},
}
: {
Expand Down Expand Up @@ -1783,6 +1803,7 @@ export default async function getBaseWebpackConfig(
relay: config.compiler?.relay,
emotion: config.experimental?.emotion,
modularizeImports: config.experimental?.modularizeImports,
legacyBrowsers: config.experimental?.legacyBrowsers,
})

const cache: any = {
Expand Down
11 changes: 9 additions & 2 deletions packages/next/build/webpack/loaders/next-swc-loader.js
Expand Up @@ -36,8 +36,14 @@ async function loaderTransform(parentTrace, source, inputSourceMap) {

let loaderOptions = this.getOptions() || {}

const { isServer, pagesDir, hasReactRefresh, nextConfig, jsConfig } =
loaderOptions
const {
isServer,
pagesDir,
hasReactRefresh,
nextConfig,
jsConfig,
supportedBrowsers,
} = loaderOptions
const isPageFile = filename.startsWith(pagesDir)

const swcOptions = getLoaderSWCOptions({
Expand All @@ -49,6 +55,7 @@ async function loaderTransform(parentTrace, source, inputSourceMap) {
hasReactRefresh,
nextConfig,
jsConfig,
supportedBrowsers,
})

const programmaticOptions = {
Expand Down
5 changes: 5 additions & 0 deletions packages/next/server/config-shared.ts
Expand Up @@ -79,6 +79,8 @@ export interface NextJsWebpackConfig {
}

export interface ExperimentalConfig {
legacyBrowsers?: boolean
browsersListForSwc?: boolean
manualClientBasePath?: boolean
newNextLinkBehavior?: boolean
disablePostcssPresetEnv?: boolean
Expand Down Expand Up @@ -473,6 +475,9 @@ export const defaultConfig: NextConfig = {
staticPageGenerationTimeout: 60,
swcMinify: false,
experimental: {
// TODO: change default in next major release (current v12.1.5)
legacyBrowsers: true,
browsersListForSwc: false,
// TODO: change default in next major release (current v12.1.5)
newNextLinkBehavior: false,
cpus: Math.max(
Expand Down
7 changes: 7 additions & 0 deletions packages/next/shared/lib/constants.ts
Expand Up @@ -26,6 +26,13 @@ export const CLIENT_PUBLIC_FILES_PATH = 'public'
export const CLIENT_STATIC_FILES_PATH = 'static'
export const CLIENT_STATIC_FILES_RUNTIME = 'runtime'
export const STRING_LITERAL_DROP_BUNDLE = '__NEXT_DROP_CLIENT_FILE__'
export const MODERN_BROWSERSLIST_TARGET = [
'chrome 61',
'edge 16',
'firefox 60',
'opera 48',
'safari 11',
]
export const NEXT_BUILTIN_DOCUMENT = '__NEXT_BUILTIN_DOCUMENT__'

// server/middleware-flight-manifest.js
Expand Down
22 changes: 22 additions & 0 deletions test/e2e/browserslist/app/pages/index.js
@@ -0,0 +1,22 @@
import React, { useEffect } from 'react'
const helloWorld = 'hello world'

class MyComp extends React.Component {
render() {
return <h1>Hello World</h1>
}
}

export default function Page() {
useEffect(() => {
;(async () => {
console.log(helloWorld)
})()
}, [])
return (
<>
{helloWorld}
<MyComp />
</>
)
}
53 changes: 53 additions & 0 deletions test/e2e/browserslist/browserslist.test.ts
@@ -0,0 +1,53 @@
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import { renderViaHTTP, fetchViaHTTP } from 'next-test-utils'
import path from 'path'
import cheerio from 'cheerio'
const appDir = path.join(__dirname, 'app')

describe('Browserslist', () => {
let next: NextInstance

beforeAll(async () => {
next = await createNext({
files: {
pages: new FileRef(path.join(appDir, 'pages')),
'.browserslistrc': 'Chrome 73',
},
nextConfig: {
experimental: {
browsersListForSwc: true,
},
},
dependencies: {},
})
})
afterAll(() => next.destroy())

it('should apply browserslist target', async () => {
const html = await renderViaHTTP(next.url, '/')
const $ = cheerio.load(html)

let finished = false
await Promise.all(
$('script')
.toArray()
.map(async (el) => {
const src = $(el).attr('src')
if (!src) return
if (src.includes('/index')) {
const code = await fetchViaHTTP(next.url, src).then((res) =>
res.text()
)

const isDev = (global as any).isNextDev
expect(
code.includes(isDev ? 'async ()=>{' : 'async()=>{console.log(')
).toBe(true)
finished = true
}
})
)
expect(finished).toBe(true)
})
})
53 changes: 53 additions & 0 deletions test/e2e/browserslist/legacybrowsers-false.test.ts
@@ -0,0 +1,53 @@
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import { renderViaHTTP, fetchViaHTTP } from 'next-test-utils'
import path from 'path'
import cheerio from 'cheerio'
const appDir = path.join(__dirname, 'app')

describe('legacyBrowsers: false', () => {
let next: NextInstance

beforeAll(async () => {
next = await createNext({
files: {
pages: new FileRef(path.join(appDir, 'pages')),
},
nextConfig: {
experimental: {
legacyBrowsers: false,
browsersListForSwc: true,
},
},
dependencies: {},
})
})
afterAll(() => next.destroy())

it('should apply with legacyBrowsers: false correctly', async () => {
const html = await renderViaHTTP(next.url, '/')
const $ = cheerio.load(html)

let finished = false
await Promise.all(
$('script')
.toArray()
.map(async (el) => {
const src = $(el).attr('src')
if (!src) return
if (src.includes('/index')) {
const code = await fetchViaHTTP(next.url, src).then((res) =>
res.text()
)

const isDev = (global as any).isNextDev
expect(
code.includes(isDev ? 'async ()=>{' : 'async()=>{console.log(')
).toBe(true)
finished = true
}
})
)
expect(finished).toBe(true)
})
})
52 changes: 52 additions & 0 deletions test/e2e/browserslist/legacybrowsers-true.test.ts
@@ -0,0 +1,52 @@
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import { renderViaHTTP, fetchViaHTTP } from 'next-test-utils'
import path from 'path'
import cheerio from 'cheerio'
const appDir = path.join(__dirname, 'app')

describe('legacyBrowsers: true', () => {
let next: NextInstance

beforeAll(async () => {
next = await createNext({
files: {
pages: new FileRef(path.join(appDir, 'pages')),
},
nextConfig: {
experimental: {
browsersListForSwc: true,
},
},
dependencies: {},
})
})
afterAll(() => next.destroy())

it('should apply legacyBrowsers: true by default', async () => {
const html = await renderViaHTTP(next.url, '/')
const $ = cheerio.load(html)

let finished = false
await Promise.all(
$('script')
.toArray()
.map(async (el) => {
const src = $(el).attr('src')
if (!src) return
if (src.includes('/index')) {
const code = await fetchViaHTTP(next.url, src).then((res) =>
res.text()
)

const isDev = (global as any).isNextDev
expect(
code.includes(isDev ? 'async ()=>{' : 'async()=>{console.log(')
).toBe(false)
finished = true
}
})
)
expect(finished).toBe(true)
})
})

0 comments on commit fe3d6b7

Please sign in to comment.