Skip to content

Commit

Permalink
Provide page runtime parsing utils (#34922)
Browse files Browse the repository at this point in the history
* parse runtime config

* add test

* fix typing

* fix lint warning

* change branch purging condition

* dont fallback to global runtime
  • Loading branch information
huozhi committed Mar 2, 2022
1 parent 7b2fb70 commit d9d494a
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 32 deletions.
61 changes: 59 additions & 2 deletions 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'
Expand All @@ -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'
Expand Down Expand Up @@ -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<string, string> = {}

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',
Expand All @@ -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'],
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions packages/next/build/swc/index.d.ts
@@ -0,0 +1,7 @@
export function isWasm(): Promise<boolean>
export function transform(src: string, options?: any): Promise<any>
export function transformSync(src: string, options?: any): any
export function minify(src: string, options: any): Promise<string>
export function minifySync(src: string, options: any): string
export function bundle(options: any): Promise<any>
export function parse(src: string, options: any): any
4 changes: 3 additions & 1 deletion 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()
Expand Down Expand Up @@ -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))
}
7 changes: 7 additions & 0 deletions 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
29 changes: 19 additions & 10 deletions packages/next/build/swc/options.js
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down
11 changes: 1 addition & 10 deletions packages/next/build/webpack/loaders/next-flight-client-loader.ts
Expand Up @@ -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) {
Expand Down Expand Up @@ -48,13 +44,8 @@ async function parseExportNamesInto(
transformedSource: string,
names: Array<string>
): Promise<void> {
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++) {
Expand Down
@@ -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']
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions test/integration/react-18/app/pages/index.js
Expand Up @@ -19,3 +19,7 @@ export default function Index() {
</div>
)
}

export const config = {
runtime: 'edge',
}
8 changes: 8 additions & 0 deletions test/unit/fixtures/page-runtime/edge.js
@@ -0,0 +1,8 @@
export default function Edge() {
return 'edge'
}

export const config = {
runtime: 'edge',
amp: false,
}
8 changes: 8 additions & 0 deletions test/unit/fixtures/page-runtime/nodejs.js
@@ -0,0 +1,8 @@
export default function Nodejs() {
return 'nodejs'
}

export const config = {
amp: false,
runtime: 'nodejs',
}
3 changes: 3 additions & 0 deletions test/unit/fixtures/page-runtime/static.js
@@ -0,0 +1,3 @@
export default function Static() {
return 'static'
}
27 changes: 27 additions & 0 deletions 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)
})
})

0 comments on commit d9d494a

Please sign in to comment.