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

Relay Support in Rust Compiler #33240

Merged
merged 46 commits into from Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
2aaa426
Add support for relay compiler imports
tbezman Jan 12, 2022
f794fcf
Add relay deps for integration tests
tbezman Jan 12, 2022
6c966d2
Add integration test for Relay SWC Compiler
tbezman Jan 12, 2022
8ee3e6c
More exhaustive tests
tbezman Jan 13, 2022
46e2534
Remove dead code
tbezman Jan 13, 2022
66e6859
Pass through relay config from next config
tbezman Jan 13, 2022
eb88c71
Run relay compiler when running integration test
tbezman Jan 13, 2022
4ef5e23
Remove more dead code
tbezman Jan 13, 2022
90d3bce
Merge branch 'canary' into relay-plugin
tbezman Jan 13, 2022
718929c
Remove useless to_string and String::from
tbezman Jan 14, 2022
6d03ee9
Remove clone in favor of borrowing
tbezman Jan 14, 2022
523bc25
Use borrow instead of &*
tbezman Jan 14, 2022
61826a4
Removed more clones
tbezman Jan 14, 2022
3fe7119
Merge branch 'relay-plugin' of github.com:tbezman/next.js into relay-…
tbezman Jan 14, 2022
eb7daa1
Add relay and remove parking lot core since it doesn't seem like it's…
tbezman Jan 14, 2022
4f75ae5
Switch to relay.config.js
tbezman Jan 15, 2022
46f46f2
Add relay packages so we can resolve the artifact using their code
tbezman Jan 15, 2022
400def3
Use relay to determine artifact directory
tbezman Jan 15, 2022
b6d924f
Remove artifact dir test since that's handled by the relay packages
tbezman Jan 15, 2022
d419ba0
Update tests to use right cwd
tbezman Jan 15, 2022
3830a8d
Add support for fragments
tbezman Jan 15, 2022
5fa72cf
Merge remote-tracking branch 'upstream/canary' into relay-plugin
tbezman Jan 15, 2022
cd6ed9c
Added the copy trait
tbezman Jan 15, 2022
f7cfd1c
Address latest kdy1 feedback
tbezman Jan 16, 2022
fee2e11
Remove some unused imports
tbezman Jan 16, 2022
2b53fa1
Create new integration test for multi project configs
tbezman Jan 17, 2022
5612526
Add support for multi project configs and error handling
tbezman Jan 17, 2022
244a4da
Update fixture to support new API
tbezman Jan 17, 2022
ba4e846
Merge remote-tracking branch 'upstream/canary' into relay-plugin
tbezman Jan 17, 2022
8ba0998
Remove describe.only
tbezman Jan 17, 2022
8782671
Bring back parking lot and bump version to avoid conflicts
tbezman Jan 17, 2022
8343e17
Address latest kdy1 feedback
tbezman Jan 17, 2022
686ff01
Add note about Relay to the docs
timneutkens Jan 17, 2022
27057e5
Fix unit tests
tbezman Jan 18, 2022
acea05f
Switch to use a regex :)
tbezman Jan 18, 2022
85ac72c
Add support for subscription in regex
tbezman Jan 18, 2022
7d35873
Address kdy1 feedback
tbezman Jan 19, 2022
d24ae71
Don't include Relay when targeting wasm
tbezman Jan 22, 2022
5f3f298
Merge branch 'canary' of https://github.com/vercel/next.js into relay…
tbezman Jan 22, 2022
aa0f676
Update packages/next-swc/crates/core/src/relay.rs
tbezman Jan 26, 2022
4a4a5b6
Update packages/next-swc/crates/core/src/relay.rs
tbezman Jan 26, 2022
f6cc9d7
Update packages/next-swc/crates/core/src/relay.rs
tbezman Jan 26, 2022
fffa3e1
Merge remote-tracking branch 'upstream/canary' into relay-plugin
tbezman Jan 26, 2022
b5f89bc
Add better docs for CouldNotCategorize error
tbezman Jan 26, 2022
1682ac3
Add relay: false to full test suite
tbezman Jan 26, 2022
464dd97
Prettier fix
tbezman Jan 26, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -59,6 +59,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 @@ -145,6 +146,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.1",
"relay-runtime": "13.0.1",
timneutkens marked this conversation as resolved.
Show resolved Hide resolved
"release": "6.3.0",
"request-promise-core": "1.1.2",
"resolve-from": "5.0.0",
Expand Down
8 changes: 8 additions & 0 deletions packages/next-swc/crates/core/src/lib.rs
Expand Up @@ -53,6 +53,7 @@ pub mod next_dynamic;
pub mod next_ssg;
pub mod page_config;
pub mod react_remove_properties;
pub mod relay;
pub mod remove_console;
pub mod shake_exports;
pub mod styled_jsx;
Expand Down Expand Up @@ -91,6 +92,9 @@ pub struct TransformOptions {
#[serde(default)]
pub react_remove_properties: Option<react_remove_properties::Config>,

#[serde(default)]
pub relay: Option<relay::Config>,

#[serde(default)]
pub shake_exports: Option<shake_exports::Config>,
}
Expand Down Expand Up @@ -130,6 +134,10 @@ pub fn custom_before_pass(
page_config::page_config(opts.is_development, opts.is_page_file),
!opts.disable_page_config
),
match &opts.relay {
tbezman marked this conversation as resolved.
Show resolved Hide resolved
Some(config) => Either::Left(relay::relay(config.clone(), file.name.clone())),
_ => Either::Right(noop()),
},
match &opts.remove_console {
Some(config) if config.truthy() =>
Either::Left(remove_console::remove_console(config.clone())),
Expand Down
157 changes: 157 additions & 0 deletions packages/next-swc/crates/core/src/relay.rs
@@ -0,0 +1,157 @@
use std::path::PathBuf;

tbezman marked this conversation as resolved.
Show resolved Hide resolved
use pathdiff::diff_paths;
use serde::Deserialize;
use swc_atoms::{js_word, JsWord};
use swc_common::FileName::Real;
use swc_common::{FileName, Span};
use swc_ecmascript::ast::*;
use swc_ecmascript::visit::{Fold, FoldWith};

#[derive(Clone, Debug, Deserialize)]
tbezman marked this conversation as resolved.
Show resolved Hide resolved
#[serde(rename_all = "snake_case")]
pub enum RelayLanguageConfig {
Typescript,
Flow,
}

#[derive(Clone, Debug, Deserialize)]
pub struct Config {
pub artifact_directory: Option<PathBuf>,
pub language: RelayLanguageConfig,
}

impl Config {
fn file_extension(&mut self) -> &'static str {
match self.language {
RelayLanguageConfig::Typescript => "ts",
RelayLanguageConfig::Flow => "js",
}
}
}

struct Relay {
config: Config,
file_name: FileName,
}

fn pull_first_operation_name_from_tpl(tpl: TaggedTpl) -> Option<String> {
tbezman marked this conversation as resolved.
Show resolved Hide resolved
tpl.tpl
.quasis
.into_iter()
.filter_map(|quasis| {
tbezman marked this conversation as resolved.
Show resolved Hide resolved
let template_string_content = String::from(quasis.raw.value.to_string());
tbezman marked this conversation as resolved.
Show resolved Hide resolved
let split_content = template_string_content.split(" ").collect::<Vec<&str>>();
tbezman marked this conversation as resolved.
Show resolved Hide resolved

let operation = split_content
.chunks(2)
.filter_map(|slice| {
if slice.len() == 1 {
return None;
}

let word = slice[0];
let next_word = slice[1];

if word == "query" || word == "subscription" || word == "mutation" {
tbezman marked this conversation as resolved.
Show resolved Hide resolved
return Some(String::from(next_word));
}

None
})
.next();

return operation;
})
.next()
}

fn build_require_expr_from_path(path: String) -> Expr {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not directly related to this specific PR, but I wonder how this can be implemented here.

Currently, our babel plugin fundamentally does two things: 1) adds require('...') calls instead of graphql tags. and 2) warns if the text operation/fragment is changed, but was not recompile, and displays a runtime warning: https://github.com/facebook/relay/blob/main/packages/babel-plugin-relay/compileGraphQLTag.js#L128

Not sure if there is JS runtime part in next.js that can also handle that second part.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alunyov Can you explain the use case for 2? Might be able to get away with less complexity if we don't add it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tbezman Sure! A developer changed the fragment or a query (added/changed field, etc), but didn't run the relay-compiler. When they refresh the page, the generated artifact for this file (.graphql.js file) won't contain the changes, this sometimes may lead to confusing situations, when developers may not see changes they are expecting on the screen.

To reduce the confusion, In the babel plugin we're comping the md5 hash of the text inside graphql tag. And we're comparing this hash to the hash in the generated artifact (created by the compiler), if these hashes are different - we're showing a warning in the browser's console. When developers debug the outdated views, they may see the reason in the console.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes a ton of sense. @alunyov Is there somewhere in the relay source where y'all export a function to hash a query / mutation / fragment / subscription? Hoping we can avoid duplicating functionality here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, maybe we can address this in a follow up PR. I'm helping a team migrate to Relay / GraphQL this week and want to keep them on the Rust compiler.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tbezman sure, totally fine to do this in a follow-up.

This is what we're using to create these source_hashes:

https://github.com/facebook/relay/blob/14550c8bd80e53369852db66d7681f13d6dd5f84/compiler/crates/relay-compiler/src/build_project/build_ir.rs#L33-L36

tbezman marked this conversation as resolved.
Show resolved Hide resolved
Expr::Call(CallExpr {
span: Default::default(),
tbezman marked this conversation as resolved.
Show resolved Hide resolved
callee: ExprOrSuper::Expr(Box::new(Expr::Ident(Ident::new(
tbezman marked this conversation as resolved.
Show resolved Hide resolved
js_word!("require"),
Span::default(),
)))),
args: vec![ExprOrSpread {
tbezman marked this conversation as resolved.
Show resolved Hide resolved
spread: None,
expr: Box::new(Expr::Lit(Lit::Str(Str {
span: Default::default(),
value: JsWord::from(path),
has_escape: false,
kind: Default::default(),
}))),
}],
type_args: None,
})
}

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

match expr.clone() {
tbezman marked this conversation as resolved.
Show resolved Hide resolved
Expr::TaggedTpl(tpl) => {
if let Some(built_expr) = self.build_call_expr_from_tpl(tpl) {
built_expr
} else {
expr
}
}
_ => expr,
}
}
}

impl Relay {
fn build_call_expr_from_tpl(&mut self, tpl: TaggedTpl) -> Option<Expr> {
if let Expr::Ident(ident) = *tpl.tag.clone() {
tbezman marked this conversation as resolved.
Show resolved Hide resolved
if ident.sym.to_string() != "graphql" {
tbezman marked this conversation as resolved.
Show resolved Hide resolved
return None;
}
}

let operation_name = pull_first_operation_name_from_tpl(tpl);

if let (Some(operation_name), Real(source_path_buf)) =
(operation_name, self.file_name.clone())
tbezman marked this conversation as resolved.
Show resolved Hide resolved
{
let path_to_source_dir = source_path_buf.parent().unwrap();
let generated_file_name = format!(
"{}.graphql.{}",
operation_name,
self.config.file_extension().clone()
tbezman marked this conversation as resolved.
Show resolved Hide resolved
);

let fully_qualified_require_path = match self.config.artifact_directory.clone() {
tbezman marked this conversation as resolved.
Show resolved Hide resolved
tbezman marked this conversation as resolved.
Show resolved Hide resolved
Some(artifact_directory) => std::env::current_dir()
tbezman marked this conversation as resolved.
Show resolved Hide resolved
.unwrap()
.join(artifact_directory)
.join(generated_file_name),
_ => path_to_source_dir
.clone()
.join("__generated__")
.join(generated_file_name),
};

let mut require_path = String::from(
diff_paths(fully_qualified_require_path, path_to_source_dir)
.unwrap()
.to_str()
.unwrap(),
);

if !require_path.starts_with(".") {
require_path = format!("./{}", require_path);
}

return Some(build_require_expr_from_path(require_path));
}

None
}
}

pub fn relay(config: Config, file_name: FileName) -> impl Fold {
Relay { config, file_name }
}
45 changes: 45 additions & 0 deletions packages/next-swc/crates/core/tests/fixture.rs
@@ -1,3 +1,4 @@
use next_swc::relay::{relay, RelayLanguageConfig};
use next_swc::{
amp_attributes::amp_attributes,
next_dynamic::next_dynamic,
Expand All @@ -11,6 +12,7 @@ use next_swc::{
use std::path::PathBuf;
use swc_common::{chain, comments::SingleThreadedComments, FileName, Mark, Span, DUMMY_SP};
use swc_ecma_transforms_testing::{test, test_fixture};
use swc_ecmascript::parser::TsConfig;
use swc_ecmascript::{
parser::{EsConfig, Syntax},
transforms::{react::jsx, resolver},
Expand Down Expand Up @@ -149,6 +151,49 @@ fn page_config_fixture(input: PathBuf) {
test_fixture(syntax(), &|_tr| page_config_test(), &input, &output);
}

#[fixture("tests/fixture/relay/no-artifact-dir/**/input.ts*")]
fn relay_no_artifact_dir_fixture(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");
test_fixture(
syntax(),
&|_tr| {
relay(
next_swc::relay::Config {
artifact_directory: None,
language: RelayLanguageConfig::Typescript,
},
FileName::Real(PathBuf::from(input.clone())),
)
},
&input,
&output,
);
}

#[fixture("tests/fixture/relay/artifact-dir/**/input.ts*")]
fn relay_fixture(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");
test_fixture(
Syntax::Typescript(TsConfig {
tsx: true,
decorators: false,
dts: false,
no_early_errors: false,
}),
&|_tr| {
relay(
next_swc::relay::Config {
artifact_directory: Some(PathBuf::from("some/generated/dir")),
language: RelayLanguageConfig::Typescript,
},
FileName::Real(PathBuf::from(input.clone())),
)
},
&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
@@ -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
}
`)
}
@@ -0,0 +1,10 @@
const variableQuery = require("../../../../../../some/generated/dir/InputVariableQuery.graphql.ts");
fetchQuery(require("../../../../../../some/generated/dir/InputUsedInFunctionCallQuery.graphql.ts"));
function SomeQueryComponent() {
useLazyLoadQuery(require("../../../../../../some/generated/dir/InputInHookQuery.graphql.ts"));
}
const variableMutation = require("../../../../../../some/generated/dir/InputVariableMutation.graphql.ts");
commitMutation(environment, require("../../../../../../some/generated/dir/InputUsedInFunctionCallMutation.graphql.ts"));
function SomeMutationComponent() {
useMutation(require("../../../../../../some/generated/dir/InputInHookMutation.graphql.ts"));
}
@@ -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
}
`)
}
@@ -0,0 +1,13 @@
const variableQuery = require('./__generated__/InputVariableQuery.graphql.ts')
fetchQuery(require('./__generated__/InputUsedInFunctionCallQuery.graphql.ts'))
function SomeQueryComponent() {
useLazyLoadQuery(require('./__generated__/InputInHookQuery.graphql.ts'))
}
const variableMutation = require('./__generated__/InputVariableMutation.graphql.ts')
commitMutation(
environment,
require('./__generated__/InputUsedInFunctionCallMutation.graphql.ts')
)
function SomeMutationComponent() {
useMutation(require('./__generated__/InputInHookMutation.graphql.ts'))
}
1 change: 1 addition & 0 deletions packages/next/build/swc/options.js
Expand Up @@ -75,6 +75,7 @@ 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 @@ -1614,6 +1614,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