Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use swc parse for flight server and client loaders #33713

Merged
merged 16 commits into from Jan 27, 2022
4 changes: 3 additions & 1 deletion packages/next-swc/crates/napi/src/lib.rs
Expand Up @@ -44,7 +44,7 @@ mod bundle;
mod minify;
mod transform;
mod util;

mod parse;

static COMPILER: Lazy<Arc<Compiler>> = Lazy::new(|| {
let cm = Arc::new(SourceMap::new(FilePathMapping::empty()));
Expand All @@ -68,6 +68,8 @@ fn init(mut exports: JsObject) -> napi::Result<()> {

exports.create_named_method("minify", minify::minify)?;
exports.create_named_method("minifySync", minify::minify_sync)?;

exports.create_named_method("parse", parse::parse)?;

Ok(())
}
Expand Down
70 changes: 70 additions & 0 deletions packages/next-swc/crates/napi/src/parse.rs
@@ -0,0 +1,70 @@
use crate::util::{deserialize_json, CtxtExt, MapErr};
use anyhow::Context as _;
use napi::{CallContext, Either, Env, JsObject, JsString, JsUndefined, Task};
use std::sync::Arc;
use swc::{config::ParseOptions, try_with_handler};
use swc_common::{FileName, FilePathMapping, SourceMap};

pub struct ParseTask {
pub filename: FileName,
pub src: String,
pub options: String,
}

pub fn complete_parse<'a>(env: &Env, ast_json: String) -> napi::Result<JsString> {
env.create_string_from_std(ast_json)
}

impl Task for ParseTask {
type Output = String;
type JsValue = JsString;

fn compute(&mut self) -> napi::Result<Self::Output> {
let c = swc::Compiler::new(Arc::new(SourceMap::new(FilePathMapping::empty())));

let options: ParseOptions = deserialize_json(&self.options).convert_err()?;
let fm =
c.cm.new_source_file(self.filename.clone(), self.src.clone());
let program = try_with_handler(c.cm.clone(), false, |handler| {
c.parse_js(
fm,
&handler,
options.target,
options.syntax,
options.is_module,
options.comments,
)
})
.convert_err()?;

let ast_json = serde_json::to_string(&program)
.context("failed to serialize Program")
.convert_err()?;

Ok(ast_json)
}

fn resolve(self, env: Env, result: Self::Output) -> napi::Result<Self::JsValue> {
complete_parse(&env, result)
}
}

#[js_function(3)]
pub fn parse(ctx: CallContext) -> napi::Result<JsObject> {
let src = ctx.get::<JsString>(0)?.into_utf8()?.as_str()?.to_string();
let options = ctx.get_buffer_as_string(1)?;
let filename = ctx.get::<Either<JsString, JsUndefined>>(2)?;
let filename = if let Either::A(value) = filename {
FileName::Real(value.into_utf8()?.as_str()?.to_owned().into())
} else {
FileName::Anon
};

ctx.env
.spawn(ParseTask {
filename,
src,
options,
})
.map(|t| t.promise_object())
}
12 changes: 12 additions & 0 deletions packages/next/build/swc/index.js
Expand Up @@ -72,6 +72,9 @@ async function loadWasm() {
minify(src, options) {
return Promise.resolve(bindings.minifySync(src.toString(), options))
},
parse(src, options) {
return Promise.resolve(bindings.parse(src.toString(), options))
},
}
return wasmBindings
} catch (e) {
Expand Down Expand Up @@ -179,6 +182,10 @@ function loadNative() {
bundle(options) {
return bindings.bundle(toBuffer(options))
},

parse(src, options) {
return bindings.parse(src, toBuffer(options ?? {}))
},
}
return nativeBindings
}
Expand Down Expand Up @@ -219,3 +226,8 @@ export async function bundle(options) {
let bindings = loadBindingsSync()
return bindings.bundle(toBuffer(options))
}

export async function parse(src, options) {
let bindings = loadBindingsSync()
return bindings.parse(src, options).then((astStr) => JSON.parse(astStr))
}
6 changes: 3 additions & 3 deletions packages/next/build/swc/options.js
Expand Up @@ -5,7 +5,7 @@ const regeneratorRuntimePath = require.resolve(
'next/dist/compiled/regenerator-runtime'
)

function getBaseSWCOptions({
export function getBaseSWCOptions({
filename,
jest,
development,
Expand Down Expand Up @@ -45,9 +45,9 @@ function getBaseSWCOptions({
pragma: 'React.createElement',
pragmaFrag: 'React.Fragment',
throwIfNamespace: true,
development: development,
development: !!development,
useBuiltins: true,
refresh: hasReactRefresh,
refresh: !!hasReactRefresh,
},
optimizer: {
simplify: false,
Expand Down
87 changes: 22 additions & 65 deletions packages/next/build/webpack/loaders/next-flight-client-loader.ts
Expand Up @@ -5,29 +5,16 @@
* LICENSE file in the root directory of this source tree.
*/

import * as acorn from 'next/dist/compiled/acorn'

type ResolveContext = {
conditions: Array<string>
parentURL: string | void
}

type ResolveFunction = (
specifier: string,
context: ResolveContext,
resolve: ResolveFunction
) => { url: string } | Promise<{ url: string }>

type TransformSourceFunction = (url: string, callback: () => void) => void

type Source = string | ArrayBuffer | Uint8Array

let stashedResolve: null | ResolveFunction = null
// 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) {
case 'Identifier':
names.push(node.name)
names.push(node.value)
return
case 'ObjectPattern':
for (let i = 0; i < node.properties.length; i++)
Expand Down Expand Up @@ -56,50 +43,25 @@ function addExportNames(names: string[], node: any) {
}
}

function resolveClientImport(
specifier: string,
parentURL: string
): { url: string } | Promise<{ url: string }> {
// Resolve an import specifier as if it was loaded by the client. This doesn't use
// the overrides that this loader does but instead reverts to the default.
// This resolution algorithm will not necessarily have the same configuration
// as the actual client loader. It should mostly work and if it doesn't you can
// always convert to explicit exported names instead.
const conditions = ['node', 'import']
if (stashedResolve === null) {
throw new Error(
'Expected resolve to have been called before transformSource'
)
}
return stashedResolve(specifier, { conditions, parentURL }, stashedResolve)
}

async function parseExportNamesInto(
resourcePath: string,
transformedSource: string,
names: Array<string>,
parentURL: string,
loadModule: TransformSourceFunction
names: Array<string>
): Promise<void> {
const { body } = acorn.parse(transformedSource, {
ecmaVersion: 11,
sourceType: 'module',
}) as any
const opts = getBaseSWCOptions({
filename: resourcePath,
globalWindow: true,
})

const { body } = await parse(transformedSource, {
...opts.jsc.parser,
isModule: true,
})
for (let i = 0; i < body.length; i++) {
const node = body[i]
switch (node.type) {
case 'ExportAllDeclaration':
if (node.exported) {
addExportNames(names, node.exported)
continue
} else {
const { url } = await resolveClientImport(
node.source.value,
parentURL
)
const source = ''
parseExportNamesInto(source, names, url, loadModule)
continue
}
// TODO: support export * from module path
// case 'ExportAllDeclaration':
case 'ExportDefaultDeclaration':
names.push('default')
continue
Expand Down Expand Up @@ -129,8 +91,8 @@ async function parseExportNamesInto(

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

if (resourceQuery !== '?flight') return source
Expand All @@ -142,12 +104,7 @@ export default async function transformSource(
}

const names: string[] = []
await parseExportNamesInto(
transformedSource as string,
names,
url + resourceQuery,
this.loadModule
)
await parseExportNamesInto(resourcePath, transformedSource, names)

// next.js/packages/next/<component>.js
if (/[\\/]next[\\/](link|image)\.js$/.test(url)) {
Expand Down
42 changes: 26 additions & 16 deletions packages/next/build/webpack/loaders/next-flight-server-loader.ts
@@ -1,4 +1,8 @@
import * as acorn from 'next/dist/compiled/acorn'
// 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'

function isClientComponent(importSource: string, pageExtensions: string[]) {
Expand Down Expand Up @@ -28,6 +32,7 @@ export function isImageImport(importSource: string) {
}

async function parseImportsInfo(
resourcePath: string,
source: string,
imports: Array<string>,
isClientCompilation: boolean,
Expand All @@ -36,21 +41,22 @@ async function parseImportsInfo(
source: string
defaultExportName: string
}> {
const { body } = acorn.parse(source, {
ecmaVersion: 11,
sourceType: 'module',
}) as any

const opts = getBaseSWCOptions({
filename: resourcePath,
globalWindow: isClientCompilation,
})

const ast = await parse(source, { ...opts.jsc.parser, isModule: true })
const { body } = ast
const beginPos = ast.span.start
let transformedSource = ''
let lastIndex = 0
let defaultExportName = 'RSCComponent'

let defaultExportName
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 (
!(
Expand All @@ -61,10 +67,11 @@ async function parseImportsInfo(
) {
continue
}
transformedSource += source.substring(
const importDeclarations = source.substring(
lastIndex,
node.source.start - 1
node.source.span.start - beginPos
)
transformedSource += importDeclarations
transformedSource += JSON.stringify(`${node.source.value}?flight`)
} else {
// For the client compilation, we skip all modules imports but
Expand All @@ -84,16 +91,16 @@ async function parseImportsInfo(
}
}

lastIndex = node.source.end
lastIndex = node.source.span.end - beginPos
imports.push(`require(${JSON.stringify(importSource)})`)
continue
}
case 'ExportDefaultDeclaration': {
const def = node.declaration
const def = node.decl
if (def.type === 'Identifier') {
defaultExportName = def.name
} else if (def.type === 'FunctionDeclaration') {
defaultExportName = def.id.name
} else if (def.type === 'FunctionExpression') {
defaultExportName = def.identifier.value
}
break
}
Expand Down Expand Up @@ -129,6 +136,7 @@ export default async function transformSource(
const imports: string[] = []
const { source: transformedSource, defaultExportName } =
await parseImportsInfo(
resourcePath,
source,
imports,
isClientCompilation,
Expand All @@ -150,7 +158,9 @@ export default async function transformSource(
const noop = `export const __rsc_noop__=()=>{${imports.join(';')}}`
const defaultExportNoop = isClientCompilation
? `export default function ${defaultExportName}(){}\n${defaultExportName}.__next_rsc__=1;`
: `${defaultExportName}.__next_rsc__=1;`
: defaultExportName
? `${defaultExportName}.__next_rsc__=1;`
: ''

const transformed = transformedSource + '\n' + noop + '\n' + defaultExportNoop

Expand Down
@@ -0,0 +1 @@
export * from './client-exports'
@@ -0,0 +1,20 @@
export * from './client-exports'

// TODO: add exports all test case in pages
/**

import * as all from '../components/client-exports-all'
import * as allClient from '../components/client-exports-all.client'

export default function Page() {
const { a, b, c, d, e } = all
const { a: ac, b: bc, c: cc, d: dc, e: ec } = allClient
return (
<div>
<div id='server'>{a}{b}{c}{d}{e[0]}</div>
<div id='client'>{ac}{bc}{cc}{dc}{ec[0]}</div>
</div>
)
}

*/