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 #33702

Merged
merged 21 commits into from Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
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;
kdy1 marked this conversation as resolved.
Show resolved Hide resolved
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 = {
kdy1 marked this conversation as resolved.
Show resolved Hide resolved
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(),
Copy link

Choose a reason for hiding this comment

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

The compiler supports eager esm as well, probably worth also exposing this.

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