-
Notifications
You must be signed in to change notification settings - Fork 26.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
760750d
commit 32e1dd9
Showing
12 changed files
with
433 additions
and
179 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
) |
Oops, something went wrong.