From d0b280f5bac5ba0ac9ec5b506f1cb7be3fdb5515 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 16 Feb 2022 20:32:29 +0100 Subject: [PATCH 1/6] parse runtime config --- examples/hello-world/pages/index.js | 4 ++ packages/next/build/entries.ts | 67 ++++++++++++++++++- packages/next/build/swc/index.d.ts | 7 ++ packages/next/build/swc/index.js | 4 +- packages/next/build/swc/options.d.ts | 7 ++ packages/next/build/swc/options.js | 29 +++++--- .../loaders/next-flight-client-loader.ts | 11 +-- .../loaders/next-flight-server-loader.ts | 10 +-- test/integration/react-18/app/pages/index.js | 4 ++ 9 files changed, 110 insertions(+), 33 deletions(-) create mode 100644 packages/next/build/swc/index.d.ts create mode 100644 packages/next/build/swc/options.d.ts diff --git a/examples/hello-world/pages/index.js b/examples/hello-world/pages/index.js index dd2c3af8cab8..88fdc555b860 100644 --- a/examples/hello-world/pages/index.js +++ b/examples/hello-world/pages/index.js @@ -10,3 +10,7 @@ export default function IndexPage() { ) } + +export const config = { + runtime: 'edge', +} diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 15f3ab1a7c1e..9f89670e2cfb 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -1,3 +1,4 @@ +import fs from 'fs' import chalk from 'next/dist/compiled/chalk' import { posix, join } from 'path' import { stringify } from 'querystring' @@ -12,6 +13,7 @@ import { ClientPagesLoaderOptions } from './webpack/loaders/next-client-pages-lo import { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader' import { LoadedEnvFiles } from '@next/env' import { NextConfigComplete } from '../server/config-shared' +import { parse } from '../build/swc' import { isCustomErrorPage, isFlightPage, isReservedPage } from './utils' import { ssrEntries } from './webpack/plugins/middleware-plugin' import type { webpack5 } from 'next/dist/compiled/webpack/webpack' @@ -101,6 +103,64 @@ type Entrypoints = { edgeServer: webpack5.EntryObject } +export async function getPageRuntime( + pageFilePath: string, + globalRuntime: string | undefined +) { + let pageRuntime: string | undefined = undefined + const pageContent = await fs.promises.readFile(pageFilePath, { + encoding: 'utf8', + }) + // branch prunes for entry page without runtime option + if (pageContent.includes('export const config =')) { + const { body } = await parse(pageContent, { + filename: pageFilePath, + isModule: true, + }) + body.filter((node: any) => { + const { type, declaration } = node + const valueNode = declaration?.declarations?.[0] + if (type === 'ExportDeclaration' && valueNode?.id?.value === 'config') { + const props = valueNode.init.properties + console.log(props) + const runtimeKeyValue = props.find( + (prop: any) => prop.key.value === 'runtime' + ) + const runtime = runtimeKeyValue?.value?.value + pageRuntime = + runtime === 'edge' || runtime === 'nodejs' ? runtime : pageRuntime + } + }) + } + + return pageRuntime || globalRuntime +} + +export async function createPagesRuntimeMapping( + pagesDir: string, + pages: PagesMapping, + config: NextConfigComplete +) { + const globalRuntime = config.experimental.runtime + const pagesRuntime: Record = {} + + const promises = Object.keys(pages).map(async (page) => { + const absolutePagePath = pages[page] + const isReserved = isReservedPage(page) + if (!isReserved) { + const pageFilePath = join( + pagesDir, + absolutePagePath.replace(PAGES_DIR_ALIAS, '') + ) + const runtime = await getPageRuntime(pageFilePath, globalRuntime) + if (runtime) { + pagesRuntime[page] = runtime + } + } + }) + return await Promise.all(promises) +} + export function createEntrypoints( pages: PagesMapping, target: 'server' | 'serverless' | 'experimental-serverless-trace', @@ -117,8 +177,6 @@ export function createEntrypoints( Object.keys(config.publicRuntimeConfig).length > 0 || Object.keys(config.serverRuntimeConfig).length > 0 - const edgeRuntime = config.experimental.runtime === 'edge' - const defaultServerlessOptions = { absoluteAppPath: pages['/_app'], absoluteDocumentPath: pages['/_document'], @@ -146,7 +204,10 @@ export function createEntrypoints( reactRoot: config.experimental.reactRoot ? 'true' : '', } - Object.keys(pages).forEach((page) => { + const globalRuntime = config.experimental.runtime + const edgeRuntime = globalRuntime === 'edge' + + Object.keys(pages).map((page) => { const absolutePagePath = pages[page] const bundleFile = normalizePagePath(page) const isApiRoute = page.match(API_ROUTE) diff --git a/packages/next/build/swc/index.d.ts b/packages/next/build/swc/index.d.ts new file mode 100644 index 000000000000..5ad9312eb520 --- /dev/null +++ b/packages/next/build/swc/index.d.ts @@ -0,0 +1,7 @@ +export function isWasm(): Promise +export function transform(src: string, options: any): Promise +export function transformSync(src: string, options: any): string +export function minify(src: string, options: any): Promise +export function minifySync(src: string, options: any): string +export function bundle(options: any): Promise +export function parse(src: string, options: any): any diff --git a/packages/next/build/swc/index.js b/packages/next/build/swc/index.js index 8998782147c3..0f7dc5466a1c 100644 --- a/packages/next/build/swc/index.js +++ b/packages/next/build/swc/index.js @@ -1,6 +1,7 @@ import { platform, arch } from 'os' import { platformArchTriples } from 'next/dist/compiled/@napi-rs/triples' import * as Log from '../output/log' +import { getParserOptions } from './options' const ArchName = arch() const PlatformName = platform() @@ -229,5 +230,6 @@ export async function bundle(options) { export async function parse(src, options) { let bindings = loadBindingsSync() - return bindings.parse(src, options).then((astStr) => JSON.parse(astStr)) + let parserOptions = getParserOptions(options) + return bindings.parse(src, parserOptions).then((astStr) => JSON.parse(astStr)) } diff --git a/packages/next/build/swc/options.d.ts b/packages/next/build/swc/options.d.ts new file mode 100644 index 000000000000..35c16b98e565 --- /dev/null +++ b/packages/next/build/swc/options.d.ts @@ -0,0 +1,7 @@ +export function getParserOptions(options: { + filename: string + jsConfig?: any + [key: string]: any +}): any +export function getJestSWCOptions(...args: any[]): any +export function getLoaderSWCOptions(...args: any[]): any diff --git a/packages/next/build/swc/options.js b/packages/next/build/swc/options.js index 2d2cbee22fa8..1d763b27d11f 100644 --- a/packages/next/build/swc/options.js +++ b/packages/next/build/swc/options.js @@ -5,7 +5,23 @@ const regeneratorRuntimePath = require.resolve( 'next/dist/compiled/regenerator-runtime' ) -export function getBaseSWCOptions({ +export function getParserOptions({ filename, jsConfig, ...rest }) { + const isTSFile = filename.endsWith('.ts') + const isTypeScript = isTSFile || filename.endsWith('.tsx') + const enableDecorators = Boolean( + jsConfig?.compilerOptions?.experimentalDecorators + ) + return { + ...rest, + syntax: isTypeScript ? 'typescript' : 'ecmascript', + dynamicImport: true, + decorators: enableDecorators, + // Exclude regular TypeScript files from React transformation to prevent e.g. generic parameters and angle-bracket type assertion from being interpreted as JSX tags. + [isTypeScript ? 'tsx' : 'jsx']: isTSFile ? false : true, + } +} + +function getBaseSWCOptions({ filename, jest, development, @@ -15,8 +31,7 @@ export function getBaseSWCOptions({ resolvedBaseUrl, jsConfig, }) { - const isTSFile = filename.endsWith('.ts') - const isTypeScript = isTSFile || filename.endsWith('.tsx') + const parserConfig = getParserOptions({ filename, jsConfig }) const paths = jsConfig?.compilerOptions?.paths const enableDecorators = Boolean( jsConfig?.compilerOptions?.experimentalDecorators @@ -32,13 +47,7 @@ export function getBaseSWCOptions({ paths, } : {}), - parser: { - syntax: isTypeScript ? 'typescript' : 'ecmascript', - dynamicImport: true, - decorators: enableDecorators, - // Exclude regular TypeScript files from React transformation to prevent e.g. generic parameters and angle-bracket type assertion from being interpreted as JSX tags. - [isTypeScript ? 'tsx' : 'jsx']: isTSFile ? false : true, - }, + parser: parserConfig, transform: { // Enables https://github.com/swc-project/swc/blob/0359deb4841be743d73db4536d4a22ac797d7f65/crates/swc_ecma_ext_transforms/src/jest.rs diff --git a/packages/next/build/webpack/loaders/next-flight-client-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-loader.ts index c7cc6c4badea..afe614e85906 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-loader.ts @@ -5,11 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -// TODO: add ts support for next-swc api -// @ts-ignore import { parse } from '../../swc' -// @ts-ignore -import { getBaseSWCOptions } from '../../swc/options' function addExportNames(names: string[], node: any) { switch (node.type) { @@ -48,13 +44,8 @@ async function parseExportNamesInto( transformedSource: string, names: Array ): Promise { - const opts = getBaseSWCOptions({ - filename: resourcePath, - globalWindow: true, - }) - const { body } = await parse(transformedSource, { - ...opts.jsc.parser, + filename: resourcePath, isModule: true, }) for (let i = 0; i < body.length; i++) { diff --git a/packages/next/build/webpack/loaders/next-flight-server-loader.ts b/packages/next/build/webpack/loaders/next-flight-server-loader.ts index fdaf07b21277..25b23bf5d47c 100644 --- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts @@ -1,8 +1,4 @@ -// TODO: add ts support for next-swc api -// @ts-ignore import { parse } from '../../swc' -// @ts-ignore -import { getBaseSWCOptions } from '../../swc/options' import { getRawPageExtensions } from '../../utils' const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif'] @@ -46,11 +42,7 @@ async function parseImportsInfo({ source: string defaultExportName: string }> { - const opts = getBaseSWCOptions({ - filename: resourcePath, - globalWindow: isClientCompilation, - }) - const ast = await parse(source, { ...opts.jsc.parser, isModule: true }) + const ast = await parse(source, { filename: resourcePath, isModule: true }) const { body } = ast let transformedSource = '' let lastIndex = 0 diff --git a/test/integration/react-18/app/pages/index.js b/test/integration/react-18/app/pages/index.js index beef70aff7f4..9ce5416ab738 100644 --- a/test/integration/react-18/app/pages/index.js +++ b/test/integration/react-18/app/pages/index.js @@ -19,3 +19,7 @@ export default function Index() { ) } + +export const config = { + runtime: 'edge', +} From 4c8c4039e37b6884681140c8eedabbbb54b37ab3 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 1 Mar 2022 17:01:16 +0100 Subject: [PATCH 2/6] add test --- packages/next/build/entries.ts | 1 - test/unit/fixtures/page-runtime/edge.js | 8 ++++++++ test/unit/fixtures/page-runtime/nodejs.js | 8 ++++++++ test/unit/parse-page-runtime.test.ts | 20 ++++++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 test/unit/fixtures/page-runtime/edge.js create mode 100644 test/unit/fixtures/page-runtime/nodejs.js create mode 100644 test/unit/parse-page-runtime.test.ts diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 9f89670e2cfb..499c3ed9837b 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -122,7 +122,6 @@ export async function getPageRuntime( const valueNode = declaration?.declarations?.[0] if (type === 'ExportDeclaration' && valueNode?.id?.value === 'config') { const props = valueNode.init.properties - console.log(props) const runtimeKeyValue = props.find( (prop: any) => prop.key.value === 'runtime' ) diff --git a/test/unit/fixtures/page-runtime/edge.js b/test/unit/fixtures/page-runtime/edge.js new file mode 100644 index 000000000000..b5d1e0278d2c --- /dev/null +++ b/test/unit/fixtures/page-runtime/edge.js @@ -0,0 +1,8 @@ +export default function Edge() { + return 'edge' +} + +export const config = { + runtime: 'edge', + amp: false, +} diff --git a/test/unit/fixtures/page-runtime/nodejs.js b/test/unit/fixtures/page-runtime/nodejs.js new file mode 100644 index 000000000000..e2e31db3a9fd --- /dev/null +++ b/test/unit/fixtures/page-runtime/nodejs.js @@ -0,0 +1,8 @@ +export default function Nodejs() { + return 'nodejs' +} + +export const config = { + amp: false, + runtime: 'nodejs', +} diff --git a/test/unit/parse-page-runtime.test.ts b/test/unit/parse-page-runtime.test.ts new file mode 100644 index 000000000000..ee8a60b46e91 --- /dev/null +++ b/test/unit/parse-page-runtime.test.ts @@ -0,0 +1,20 @@ +import { getPageRuntime } from 'next/dist/build/entries' +import { join } from 'path' + +const fixtureDir = join(__dirname, 'fixtures') + +describe('parse page runtime config', () => { + it('should parse nodejs runtime correctly', async () => { + const runtime = await getPageRuntime( + join(fixtureDir, 'page-runtime/nodejs.js') + ) + expect(runtime).toBe('nodejs') + }) + + it('should parse edge runtime correctly', async () => { + const runtime = await getPageRuntime( + join(fixtureDir, 'page-runtime/edge.js') + ) + expect(runtime).toBe('edge') + }) +}) From 3775ccc96c17afa22c661a04c37a91a0370c9454 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 1 Mar 2022 17:15:29 +0100 Subject: [PATCH 3/6] fix typing --- packages/next/build/entries.ts | 2 +- packages/next/build/swc/index.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 499c3ed9837b..17a38a1394ed 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -105,7 +105,7 @@ type Entrypoints = { export async function getPageRuntime( pageFilePath: string, - globalRuntime: string | undefined + globalRuntime?: string ) { let pageRuntime: string | undefined = undefined const pageContent = await fs.promises.readFile(pageFilePath, { diff --git a/packages/next/build/swc/index.d.ts b/packages/next/build/swc/index.d.ts index 5ad9312eb520..779b53084a45 100644 --- a/packages/next/build/swc/index.d.ts +++ b/packages/next/build/swc/index.d.ts @@ -1,6 +1,6 @@ export function isWasm(): Promise -export function transform(src: string, options: any): Promise -export function transformSync(src: string, options: any): string +export function transform(src: string, options?: any): Promise +export function transformSync(src: string, options?: any): any export function minify(src: string, options: any): Promise export function minifySync(src: string, options: any): string export function bundle(options: any): Promise From 2afeee515c46c70e3ff0d52490d16cbfbc6be71d Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 1 Mar 2022 17:36:07 +0100 Subject: [PATCH 4/6] fix lint warning --- packages/next/build/entries.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 17a38a1394ed..c6d3bc0a3485 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -117,7 +117,7 @@ export async function getPageRuntime( filename: pageFilePath, isModule: true, }) - body.filter((node: any) => { + body.forEach((node: any) => { const { type, declaration } = node const valueNode = declaration?.declarations?.[0] if (type === 'ExportDeclaration' && valueNode?.id?.value === 'config') { @@ -206,7 +206,7 @@ export function createEntrypoints( const globalRuntime = config.experimental.runtime const edgeRuntime = globalRuntime === 'edge' - Object.keys(pages).map((page) => { + Object.keys(pages).forEach((page) => { const absolutePagePath = pages[page] const bundleFile = normalizePagePath(page) const isApiRoute = page.match(API_ROUTE) From ddb6613fed79792dbffe55c62234b17db55e5ba0 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 2 Mar 2022 14:01:59 +0100 Subject: [PATCH 5/6] change branch purging condition --- examples/hello-world/pages/index.js | 4 ---- packages/next/build/entries.ts | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/hello-world/pages/index.js b/examples/hello-world/pages/index.js index 88fdc555b860..dd2c3af8cab8 100644 --- a/examples/hello-world/pages/index.js +++ b/examples/hello-world/pages/index.js @@ -10,7 +10,3 @@ export default function IndexPage() { ) } - -export const config = { - runtime: 'edge', -} diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index c6d3bc0a3485..43be2a3743e3 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -112,7 +112,7 @@ export async function getPageRuntime( encoding: 'utf8', }) // branch prunes for entry page without runtime option - if (pageContent.includes('export const config =')) { + if (pageContent.includes('runtime:')) { const { body } = await parse(pageContent, { filename: pageFilePath, isModule: true, From 4ddb0e3baff08fa3b4be064ef8a1b5be841a2c7b Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 2 Mar 2022 16:52:41 +0100 Subject: [PATCH 6/6] dont fallback to global runtime --- packages/next/build/entries.ts | 19 ++++++++----------- test/unit/fixtures/page-runtime/static.js | 3 +++ test/unit/parse-page-runtime.test.ts | 7 +++++++ 3 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 test/unit/fixtures/page-runtime/static.js diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 43be2a3743e3..45ce053f3ac6 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -103,21 +103,18 @@ type Entrypoints = { edgeServer: webpack5.EntryObject } -export async function getPageRuntime( - pageFilePath: string, - globalRuntime?: string -) { +export async function getPageRuntime(pageFilePath: string) { let pageRuntime: string | undefined = undefined const pageContent = await fs.promises.readFile(pageFilePath, { encoding: 'utf8', }) // branch prunes for entry page without runtime option - if (pageContent.includes('runtime:')) { + if (pageContent.includes('runtime')) { const { body } = await parse(pageContent, { filename: pageFilePath, isModule: true, }) - body.forEach((node: any) => { + body.some((node: any) => { const { type, declaration } = node const valueNode = declaration?.declarations?.[0] if (type === 'ExportDeclaration' && valueNode?.id?.value === 'config') { @@ -128,19 +125,19 @@ export async function getPageRuntime( const runtime = runtimeKeyValue?.value?.value pageRuntime = runtime === 'edge' || runtime === 'nodejs' ? runtime : pageRuntime + return true } + return false }) } - return pageRuntime || globalRuntime + return pageRuntime } export async function createPagesRuntimeMapping( pagesDir: string, - pages: PagesMapping, - config: NextConfigComplete + pages: PagesMapping ) { - const globalRuntime = config.experimental.runtime const pagesRuntime: Record = {} const promises = Object.keys(pages).map(async (page) => { @@ -151,7 +148,7 @@ export async function createPagesRuntimeMapping( pagesDir, absolutePagePath.replace(PAGES_DIR_ALIAS, '') ) - const runtime = await getPageRuntime(pageFilePath, globalRuntime) + const runtime = await getPageRuntime(pageFilePath) if (runtime) { pagesRuntime[page] = runtime } diff --git a/test/unit/fixtures/page-runtime/static.js b/test/unit/fixtures/page-runtime/static.js new file mode 100644 index 000000000000..8e8c073a82fc --- /dev/null +++ b/test/unit/fixtures/page-runtime/static.js @@ -0,0 +1,3 @@ +export default function Static() { + return 'static' +} diff --git a/test/unit/parse-page-runtime.test.ts b/test/unit/parse-page-runtime.test.ts index ee8a60b46e91..701363b27a3f 100644 --- a/test/unit/parse-page-runtime.test.ts +++ b/test/unit/parse-page-runtime.test.ts @@ -17,4 +17,11 @@ describe('parse page runtime config', () => { ) expect(runtime).toBe('edge') }) + + it('should return undefined if no runtime is specified', async () => { + const runtime = await getPageRuntime( + join(fixtureDir, 'page-runtime/static.js') + ) + expect(runtime).toBe(undefined) + }) })