Skip to content

Commit

Permalink
Merge branch 'canary' into shu/f8bc
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] committed Jan 27, 2022
2 parents 7fe8c37 + 3bda6e6 commit 90bd8d7
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 85 deletions.
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>
)
}
*/

0 comments on commit 90bd8d7

Please sign in to comment.