Skip to content

Commit 31c225a

Browse files
ijjkalunyov
authored andcommitted
Relay Support in Rust Compiler (vercel#33702)
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 <[email protected]>
1 parent 9b14ee5 commit 31c225a

File tree

35 files changed

+1049
-5
lines changed

35 files changed

+1049
-5
lines changed

docs/advanced-features/compiler.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,26 @@ const customJestConfig = {
9494
module.exports = createJestConfig(customJestConfig)
9595
```
9696

97+
### Relay
98+
99+
To enable [Relay](https://relay.dev/) support:
100+
101+
```js
102+
// next.config.js
103+
module.exports = {
104+
experimental: {
105+
relay: {
106+
// This should match relay.config.js
107+
src: './',
108+
artifactDirectory: './__generated__'
109+
language: 'typescript',
110+
},
111+
},
112+
}
113+
```
114+
115+
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.
116+
97117
### Remove React Properties
98118

99119
Allows to remove JSX properties. This is often used for testing. Similar to `babel-plugin-react-remove-properties`.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@types/http-proxy": "1.17.3",
6161
"@types/jest": "24.0.13",
6262
"@types/node": "13.11.0",
63+
"@types/relay-runtime": "13.0.0",
6364
"@types/selenium-webdriver": "4.0.15",
6465
"@types/sharp": "0.29.3",
6566
"@types/string-hash": "1.1.1",
@@ -147,6 +148,8 @@
147148
"react-dom": "17.0.2",
148149
"react-dom-18": "npm:[email protected]",
149150
"react-ssr-prepass": "1.0.8",
151+
"relay-compiler": "13.0.2",
152+
"relay-runtime": "13.0.2",
150153
"react-virtualized": "9.22.3",
151154
"release": "6.3.0",
152155
"request-promise-core": "1.1.2",

packages/next-swc/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/next-swc/crates/core/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ crate-type = ["cdylib", "rlib"]
88

99
[dependencies]
1010
chrono = "0.4"
11+
once_cell = "1.8.0"
1112
easy-error = "1.0.0"
1213
either = "1"
1314
fxhash = "0.2.1"
@@ -30,5 +31,3 @@ regex = "1.5"
3031
swc_ecma_transforms_testing = "0.59.0"
3132
testing = "0.18.0"
3233
walkdir = "2.3.2"
33-
34-

packages/next-swc/crates/core/src/lib.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ pub mod next_dynamic;
5353
pub mod next_ssg;
5454
pub mod page_config;
5555
pub mod react_remove_properties;
56+
#[cfg(not(target_arch = "wasm32"))]
57+
pub mod relay;
5658
pub mod remove_console;
5759
pub mod shake_exports;
5860
pub mod styled_jsx;
@@ -91,6 +93,10 @@ pub struct TransformOptions {
9193
#[serde(default)]
9294
pub react_remove_properties: Option<react_remove_properties::Config>,
9395

96+
#[serde(default)]
97+
#[cfg(not(target_arch = "wasm32"))]
98+
pub relay: Option<relay::Config>,
99+
94100
#[serde(default)]
95101
pub shake_exports: Option<shake_exports::Config>,
96102
}
@@ -99,7 +105,19 @@ pub fn custom_before_pass(
99105
cm: Arc<SourceMap>,
100106
file: Arc<SourceFile>,
101107
opts: &TransformOptions,
102-
) -> impl Fold {
108+
) -> impl Fold + '_ {
109+
#[cfg(target_arch = "wasm32")]
110+
let relay_plugin = noop();
111+
112+
#[cfg(not(target_arch = "wasm32"))]
113+
let relay_plugin = {
114+
if let Some(config) = &opts.relay {
115+
Either::Left(relay::relay(config, file.name.clone()))
116+
} else {
117+
Either::Right(noop())
118+
}
119+
};
120+
103121
chain!(
104122
disallow_re_export_all_in_page::disallow_re_export_all_in_page(opts.is_page_file),
105123
styled_jsx::styled_jsx(cm.clone()),
@@ -130,6 +148,7 @@ pub fn custom_before_pass(
130148
page_config::page_config(opts.is_development, opts.is_page_file),
131149
!opts.disable_page_config
132150
),
151+
relay_plugin,
133152
match &opts.remove_console {
134153
Some(config) if config.truthy() =>
135154
Either::Left(remove_console::remove_console(config.clone())),
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
use once_cell::sync::Lazy;
2+
use regex::Regex;
3+
use serde::Deserialize;
4+
use std::path::{Path, PathBuf};
5+
use swc_atoms::JsWord;
6+
use swc_common::errors::HANDLER;
7+
use swc_common::FileName;
8+
use swc_ecmascript::ast::*;
9+
use swc_ecmascript::utils::{quote_ident, ExprFactory};
10+
use swc_ecmascript::visit::{Fold, FoldWith};
11+
12+
#[derive(Copy, Clone, Debug, Deserialize)]
13+
#[serde(rename_all = "lowercase")]
14+
pub enum RelayLanguageConfig {
15+
TypeScript,
16+
Flow,
17+
}
18+
19+
impl Default for RelayLanguageConfig {
20+
fn default() -> Self {
21+
Self::Flow
22+
}
23+
}
24+
25+
struct Relay<'a> {
26+
root_dir: PathBuf,
27+
file_name: FileName,
28+
config: &'a Config,
29+
}
30+
31+
#[derive(Deserialize, Debug, Default, Clone)]
32+
#[serde(rename_all = "camelCase")]
33+
pub struct Config {
34+
pub src: PathBuf,
35+
pub artifact_directory: Option<PathBuf>,
36+
#[serde(default)]
37+
pub language: RelayLanguageConfig,
38+
}
39+
40+
fn pull_first_operation_name_from_tpl(tpl: &TaggedTpl) -> Option<String> {
41+
tpl.tpl.quasis.iter().find_map(|quasis| {
42+
static OPERATION_REGEX: Lazy<Regex> =
43+
Lazy::new(|| Regex::new(r"(fragment|mutation|query|subscription) (\w+)").unwrap());
44+
45+
let capture_group = OPERATION_REGEX.captures_iter(&quasis.raw.value).next();
46+
47+
capture_group.map(|capture_group| capture_group[2].to_string())
48+
})
49+
}
50+
51+
fn build_require_expr_from_path(path: &str) -> Expr {
52+
Expr::Call(CallExpr {
53+
span: Default::default(),
54+
callee: quote_ident!("require").as_callee(),
55+
args: vec![
56+
Lit::Str(Str {
57+
span: Default::default(),
58+
value: JsWord::from(path),
59+
has_escape: false,
60+
kind: Default::default(),
61+
})
62+
.as_arg(),
63+
],
64+
type_args: None,
65+
})
66+
}
67+
68+
impl<'a> Fold for Relay<'a> {
69+
fn fold_expr(&mut self, expr: Expr) -> Expr {
70+
let expr = expr.fold_children_with(self);
71+
72+
match &expr {
73+
Expr::TaggedTpl(tpl) => {
74+
if let Some(built_expr) = self.build_call_expr_from_tpl(tpl) {
75+
built_expr
76+
} else {
77+
expr
78+
}
79+
}
80+
_ => expr,
81+
}
82+
}
83+
}
84+
85+
#[derive(Debug)]
86+
enum BuildRequirePathError {
87+
FileNameNotReal,
88+
ArtifactDirectoryExpected,
89+
}
90+
91+
fn path_for_artifact(
92+
root_dir: &Path,
93+
config: &Config,
94+
definition_name: &str,
95+
) -> Result<PathBuf, BuildRequirePathError> {
96+
let filename = match &config.language {
97+
RelayLanguageConfig::Flow => format!("{}.graphql.js", definition_name),
98+
RelayLanguageConfig::TypeScript => {
99+
format!("{}.graphql.ts", definition_name)
100+
}
101+
};
102+
103+
if let Some(artifact_directory) = &config.artifact_directory {
104+
Ok(root_dir.join(artifact_directory).join(filename))
105+
} else {
106+
Err(BuildRequirePathError::ArtifactDirectoryExpected)
107+
}
108+
}
109+
110+
impl<'a> Relay<'a> {
111+
fn build_require_path(
112+
&mut self,
113+
operation_name: &str,
114+
) -> Result<PathBuf, BuildRequirePathError> {
115+
match &self.file_name {
116+
FileName::Real(_real_file_name) => {
117+
path_for_artifact(&self.root_dir, self.config, operation_name)
118+
}
119+
_ => Err(BuildRequirePathError::FileNameNotReal),
120+
}
121+
}
122+
123+
fn build_call_expr_from_tpl(&mut self, tpl: &TaggedTpl) -> Option<Expr> {
124+
if let Expr::Ident(ident) = &*tpl.tag {
125+
if &*ident.sym != "graphql" {
126+
return None;
127+
}
128+
}
129+
130+
let operation_name = pull_first_operation_name_from_tpl(tpl);
131+
132+
match operation_name {
133+
None => None,
134+
Some(operation_name) => match self.build_require_path(operation_name.as_str()) {
135+
Ok(final_path) => Some(build_require_expr_from_path(final_path.to_str().unwrap())),
136+
Err(err) => {
137+
let base_error = "Could not transform GraphQL template to a Relay import.";
138+
let error_message = match err {
139+
BuildRequirePathError::FileNameNotReal => "Source file was not a real \
140+
file. This is likely a bug and \
141+
should be reported to Next.js"
142+
.to_string(),
143+
BuildRequirePathError::ArtifactDirectoryExpected => {
144+
"The `artifactDirectory` is expected to be set in the Relay config \
145+
file to work correctly with Next.js."
146+
.to_string()
147+
}
148+
};
149+
150+
HANDLER.with(|handler| {
151+
handler.span_err(
152+
tpl.span,
153+
format!("{} {}", base_error, error_message).as_str(),
154+
);
155+
});
156+
157+
None
158+
}
159+
},
160+
}
161+
}
162+
}
163+
164+
pub fn relay<'a>(config: &'a Config, file_name: FileName) -> impl Fold + '_ {
165+
Relay {
166+
root_dir: std::env::current_dir().unwrap(),
167+
file_name,
168+
config,
169+
}
170+
}

packages/next-swc/crates/core/tests/fixture.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use next_swc::{
44
next_ssg::next_ssg,
55
page_config::page_config_test,
66
react_remove_properties::remove_properties,
7+
relay::{relay, Config as RelayConfig, RelayLanguageConfig},
78
remove_console::remove_console,
89
shake_exports::{shake_exports, Config as ShakeExportsConfig},
910
styled_jsx::styled_jsx,
@@ -149,6 +150,22 @@ fn page_config_fixture(input: PathBuf) {
149150
test_fixture(syntax(), &|_tr| page_config_test(), &input, &output);
150151
}
151152

153+
#[fixture("tests/fixture/relay/**/input.ts*")]
154+
fn relay_no_artifact_dir_fixture(input: PathBuf) {
155+
let output = input.parent().unwrap().join("output.js");
156+
let config = RelayConfig {
157+
language: RelayLanguageConfig::TypeScript,
158+
artifact_directory: Some(PathBuf::from("__generated__")),
159+
..Default::default()
160+
};
161+
test_fixture(
162+
syntax(),
163+
&|_tr| relay(&config, FileName::Real(PathBuf::from("input.tsx"))),
164+
&input,
165+
&output,
166+
);
167+
}
168+
152169
#[fixture("tests/fixture/remove-console/**/input.js")]
153170
fn remove_console_fixture(input: PathBuf) {
154171
let output = input.parent().unwrap().join("output.js");
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const variableQuery = graphql`
2+
query InputVariableQuery {
3+
hello
4+
}
5+
`
6+
7+
fetchQuery(graphql`
8+
query InputUsedInFunctionCallQuery {
9+
hello
10+
}
11+
`)
12+
13+
function SomeQueryComponent() {
14+
useLazyLoadQuery(graphql`
15+
query InputInHookQuery {
16+
hello
17+
}
18+
`)
19+
}
20+
21+
const variableMutation = graphql`
22+
query InputVariableMutation {
23+
someMutation
24+
}
25+
`
26+
27+
commitMutation(
28+
environment,
29+
graphql`
30+
query InputUsedInFunctionCallMutation {
31+
someMutation
32+
}
33+
`
34+
)
35+
36+
function SomeMutationComponent() {
37+
useMutation(graphql`
38+
query InputInHookMutation {
39+
someMutation
40+
}
41+
`)
42+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const variableQuery = require("$DIR/__generated__/InputVariableQuery.graphql.ts");
2+
fetchQuery(require("$DIR/__generated__/InputUsedInFunctionCallQuery.graphql.ts"));
3+
function SomeQueryComponent() {
4+
useLazyLoadQuery(require("$DIR/__generated__/InputInHookQuery.graphql.ts"));
5+
}
6+
const variableMutation = require("$DIR/__generated__/InputVariableMutation.graphql.ts");
7+
commitMutation(environment, require("$DIR/__generated__/InputUsedInFunctionCallMutation.graphql.ts"));
8+
function SomeMutationComponent() {
9+
useMutation(require("$DIR/__generated__/InputInHookMutation.graphql.ts"));
10+
}

packages/next-swc/crates/core/tests/full.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ fn test(input: &Path, minify: bool) {
5959
styled_components: Some(assert_json("{}")),
6060
remove_console: None,
6161
react_remove_properties: None,
62+
relay: None,
6263
shake_exports: None,
6364
};
6465

packages/next/build/swc/options.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export function getBaseSWCOptions({
8383
: null,
8484
removeConsole: nextConfig?.experimental?.removeConsole,
8585
reactRemoveProperties: nextConfig?.experimental?.reactRemoveProperties,
86+
relay: nextConfig?.experimental?.relay,
8687
}
8788
}
8889

packages/next/build/webpack-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1621,6 +1621,7 @@ export default async function getBaseWebpackConfig(
16211621
removeConsole: config.experimental.removeConsole,
16221622
reactRemoveProperties: config.experimental.reactRemoveProperties,
16231623
styledComponents: config.experimental.styledComponents,
1624+
relay: config.experimental.relay,
16241625
})
16251626

16261627
const cache: any = {

0 commit comments

Comments
 (0)