Skip to content

Commit

Permalink
Support export all syntax in client components (vercel#36027)
Browse files Browse the repository at this point in the history
This PR adds the support of the `export * from './foo'` syntax in client components.

## Bug

- [ ] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `yarn lint`
  • Loading branch information
shuding authored and colinhacks committed Apr 14, 2022
1 parent 8937abf commit a326936
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 18 deletions.
68 changes: 51 additions & 17 deletions packages/next/build/webpack/loaders/next-flight-client-loader.ts
Expand Up @@ -5,10 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/

import { promisify } from 'util'

import { parse } from '../../swc'
import { buildExports } from './utils'

function addExportNames(names: string[], node: any) {
if (!node) return
switch (node.type) {
case 'Identifier':
names.push(node.value)
Expand Down Expand Up @@ -40,20 +43,33 @@ function addExportNames(names: string[], node: any) {
}
}

async function parseModuleInfo(
async function collectExports(
resourcePath: string,
transformedSource: string,
names: Array<string>
): Promise<void> {
{
resolve,
loadModule,
}: {
resolve: (request: string) => Promise<string>
loadModule: (request: string) => Promise<string>
}
) {
const names: string[] = []

// Next.js built-in client components
if (/[\\/]next[\\/](link|image)\.js$/.test(resourcePath)) {
names.push('default')
}

const { body } = await parse(transformedSource, {
filename: resourcePath,
isModule: 'unknown',
})

for (let i = 0; i < body.length; i++) {
const node = body[i]

switch (node.type) {
// TODO: support export * from module path
// case 'ExportAllDeclaration':
case 'ExportDefaultExpression':
case 'ExportDefaultDeclaration':
names.push('default')
Expand All @@ -69,10 +85,10 @@ async function parseModuleInfo(
addExportNames(names, node.declaration.id)
}
}
if (node.specificers) {
const specificers = node.specificers
for (let j = 0; j < specificers.length; j++) {
addExportNames(names, specificers[j].exported)
if (node.specifiers) {
const specifiers = node.specifiers
for (let j = 0; j < specifiers.length; j++) {
addExportNames(names, specifiers[j].exported)
}
}
break
Expand All @@ -96,30 +112,48 @@ async function parseModuleInfo(
}
break
}
case 'ExportAllDeclaration':
if (node.exported) {
addExportNames(names, node.exported)
break
}

const reexportedFromResourcePath = await resolve(node.source.value)
const reexportedFromResourceSource = await loadModule(
reexportedFromResourcePath
)

names.push(
...(await collectExports(
reexportedFromResourcePath,
reexportedFromResourceSource,
{ resolve, loadModule }
))
)
continue
default:
break
}
}

return names
}

export default async function transformSource(
this: any,
source: string
): Promise<string> {
const { resourcePath } = this
const { resourcePath, resolve, loadModule, context } = this

const transformedSource = source
if (typeof transformedSource !== 'string') {
throw new Error('Expected source to have been transformed to a string.')
}

const names: string[] = []
await parseModuleInfo(resourcePath, transformedSource, names)

// Next.js built-in client components
if (/[\\/]next[\\/](link|image)\.js$/.test(resourcePath)) {
names.push('default')
}
const names = await collectExports(resourcePath, transformedSource, {
resolve: (...args) => promisify(resolve)(context, ...args),
loadModule: promisify(loadModule),
})

const moduleRefDef =
"const MODULE_REFERENCE = Symbol.for('react.module.reference');\n"
Expand Down
Expand Up @@ -67,7 +67,7 @@ export class FlightManifestPlugin {
const isClientComponent = createClientComponentFilter(this.pageExtensions)
compilation.chunkGroups.forEach((chunkGroup: any) => {
function recordModule(id: string, _chunk: any, mod: any) {
const resource = mod.resource?.replace(/\?__sc_client__$/, '')
const resource = mod.resource

// TODO: Hook into deps instead of the target module.
// That way we know by the type of dep whether to include.
Expand Down
@@ -0,0 +1 @@
export * from './one'
@@ -0,0 +1,6 @@
export function One() {
return 'one'
}

export * from './two'
export { Two as TwoAliased } from './two'
@@ -0,0 +1,3 @@
export function Two() {
return 'two'
}
Expand Up @@ -4,9 +4,12 @@ import { a, b, c, d, e } from '../components/shared-exports'
import DefaultArrow, {
Named as ClientNamed,
} from '../components/client-exports.client'

import { Cjs as CjsShared } from '../components/cjs'
import { Cjs as CjsClient } from '../components/cjs.client'

import { One, Two, TwoAliased } from '../components/export-all/index.client'

export default function Page() {
return (
<div>
Expand All @@ -29,6 +32,9 @@ export default function Page() {
<div>
<CjsClient />
</div>
<div>
Export All: <One />, <Two />, <TwoAliased />
</div>
</div>
)
}
Expand Up @@ -218,6 +218,7 @@ export default function (context, { runtime, env }) {
expect(hydratedContent).toContain('named.client')
expect(hydratedContent).toContain('cjs-shared')
expect(hydratedContent).toContain('cjs-client')
expect(hydratedContent).toContain('Export All: one, two, two')
})

it('should handle 404 requests and missing routes correctly', async () => {
Expand Down

0 comments on commit a326936

Please sign in to comment.