Skip to content

Commit

Permalink
Merge branch 'canary' into fix/jsdocs-anotation
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] committed May 20, 2022
2 parents b9e8770 + 6e40fbd commit 28e944b
Show file tree
Hide file tree
Showing 46 changed files with 625 additions and 674 deletions.
10 changes: 1 addition & 9 deletions docs/advanced-features/react-18/server-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,7 @@ export default function Document() {

### `next/app`

If you're using `_app.js`, the usage is the same as [Custom App](/docs/advanced-features/custom-app).
If you're using `_app.server.js` as a server component, see the example below where it only receives the `children` prop as React elements. You can wrap any other client or server components around `children` to customize the layout of your app.

```js
// pages/_app.server.js
export default function App({ children }) {
return children
}
```
The usage of `_app.js` is the same as [Custom App](/docs/advanced-features/custom-app). Using custom app as server component such as `_app.server.js` is not recommended, to keep align with non server components apps for client specific things like global CSS imports.

### Routing

Expand Down
189 changes: 189 additions & 0 deletions packages/next/build/analysis/extract-const-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import type {
ArrayExpression,
BooleanLiteral,
ExportDeclaration,
Identifier,
KeyValueProperty,
Module,
Node,
NullLiteral,
NumericLiteral,
ObjectExpression,
StringLiteral,
VariableDeclaration,
} from '@swc/core'

/**
* Extracts the value of an exported const variable named `exportedName`
* (e.g. "export const config = { runtime: 'edge' }") from swc's AST.
* The value must be one of (or throws UnsupportedValueError):
* - string
* - boolean
* - number
* - null
* - undefined
* - array containing values listed in this list
* - object containing values listed in this list
*
* Throws NoSuchDeclarationError if the declaration is not found.
*/
export function extractExportedConstValue(
module: Module,
exportedName: string
): any {
for (const moduleItem of module.body) {
if (!isExportDeclaration(moduleItem)) {
continue
}

const declaration = moduleItem.declaration
if (!isVariableDeclaration(declaration)) {
continue
}

if (declaration.kind !== 'const') {
continue
}

for (const decl of declaration.declarations) {
if (
isIdentifier(decl.id) &&
decl.id.value === exportedName &&
decl.init
) {
return extractValue(decl.init)
}
}
}

throw new NoSuchDeclarationError()
}

/**
* A wrapper on top of `extractExportedConstValue` that returns undefined
* instead of throwing when the thrown error is known.
*/
export function tryToExtractExportedConstValue(
module: Module,
exportedName: string
) {
try {
return extractExportedConstValue(module, exportedName)
} catch (error) {
if (
error instanceof UnsupportedValueError ||
error instanceof NoSuchDeclarationError
) {
return undefined
}
}
}

function isExportDeclaration(node: Node): node is ExportDeclaration {
return node.type === 'ExportDeclaration'
}

function isVariableDeclaration(node: Node): node is VariableDeclaration {
return node.type === 'VariableDeclaration'
}

function isIdentifier(node: Node): node is Identifier {
return node.type === 'Identifier'
}

function isBooleanLiteral(node: Node): node is BooleanLiteral {
return node.type === 'BooleanLiteral'
}

function isNullLiteral(node: Node): node is NullLiteral {
return node.type === 'NullLiteral'
}

function isStringLiteral(node: Node): node is StringLiteral {
return node.type === 'StringLiteral'
}

function isNumericLiteral(node: Node): node is NumericLiteral {
return node.type === 'NumericLiteral'
}

function isArrayExpression(node: Node): node is ArrayExpression {
return node.type === 'ArrayExpression'
}

function isObjectExpression(node: Node): node is ObjectExpression {
return node.type === 'ObjectExpression'
}

function isKeyValueProperty(node: Node): node is KeyValueProperty {
return node.type === 'KeyValueProperty'
}

class UnsupportedValueError extends Error {}
class NoSuchDeclarationError extends Error {}

function extractValue(node: Node): any {
if (isNullLiteral(node)) {
return null
} else if (isBooleanLiteral(node)) {
// e.g. true / false
return node.value
} else if (isStringLiteral(node)) {
// e.g. "abc"
return node.value
} else if (isNumericLiteral(node)) {
// e.g. 123
return node.value
} else if (isIdentifier(node)) {
switch (node.value) {
case 'undefined':
return undefined
default:
throw new UnsupportedValueError()
}
} else if (isArrayExpression(node)) {
// e.g. [1, 2, 3]
const arr = []
for (const elem of node.elements) {
if (elem) {
if (elem.spread) {
// e.g. [ ...a ]
throw new UnsupportedValueError()
}

arr.push(extractValue(elem.expression))
} else {
// e.g. [1, , 2]
// ^^
arr.push(undefined)
}
}
return arr
} else if (isObjectExpression(node)) {
// e.g. { a: 1, b: 2 }
const obj: any = {}
for (const prop of node.properties) {
if (!isKeyValueProperty(prop)) {
// e.g. { ...a }
throw new UnsupportedValueError()
}

let key
if (isIdentifier(prop.key)) {
// e.g. { a: 1, b: 2 }
key = prop.key.value
} else if (isStringLiteral(prop.key)) {
// e.g. { "a": 1, "b": 2 }
key = prop.key.value
} else {
throw new UnsupportedValueError()
}

obj[key] = extractValue(prop.value)
}

return obj
} else {
throw new UnsupportedValueError()
}
}
119 changes: 119 additions & 0 deletions packages/next/build/analysis/get-page-static-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type { PageRuntime } from '../../server/config-shared'
import type { NextConfig } from '../../server/config-shared'
import { tryToExtractExportedConstValue } from './extract-const-value'
import { parseModule } from './parse-module'
import { promises as fs } from 'fs'

export interface PageStaticInfo {
runtime?: PageRuntime
ssg?: boolean
ssr?: boolean
}

/**
* For a given pageFilePath and nextConfig, if the config supports it, this
* function will read the file and return the runtime that should be used.
* It will look into the file content only if the page *requires* a runtime
* to be specified, that is, when gSSP or gSP is used.
* Related discussion: https://github.com/vercel/next.js/discussions/34179
*/
export async function getPageStaticInfo(params: {
nextConfig: Partial<NextConfig>
pageFilePath: string
isDev?: boolean
}): Promise<PageStaticInfo> {
const { isDev, pageFilePath, nextConfig } = params

const fileContent = (await tryToReadFile(pageFilePath, !isDev)) || ''
if (/runtime|getStaticProps|getServerSideProps/.test(fileContent)) {
const swcAST = await parseModule(pageFilePath, fileContent)
const { ssg, ssr } = checkExports(swcAST)
const config = tryToExtractExportedConstValue(swcAST, 'config') || {}
if (config?.runtime === 'edge') {
return {
runtime: config.runtime,
ssr: ssr,
ssg: ssg,
}
}

// For Node.js runtime, we do static optimization.
if (config?.runtime === 'nodejs') {
return {
runtime: ssr || ssg ? config.runtime : undefined,
ssr: ssr,
ssg: ssg,
}
}

// When the runtime is required because there is ssr or ssg we fallback
if (ssr || ssg) {
return {
runtime: nextConfig.experimental?.runtime,
ssr: ssr,
ssg: ssg,
}
}
}

return { ssr: false, ssg: false }
}

/**
* Receives a parsed AST from SWC and checks if it belongs to a module that
* requires a runtime to be specified. Those are:
* - Modules with `export function getStaticProps | getServerSideProps`
* - Modules with `export { getStaticProps | getServerSideProps } <from ...>`
*/
function checkExports(swcAST: any) {
if (Array.isArray(swcAST?.body)) {
try {
for (const node of swcAST.body) {
if (
node.type === 'ExportDeclaration' &&
node.declaration?.type === 'FunctionDeclaration' &&
['getStaticProps', 'getServerSideProps'].includes(
node.declaration.identifier?.value
)
) {
return {
ssg: node.declaration.identifier.value === 'getStaticProps',
ssr: node.declaration.identifier.value === 'getServerSideProps',
}
}

if (node.type === 'ExportNamedDeclaration') {
const values = node.specifiers.map(
(specifier: any) =>
specifier.type === 'ExportSpecifier' &&
specifier.orig?.type === 'Identifier' &&
specifier.orig?.value
)

return {
ssg: values.some((value: any) =>
['getStaticProps'].includes(value)
),
ssr: values.some((value: any) =>
['getServerSideProps'].includes(value)
),
}
}
}
} catch (err) {}
}

return { ssg: false, ssr: false }
}

async function tryToReadFile(filePath: string, shouldThrow: boolean) {
try {
return await fs.readFile(filePath, {
encoding: 'utf8',
})
} catch (error) {
if (shouldThrow) {
throw error
}
}
}
15 changes: 15 additions & 0 deletions packages/next/build/analysis/parse-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import LRUCache from 'next/dist/compiled/lru-cache'
import { withPromiseCache } from '../../lib/with-promise-cache'
import { createHash } from 'crypto'
import { parse } from '../swc'

/**
* Parses a module with SWC using an LRU cache where the parsed module will
* be indexed by a sha of its content holding up to 500 entries.
*/
export const parseModule = withPromiseCache(
new LRUCache<string, any>({ max: 500 }),
async (filename: string, content: string) =>
parse(content, { isModule: 'unknown', filename }).catch(() => null),
(_, content) => createHash('sha1').update(content).digest('hex')
)
Loading

0 comments on commit 28e944b

Please sign in to comment.