/
next-flight-server-loader.ts
153 lines (134 loc) · 4.27 KB
/
next-flight-server-loader.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import * as acorn from 'next/dist/compiled/acorn'
import { getRawPageExtensions } from '../../utils'
function isClientComponent(importSource: string, pageExtensions: string[]) {
return new RegExp(`\\.client(\\.(${pageExtensions.join('|')}))?`).test(
importSource
)
}
function isServerComponent(importSource: string, pageExtensions: string[]) {
return new RegExp(`\\.server(\\.(${pageExtensions.join('|')}))?`).test(
importSource
)
}
function isNextComponent(importSource: string) {
return (
importSource.includes('next/link') || importSource.includes('next/image')
)
}
export function isImageImport(importSource: string) {
// TODO: share extension with next/image
// TODO: add other static assets, jpeg -> jpg
return ['jpg', 'jpeg', 'png', 'webp', 'avif'].some((imageExt) =>
importSource.endsWith('.' + imageExt)
)
}
async function parseImportsInfo(
source: string,
imports: Array<string>,
isClientCompilation: boolean,
pageExtensions: string[]
): Promise<{
source: string
defaultExportName: string
}> {
const { body } = acorn.parse(source, {
ecmaVersion: 11,
sourceType: 'module',
}) as any
let transformedSource = ''
let lastIndex = 0
let defaultExportName = 'RSComponent'
for (let i = 0; i < body.length; i++) {
const node = body[i]
switch (node.type) {
case 'ImportDeclaration': {
const importSource = node.source.value
if (!isClientCompilation) {
if (
!(
isClientComponent(importSource, pageExtensions) ||
isNextComponent(importSource) ||
isImageImport(importSource)
)
) {
continue
}
transformedSource += source.substring(
lastIndex,
node.source.start - 1
)
transformedSource += JSON.stringify(`${node.source.value}?flight`)
} else {
// For the client compilation, we skip all modules imports but
// always keep client components in the bundle. All client components
// have to be imported from either server or client components.
if (
!(
isClientComponent(importSource, pageExtensions) ||
isServerComponent(importSource, pageExtensions) ||
// Special cases for Next.js APIs that are considered as client
// components:
isNextComponent(importSource) ||
isImageImport(importSource)
)
) {
continue
}
}
lastIndex = node.source.end
imports.push(`require(${JSON.stringify(importSource)})`)
continue
}
case 'ExportDefaultDeclaration': {
defaultExportName = node.declaration.id.name
break
}
default:
break
}
}
if (!isClientCompilation) {
transformedSource += source.substring(lastIndex)
}
return { source: transformedSource, defaultExportName }
}
export default async function transformSource(
this: any,
source: string
): Promise<string> {
const { client: isClientCompilation, pageExtensions: pageExtensionsJson } =
this.getOptions()
const { resourcePath } = this
const pageExtensions = JSON.parse(pageExtensionsJson)
if (typeof source !== 'string') {
throw new Error('Expected source to have been transformed to a string.')
}
if (resourcePath.includes('/node_modules/')) {
return source
}
const imports: string[] = []
const { source: transformedSource, defaultExportName } =
await parseImportsInfo(
source,
imports,
isClientCompilation,
getRawPageExtensions(pageExtensions)
)
/**
* Server side component module output:
*
* export default function ServerComponent() { ... }
* + export const __rsc_noop__=()=>{ ... }
* + ServerComponent.__next_rsc__=1;
*
* Client side component module output:
*
* The function body of ServerComponent will be removed
*/
const noop = `export const __rsc_noop__=()=>{${imports.join(';')}}`
const defaultExportNoop = isClientCompilation
? `export default function ${defaultExportName}(){}\n${defaultExportName}.__next_rsc__=1;`
: `${defaultExportName}.__next_rsc__=1;`
const transformed = transformedSource + '\n' + noop + '\n' + defaultExportNoop
return transformed
}