Skip to content

Commit

Permalink
Relay Support in Rust Compiler (vercel#33702)
Browse files Browse the repository at this point in the history
Reverts vercel#33699

This re-opens the support for relay in swc, although we need to narrow in the causes for the build failures in https://github.com/vercel/next.js/runs/4950448889?check_suite_focus=true

Co-authored-by: Andrey Lunyov <102968+alunyov@users.noreply.github.com>
  • Loading branch information
2 people authored and natew committed Feb 16, 2022
1 parent 9b14ee5 commit 31c225a
Show file tree
Hide file tree
Showing 35 changed files with 1,049 additions and 5 deletions.
20 changes: 20 additions & 0 deletions docs/advanced-features/compiler.md
Expand Up @@ -94,6 +94,26 @@ const customJestConfig = {
module.exports = createJestConfig(customJestConfig)
```

### Relay

To enable [Relay](https://relay.dev/) support:

```js
// next.config.js
module.exports = {
experimental: {
relay: {
// This should match relay.config.js
src: './',
artifactDirectory: './__generated__'
language: 'typescript',
},
},
}
```

NOTE: In Next.js all JavaScripts files in `pages` directory are considered routes. So, for `relay-compiler` you'll need to specify `artifactDirectory` configuration settings outside of the `pages`, otherwise `relay-compiler` will generate files next to the source file in the `__generated__` directory, and this file will be considered a route, which will break production build.

### Remove React Properties

Allows to remove JSX properties. This is often used for testing. Similar to `babel-plugin-react-remove-properties`.
Expand Down
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -60,6 +60,7 @@
"@types/http-proxy": "1.17.3",
"@types/jest": "24.0.13",
"@types/node": "13.11.0",
"@types/relay-runtime": "13.0.0",
"@types/selenium-webdriver": "4.0.15",
"@types/sharp": "0.29.3",
"@types/string-hash": "1.1.1",
Expand Down Expand Up @@ -147,6 +148,8 @@
"react-dom": "17.0.2",
"react-dom-18": "npm:react-dom@18.0.0-rc.0",
"react-ssr-prepass": "1.0.8",
"relay-compiler": "13.0.2",
"relay-runtime": "13.0.2",
"react-virtualized": "9.22.3",
"release": "6.3.0",
"request-promise-core": "1.1.2",
Expand Down
1 change: 1 addition & 0 deletions packages/next-swc/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions packages/next-swc/crates/core/Cargo.toml
Expand Up @@ -8,6 +8,7 @@ crate-type = ["cdylib", "rlib"]

[dependencies]
chrono = "0.4"
once_cell = "1.8.0"
easy-error = "1.0.0"
either = "1"
fxhash = "0.2.1"
Expand All @@ -30,5 +31,3 @@ regex = "1.5"
swc_ecma_transforms_testing = "0.59.0"
testing = "0.18.0"
walkdir = "2.3.2"


21 changes: 20 additions & 1 deletion packages/next-swc/crates/core/src/lib.rs
Expand Up @@ -53,6 +53,8 @@ pub mod next_dynamic;
pub mod next_ssg;
pub mod page_config;
pub mod react_remove_properties;
#[cfg(not(target_arch = "wasm32"))]
pub mod relay;
pub mod remove_console;
pub mod shake_exports;
pub mod styled_jsx;
Expand Down Expand Up @@ -91,6 +93,10 @@ pub struct TransformOptions {
#[serde(default)]
pub react_remove_properties: Option<react_remove_properties::Config>,

#[serde(default)]
#[cfg(not(target_arch = "wasm32"))]
pub relay: Option<relay::Config>,

#[serde(default)]
pub shake_exports: Option<shake_exports::Config>,
}
Expand All @@ -99,7 +105,19 @@ pub fn custom_before_pass(
cm: Arc<SourceMap>,
file: Arc<SourceFile>,
opts: &TransformOptions,
) -> impl Fold {
) -> impl Fold + '_ {
#[cfg(target_arch = "wasm32")]
let relay_plugin = noop();

#[cfg(not(target_arch = "wasm32"))]
let relay_plugin = {
if let Some(config) = &opts.relay {
Either::Left(relay::relay(config, file.name.clone()))
} else {
Either::Right(noop())
}
};

chain!(
disallow_re_export_all_in_page::disallow_re_export_all_in_page(opts.is_page_file),
styled_jsx::styled_jsx(cm.clone()),
Expand Down Expand Up @@ -130,6 +148,7 @@ pub fn custom_before_pass(
page_config::page_config(opts.is_development, opts.is_page_file),
!opts.disable_page_config
),
relay_plugin,
match &opts.remove_console {
Some(config) if config.truthy() =>
Either::Left(remove_console::remove_console(config.clone())),
Expand Down
170 changes: 170 additions & 0 deletions packages/next-swc/crates/core/src/relay.rs
@@ -0,0 +1,170 @@
use once_cell::sync::Lazy;
use regex::Regex;
use serde::Deserialize;
use std::path::{Path, PathBuf};
use swc_atoms::JsWord;
use swc_common::errors::HANDLER;
use swc_common::FileName;
use swc_ecmascript::ast::*;
use swc_ecmascript::utils::{quote_ident, ExprFactory};
use swc_ecmascript::visit::{Fold, FoldWith};

#[derive(Copy, Clone, Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RelayLanguageConfig {
TypeScript,
Flow,
}

impl Default for RelayLanguageConfig {
fn default() -> Self {
Self::Flow
}
}

struct Relay<'a> {
root_dir: PathBuf,
file_name: FileName,
config: &'a Config,
}

#[derive(Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Config {
pub src: PathBuf,
pub artifact_directory: Option<PathBuf>,
#[serde(default)]
pub language: RelayLanguageConfig,
}

fn pull_first_operation_name_from_tpl(tpl: &TaggedTpl) -> Option<String> {
tpl.tpl.quasis.iter().find_map(|quasis| {
static OPERATION_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(fragment|mutation|query|subscription) (\w+)").unwrap());

let capture_group = OPERATION_REGEX.captures_iter(&quasis.raw.value).next();

capture_group.map(|capture_group| capture_group[2].to_string())
})
}

fn build_require_expr_from_path(path: &str) -> Expr {
Expr::Call(CallExpr {
span: Default::default(),
callee: quote_ident!("require").as_callee(),
args: vec![
Lit::Str(Str {
span: Default::default(),
value: JsWord::from(path),
has_escape: false,
kind: Default::default(),
})
.as_arg(),
],
type_args: None,
})
}

impl<'a> Fold for Relay<'a> {
fn fold_expr(&mut self, expr: Expr) -> Expr {
let expr = expr.fold_children_with(self);

match &expr {
Expr::TaggedTpl(tpl) => {
if let Some(built_expr) = self.build_call_expr_from_tpl(tpl) {
built_expr
} else {
expr
}
}
_ => expr,
}
}
}

#[derive(Debug)]
enum BuildRequirePathError {
FileNameNotReal,
ArtifactDirectoryExpected,
}

fn path_for_artifact(
root_dir: &Path,
config: &Config,
definition_name: &str,
) -> Result<PathBuf, BuildRequirePathError> {
let filename = match &config.language {
RelayLanguageConfig::Flow => format!("{}.graphql.js", definition_name),
RelayLanguageConfig::TypeScript => {
format!("{}.graphql.ts", definition_name)
}
};

if let Some(artifact_directory) = &config.artifact_directory {
Ok(root_dir.join(artifact_directory).join(filename))
} else {
Err(BuildRequirePathError::ArtifactDirectoryExpected)
}
}

impl<'a> Relay<'a> {
fn build_require_path(
&mut self,
operation_name: &str,
) -> Result<PathBuf, BuildRequirePathError> {
match &self.file_name {
FileName::Real(_real_file_name) => {
path_for_artifact(&self.root_dir, self.config, operation_name)
}
_ => Err(BuildRequirePathError::FileNameNotReal),
}
}

fn build_call_expr_from_tpl(&mut self, tpl: &TaggedTpl) -> Option<Expr> {
if let Expr::Ident(ident) = &*tpl.tag {
if &*ident.sym != "graphql" {
return None;
}
}

let operation_name = pull_first_operation_name_from_tpl(tpl);

match operation_name {
None => None,
Some(operation_name) => match self.build_require_path(operation_name.as_str()) {
Ok(final_path) => Some(build_require_expr_from_path(final_path.to_str().unwrap())),
Err(err) => {
let base_error = "Could not transform GraphQL template to a Relay import.";
let error_message = match err {
BuildRequirePathError::FileNameNotReal => "Source file was not a real \
file. This is likely a bug and \
should be reported to Next.js"
.to_string(),
BuildRequirePathError::ArtifactDirectoryExpected => {
"The `artifactDirectory` is expected to be set in the Relay config \
file to work correctly with Next.js."
.to_string()
}
};

HANDLER.with(|handler| {
handler.span_err(
tpl.span,
format!("{} {}", base_error, error_message).as_str(),
);
});

None
}
},
}
}
}

pub fn relay<'a>(config: &'a Config, file_name: FileName) -> impl Fold + '_ {
Relay {
root_dir: std::env::current_dir().unwrap(),
file_name,
config,
}
}
17 changes: 17 additions & 0 deletions packages/next-swc/crates/core/tests/fixture.rs
Expand Up @@ -4,6 +4,7 @@ use next_swc::{
next_ssg::next_ssg,
page_config::page_config_test,
react_remove_properties::remove_properties,
relay::{relay, Config as RelayConfig, RelayLanguageConfig},
remove_console::remove_console,
shake_exports::{shake_exports, Config as ShakeExportsConfig},
styled_jsx::styled_jsx,
Expand Down Expand Up @@ -149,6 +150,22 @@ fn page_config_fixture(input: PathBuf) {
test_fixture(syntax(), &|_tr| page_config_test(), &input, &output);
}

#[fixture("tests/fixture/relay/**/input.ts*")]
fn relay_no_artifact_dir_fixture(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");
let config = RelayConfig {
language: RelayLanguageConfig::TypeScript,
artifact_directory: Some(PathBuf::from("__generated__")),
..Default::default()
};
test_fixture(
syntax(),
&|_tr| relay(&config, FileName::Real(PathBuf::from("input.tsx"))),
&input,
&output,
);
}

#[fixture("tests/fixture/remove-console/**/input.js")]
fn remove_console_fixture(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");
Expand Down
42 changes: 42 additions & 0 deletions packages/next-swc/crates/core/tests/fixture/relay/input.tsx
@@ -0,0 +1,42 @@
const variableQuery = graphql`
query InputVariableQuery {
hello
}
`

fetchQuery(graphql`
query InputUsedInFunctionCallQuery {
hello
}
`)

function SomeQueryComponent() {
useLazyLoadQuery(graphql`
query InputInHookQuery {
hello
}
`)
}

const variableMutation = graphql`
query InputVariableMutation {
someMutation
}
`

commitMutation(
environment,
graphql`
query InputUsedInFunctionCallMutation {
someMutation
}
`
)

function SomeMutationComponent() {
useMutation(graphql`
query InputInHookMutation {
someMutation
}
`)
}
10 changes: 10 additions & 0 deletions packages/next-swc/crates/core/tests/fixture/relay/output.js
@@ -0,0 +1,10 @@
const variableQuery = require("$DIR/__generated__/InputVariableQuery.graphql.ts");
fetchQuery(require("$DIR/__generated__/InputUsedInFunctionCallQuery.graphql.ts"));
function SomeQueryComponent() {
useLazyLoadQuery(require("$DIR/__generated__/InputInHookQuery.graphql.ts"));
}
const variableMutation = require("$DIR/__generated__/InputVariableMutation.graphql.ts");
commitMutation(environment, require("$DIR/__generated__/InputUsedInFunctionCallMutation.graphql.ts"));
function SomeMutationComponent() {
useMutation(require("$DIR/__generated__/InputInHookMutation.graphql.ts"));
}
1 change: 1 addition & 0 deletions packages/next-swc/crates/core/tests/full.rs
Expand Up @@ -59,6 +59,7 @@ fn test(input: &Path, minify: bool) {
styled_components: Some(assert_json("{}")),
remove_console: None,
react_remove_properties: None,
relay: None,
shake_exports: None,
};

Expand Down
1 change: 1 addition & 0 deletions packages/next/build/swc/options.js
Expand Up @@ -83,6 +83,7 @@ export function getBaseSWCOptions({
: null,
removeConsole: nextConfig?.experimental?.removeConsole,
reactRemoveProperties: nextConfig?.experimental?.reactRemoveProperties,
relay: nextConfig?.experimental?.relay,
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/next/build/webpack-config.ts
Expand Up @@ -1621,6 +1621,7 @@ export default async function getBaseWebpackConfig(
removeConsole: config.experimental.removeConsole,
reactRemoveProperties: config.experimental.reactRemoveProperties,
styledComponents: config.experimental.styledComponents,
relay: config.experimental.relay,
})

const cache: any = {
Expand Down

0 comments on commit 31c225a

Please sign in to comment.