From d9d494adaf2114f9519b8ea816126ed4787deffb Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 2 Mar 2022 18:41:41 +0100 Subject: [PATCH] Provide page runtime parsing utils (#34922) * parse runtime config * add test * fix typing * fix lint warning * change branch purging condition * dont fallback to global runtime --- packages/next/build/entries.ts | 61 ++++++++++++++++++- 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 ++ test/unit/fixtures/page-runtime/edge.js | 8 +++ test/unit/fixtures/page-runtime/nodejs.js | 8 +++ test/unit/fixtures/page-runtime/static.js | 3 + test/unit/parse-page-runtime.test.ts | 27 ++++++++ 12 files changed, 147 insertions(+), 32 deletions(-) create mode 100644 packages/next/build/swc/index.d.ts create mode 100644 packages/next/build/swc/options.d.ts 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/fixtures/page-runtime/static.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 15f3ab1a7c1e..45ce053f3ac6 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,60 @@ type Entrypoints = { edgeServer: webpack5.EntryObject } +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')) { + const { body } = await parse(pageContent, { + filename: pageFilePath, + isModule: true, + }) + body.some((node: any) => { + const { type, declaration } = node + const valueNode = declaration?.declarations?.[0] + if (type === 'ExportDeclaration' && valueNode?.id?.value === 'config') { + const props = valueNode.init.properties + const runtimeKeyValue = props.find( + (prop: any) => prop.key.value === 'runtime' + ) + const runtime = runtimeKeyValue?.value?.value + pageRuntime = + runtime === 'edge' || runtime === 'nodejs' ? runtime : pageRuntime + return true + } + return false + }) + } + + return pageRuntime +} + +export async function createPagesRuntimeMapping( + pagesDir: string, + pages: PagesMapping +) { + 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) + if (runtime) { + pagesRuntime[page] = runtime + } + } + }) + return await Promise.all(promises) +} + export function createEntrypoints( pages: PagesMapping, target: 'server' | 'serverless' | 'experimental-serverless-trace', @@ -117,8 +173,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,6 +200,9 @@ export function createEntrypoints( reactRoot: config.experimental.reactRoot ? 'true' : '', } + const globalRuntime = config.experimental.runtime + const edgeRuntime = globalRuntime === 'edge' + Object.keys(pages).forEach((page) => { const absolutePagePath = pages[page] const bundleFile = normalizePagePath(page) diff --git a/packages/next/build/swc/index.d.ts b/packages/next/build/swc/index.d.ts new file mode 100644 index 000000000000..779b53084a45 --- /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): any +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', +} 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/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 new file mode 100644 index 000000000000..701363b27a3f --- /dev/null +++ b/test/unit/parse-page-runtime.test.ts @@ -0,0 +1,27 @@ +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') + }) + + it('should return undefined if no runtime is specified', async () => { + const runtime = await getPageRuntime( + join(fixtureDir, 'page-runtime/static.js') + ) + expect(runtime).toBe(undefined) + }) +})