Relay Support in Rust Compiler (#33240)

## Feature

Implements feature requested in https://github.com/vercel/next.js/issues/30805. 

A few people including myself have been looking to use Relay with Next.JS and want to use the new Rust Compiler. This is my stab at an implementation. 

### How it works?
Finds all  `graphql` tagged template experssions and replaces them with `require`s to the file generated by Relay.

### Where I need help
- I've only worked with Rust a handful of times so I would appreciate any feedback on my use of language features.
- Is there any performance overhead to many duplicate usages of `require`? I imagine there's a cache in place but I want to be sure.
- I've added some unit tests & integration tests but I might be missing some use cases. Feel free to comment some use cases I'm not thinking about.

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Documentation added
  - I haven't added any docs since this is an experimental API.

## Documentation / Examples

You're expected to be running the Relay Compiler along side Next.JS when you're developing. This is pretty standard. I wouldn't expect people to have any problem with this.

### Usage
In your `next.config.js`
```js
module.exports = {
  experimental: {
    relay: {
      language: 'typescript', // or 'javascript`
      artifactDirectory: 'path/to/you/artifact/directory' // you can leave this undefined if you did not specify one in the `relay.json`
    }
  }
}
```


Co-authored-by: Tim Neutkens <6324199+timneutkens@users.noreply.github.com>
This commit is contained in:
Terence Bezman 2022-01-26 00:23:57 -08:00 committed by GitHub
parent 99d4d6c5a4
commit b20eb99a4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 2421 additions and 138 deletions

View file

@ -94,6 +94,19 @@ const customJestConfig = {
module.exports = createJestConfig(customJestConfig)
```
### Relay
To enable [Relay](https://relay.dev/) support:
```js
// next.config.js
module.exports = {
experimental: {
relay: true,
},
}
```
### Remove React Properties
Allows to remove JSX properties. This is often used for testing. Similar to `babel-plugin-react-remove-properties`.

View file

@ -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",
@ -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",
"release": "6.3.0",
"request-promise-core": "1.1.2",
"resolve-from": "5.0.0",

File diff suppressed because it is too large Load diff

View file

@ -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"
@ -26,6 +27,12 @@ swc_stylis = "0.43.0"
tracing = {version = "0.1.28", features = ["release_max_level_off"]}
regex = "1.5"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
relay-compiler = { git = "https://github.com/facebook/relay", tag="v13.0.1" }
relay-compiler-intern = { package="intern", git = "https://github.com/facebook/relay", tag="v13.0.1" }
relay-compiler-common = { package = "common", git = "https://github.com/facebook/relay", tag="v13.0.1" }
relay-config = { git = "https://github.com/facebook/relay", tag="v13.0.1" }
[dev-dependencies]
swc_ecma_transforms_testing = "0.59.0"
testing = "0.18.0"

View file

@ -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;
@ -91,6 +93,9 @@ pub struct TransformOptions {
#[serde(default)]
pub react_remove_properties: Option<react_remove_properties::Config>,
#[serde(default)]
pub relay: bool,
#[serde(default)]
pub shake_exports: Option<shake_exports::Config>,
}
@ -100,6 +105,17 @@ pub fn custom_before_pass(
file: Arc<SourceFile>,
opts: &TransformOptions,
) -> impl Fold {
#[cfg(target_arch = "wasm32")]
let relay_plugin = noop();
#[cfg(not(target_arch = "wasm32"))]
let relay_plugin = {
match &opts.relay {
true => Either::Left(relay::relay(file.name.clone())),
false => 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()),
@ -130,6 +146,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())),

View file

@ -0,0 +1,277 @@
use once_cell::sync::Lazy;
use pathdiff::diff_paths;
use regex::Regex;
use relay_compiler::compiler_state::{SourceSet, SourceSetName};
use relay_compiler::{create_path_for_artifact, FileCategorizer, FileGroup, ProjectConfig};
use relay_compiler_common::SourceLocationKey;
use serde::Deserialize;
use std::borrow::Cow;
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 = "snake_case")]
pub enum RelayLanguageConfig {
Typescript,
Flow,
}
struct Relay {
file_name: FileName,
relay_config_for_tests: Option<ProjectConfig>,
}
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();
match capture_group {
None => None,
Some(capture_group) => Some(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 Fold for Relay {
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<'a> {
FileNameNotReal,
MultipleSourceSetsFound {
source_set_names: Vec<SourceSetName>,
path: &'a PathBuf,
},
ProjectNotFoundForSourceSet {
source_set_name: SourceSetName,
},
FileNotASourceFile,
CouldNotCategorize {
err: Cow<'static, str>,
path: String,
},
}
// This is copied from https://github.com/facebook/relay/blob/main/compiler/crates/relay-compiler/src/build_project/generate_artifacts.rs#L251
// until the Relay team exposes it for external use.
fn path_for_artifact(
root_dir: &Path,
source_path: &Path,
project_config: &ProjectConfig,
definition_name: &str,
) -> PathBuf {
let source_file_location_key = SourceLocationKey::Standalone {
path: source_path.to_str().unwrap().parse().unwrap(),
};
let filename = if let Some(filename_for_artifact) = &project_config.filename_for_artifact {
filename_for_artifact(source_file_location_key, definition_name.parse().unwrap())
} else {
match &project_config.typegen_config.language {
relay_config::TypegenLanguage::Flow => format!("{}.graphql.js", definition_name),
relay_config::TypegenLanguage::TypeScript => {
format!("{}.graphql.ts", definition_name)
}
}
};
let output_path = create_path_for_artifact(project_config, source_file_location_key, filename);
if project_config.output.is_some() {
let absolute_output_path = root_dir.join(&output_path);
let diffed_path =
diff_paths(&absolute_output_path, &source_path.parent().unwrap()).unwrap();
return diffed_path;
}
output_path
}
impl Relay {
fn build_require_path(
&mut self,
operation_name: &str,
) -> Result<PathBuf, BuildRequirePathError> {
match &self.relay_config_for_tests {
Some(config) => match &self.file_name {
FileName::Real(real_file_path) => Ok(path_for_artifact(
&std::env::current_dir().unwrap(),
&real_file_path,
config,
operation_name,
)),
_ => Err(BuildRequirePathError::FileNameNotReal),
},
_ => {
let config =
relay_compiler::config::Config::search(&std::env::current_dir().unwrap())
.unwrap();
let categorizer = FileCategorizer::from_config(&config);
match &self.file_name {
FileName::Real(real_file_name) => {
// Make sure we have a path which is relative to the config.
// Otherwise, categorize won't be able to recognize that
// the absolute source path is a child of a source set.
let diffed_path = diff_paths(real_file_name, &config.root_dir).unwrap();
let group = categorizer.categorize(diffed_path.as_path());
match group {
Ok(group) => match group {
FileGroup::Source { source_set } => match source_set {
SourceSet::SourceSetName(source_set_name) => {
let project_config: Option<&ProjectConfig> =
config.projects.get(&source_set_name);
match project_config {
None => Err(BuildRequirePathError::ProjectNotFoundForSourceSet { source_set_name }),
Some(project_config) => {
Ok(path_for_artifact(&config.root_dir,real_file_name, &project_config, operation_name))
}
}
}
SourceSet::SourceSetNames(source_set_names) => {
Err(BuildRequirePathError::MultipleSourceSetsFound {
source_set_names,
path: real_file_name,
})
}
},
_ => Err(BuildRequirePathError::FileNotASourceFile),
},
Err(err) => Err(BuildRequirePathError::CouldNotCategorize {
err,
path: real_file_name.display().to_string(),
}),
}
}
_ => 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::MultipleSourceSetsFound {
source_set_names,
path,
} => {
format!(
"Multiple source sets were found for file: {}. Found source sets: \
[{}]. We could not determine the project config to use for the \
source file. Please consider narrowing down your source sets.",
path.to_str().unwrap(),
source_set_names
.iter()
.map(|name| name.lookup())
.collect::<Vec<&str>>()
.join(", ")
)
}
BuildRequirePathError::ProjectNotFoundForSourceSet { source_set_name } => {
format!(
"Project could not be found for the source set: {}",
source_set_name
)
}
BuildRequirePathError::FileNotASourceFile => {
"This file was not considered a source file by the Relay Compiler. \
This is likely a bug and should be reported to Next.js"
.to_string()
}
BuildRequirePathError::CouldNotCategorize { path, err } => {
format!(
"Relay was unable to categorize the file at: {}. Ensure your \
`src` path includes this file in `relay.config.js` The \
underlying error is: {}. \n\nThis is likely a bug and should be \
reported to Next.js",
path, err
)
}
};
HANDLER.with(|handler| {
handler.span_err(
tpl.span,
format!("{} {}", base_error, error_message).as_str(),
);
});
None
}
},
}
}
}
pub fn relay(file_name: FileName) -> impl Fold {
Relay {
file_name,
relay_config_for_tests: None,
}
}
pub fn test_relay(file_name: FileName, relay_config_for_tests: ProjectConfig) -> impl Fold {
Relay {
file_name,
relay_config_for_tests: Some(relay_config_for_tests),
}
}

View file

@ -1,3 +1,4 @@
use next_swc::relay::test_relay;
use next_swc::{
amp_attributes::amp_attributes,
next_dynamic::next_dynamic,
@ -8,6 +9,8 @@ use next_swc::{
shake_exports::{shake_exports, Config as ShakeExportsConfig},
styled_jsx::styled_jsx,
};
use relay_compiler::ProjectConfig;
use relay_config::TypegenLanguage;
use std::path::PathBuf;
use swc_common::{chain, comments::SingleThreadedComments, FileName, Mark, Span, DUMMY_SP};
use swc_ecma_transforms_testing::{test, test_fixture};
@ -149,6 +152,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");
test_fixture(
syntax(),
&|_tr| {
let mut config = ProjectConfig::default();
config.typegen_config.language = TypegenLanguage::TypeScript;
test_relay(FileName::Real(PathBuf::from("input.tsx")), config)
},
&input,
&output,
);
}
#[fixture("tests/fixture/remove-console/**/input.js")]
fn remove_console_fixture(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");

View file

@ -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
}
`)
}

View file

@ -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'))
}

View file

@ -59,6 +59,7 @@ fn test(input: &Path, minify: bool) {
styled_components: Some(assert_json("{}")),
remove_console: None,
react_remove_properties: None,
relay: false,
shake_exports: None,
};

View file

@ -11,7 +11,7 @@ crate-type = ["cdylib"]
anyhow = "1.0.42"
console_error_panic_hook = "0.1.6"
once_cell = "1.3.1"
parking_lot_core = "=0.8.0"
parking_lot_core = "=0.8.5"
path-clean = "0.1"
serde = {version = "1", features = ["derive"]}
serde_json = "1"

View file

@ -75,6 +75,7 @@ function getBaseSWCOptions({
: null,
removeConsole: nextConfig?.experimental?.removeConsole,
reactRemoveProperties: nextConfig?.experimental?.reactRemoveProperties,
relay: nextConfig?.experimental?.relay,
}
}

View file

@ -1617,6 +1617,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 = {

View file

@ -172,6 +172,7 @@ export type NextConfig = { [key: string]: any } & {
urlImports?: NonNullable<webpack5.Configuration['experiments']>['buildHttp']
outputFileTracingRoot?: string
outputStandalone?: boolean
relay?: boolean
}
}

View file

@ -0,0 +1,63 @@
/**
* @generated SignedSource<<187ead9fb6e7b26d71c9161bda6ab902>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime'
export type pagesQuery$variables = {}
export type pagesQueryVariables = pagesQuery$variables
export type pagesQuery$data = {
readonly greeting: string
}
export type pagesQueryResponse = pagesQuery$data
export type pagesQuery = {
variables: pagesQueryVariables
response: pagesQuery$data
}
const node: ConcreteRequest = (function () {
var v0 = [
{
alias: null,
args: null,
kind: 'ScalarField',
name: 'greeting',
storageKey: null,
},
]
return {
fragment: {
argumentDefinitions: [],
kind: 'Fragment',
metadata: null,
name: 'pagesQuery',
selections: v0 /*: any*/,
type: 'Query',
abstractKey: null,
},
kind: 'Request',
operation: {
argumentDefinitions: [],
kind: 'Operation',
name: 'pagesQuery',
selections: v0 /*: any*/,
},
params: {
cacheID: '167b6de16340efeb876a7787c90e7cec',
id: null,
metadata: {},
name: 'pagesQuery',
operationKind: 'query',
text: 'query pagesQuery {\n greeting\n}\n',
},
}
})()
;(node as any).hash = '4017856344f36f61252354e2eb442d98'
export default node

View file

@ -0,0 +1,6 @@
module.exports = {
experimental: {
relay: true,
externalDir: true,
},
}

View file

@ -0,0 +1,12 @@
import { NextApiRequest, NextApiResponse } from 'next'
export default function handler(
req: NextApiRequest,
res: NextApiResponse<{ data: { greeting: string } }>
) {
res.status(200).json({
data: {
greeting: 'Hello, World!',
},
})
}

View file

@ -0,0 +1,63 @@
import {
Environment,
FetchFunction,
fetchQuery,
graphql,
Network,
RecordSource,
Store,
} from 'relay-runtime'
import { GetServerSideProps } from 'next'
import { pagesQuery } from '../../__generated__/pagesQuery.graphql'
type Props = { greeting: string }
export default function Index({ greeting }: Props) {
return <p>Project A:{greeting}</p>
}
function createGraphQLFetcher(host: string | undefined): FetchFunction {
return async function fetchGraphQL(params, variables) {
const url = host ? `http://${host}/api/query` : `/api/query`
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: params.text,
variables,
}),
})
return await response.json()
}
}
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const environment = new Environment({
store: new Store(new RecordSource({}), {}),
network: Network.create(createGraphQLFetcher(req.headers.host)),
})
const result = await fetchQuery<pagesQuery>(
environment,
graphql`
query pagesQuery {
greeting
}
`,
{}
).toPromise()
if (!result) {
throw new Error(
'Mock GraphQL Server network request finished without a response!'
)
}
return {
props: { greeting: result.greeting },
}
}

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"baseUrl": ".",
"esModuleInterop": true,
"module": "esnext",
"jsx": "preserve",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

View file

@ -0,0 +1,6 @@
module.exports = {
experimental: {
relay: true,
externalDir: true,
},
}

View file

@ -0,0 +1,12 @@
import { NextApiRequest, NextApiResponse } from 'next'
export default function handler(
req: NextApiRequest,
res: NextApiResponse<{ data: { greeting: string } }>
) {
res.status(200).json({
data: {
greeting: 'Hello, World!',
},
})
}

View file

@ -0,0 +1,63 @@
import {
Environment,
FetchFunction,
fetchQuery,
graphql,
Network,
RecordSource,
Store,
} from 'relay-runtime'
import { GetServerSideProps } from 'next'
import { pagesQuery } from '../../__generated__/pagesQuery.graphql'
type Props = { greeting: string }
export default function Index({ greeting }: Props) {
return <p>Project B:{greeting}</p>
}
function createGraphQLFetcher(host: string | undefined): FetchFunction {
return async function fetchGraphQL(params, variables) {
const url = host ? `http://${host}/api/query` : `/api/query`
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: params.text,
variables,
}),
})
return await response.json()
}
}
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const environment = new Environment({
store: new Store(new RecordSource({}), {}),
network: Network.create(createGraphQLFetcher(req.headers.host)),
})
const result = await fetchQuery<pagesQuery>(
environment,
graphql`
query pagesQuery {
greeting
}
`,
{}
).toPromise()
if (!result) {
throw new Error(
'Mock GraphQL Server network request finished without a response!'
)
}
return {
props: { greeting: result.greeting },
}
}

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"baseUrl": ".",
"esModuleInterop": true,
"module": "esnext",
"jsx": "preserve",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

View file

@ -0,0 +1,19 @@
module.exports = {
root: './',
sources: {
'project-a/pages': 'project-a',
'project-b/pages': 'project-b',
},
projects: {
'project-a': {
schema: 'schema.graphql',
language: 'typescript',
output: '__generated__',
},
'project-b': {
schema: 'schema.graphql',
language: 'typescript',
output: '__generated__',
},
},
}

View file

@ -0,0 +1,3 @@
type Query {
greeting: String!
}

View file

@ -0,0 +1,89 @@
/* eslint-env jest */
import { join } from 'path'
import { execSync } from 'child_process'
import {
findPort,
killApp,
launchApp,
nextBuild,
nextStart,
renderViaHTTP,
} from 'next-test-utils'
let app
let appPort
const projectAAppDir = join(__dirname, '../project-a')
const projectBAppDir = join(__dirname, '../project-b')
const runTests = (project) => {
it('should resolve index page correctly', async () => {
const html = await renderViaHTTP(appPort, '/')
expect(html).toContain(project)
expect(html).toContain(`Hello, World!`)
})
}
const runRelayCompiler = () => {
// Relay expects the current directory to contain a relay.json
// This ensures the CWD is the one with relay.json since running
// the relay-compiler through yarn would make the root of the repo the CWD.
execSync('../../../node_modules/relay-compiler/cli.js', {
cwd: './test/integration/relay-graphql-swc-multi-project',
})
}
describe('Relay Compiler Transform - Multi Project Config', () => {
beforeAll(() => {
runRelayCompiler()
})
describe('dev mode', () => {
describe('project-a', () => {
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(projectAAppDir, appPort, { cwd: projectAAppDir })
})
afterAll(() => killApp(app))
runTests('Project A')
})
describe('project-b', () => {
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(projectBAppDir, appPort, { cwd: projectBAppDir })
})
afterAll(() => killApp(app))
runTests('Project B')
})
})
describe('production mode', () => {
describe('project-a', () => {
beforeAll(async () => {
await nextBuild(projectAAppDir, [], { cwd: projectAAppDir })
appPort = await findPort()
app = await nextStart(projectAAppDir, appPort)
})
afterAll(() => killApp(app))
runTests('Project A')
})
describe('project-b', () => {
beforeAll(async () => {
await nextBuild(projectBAppDir, [], { cwd: projectBAppDir })
appPort = await findPort()
app = await nextStart(projectBAppDir, appPort)
})
afterAll(() => killApp(app))
runTests('Project B')
})
})
})

View file

@ -0,0 +1,63 @@
/**
* @generated SignedSource<<187ead9fb6e7b26d71c9161bda6ab902>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime'
export type pagesQuery$variables = {}
export type pagesQueryVariables = pagesQuery$variables
export type pagesQuery$data = {
readonly greeting: string
}
export type pagesQueryResponse = pagesQuery$data
export type pagesQuery = {
variables: pagesQueryVariables
response: pagesQuery$data
}
const node: ConcreteRequest = (function () {
var v0 = [
{
alias: null,
args: null,
kind: 'ScalarField',
name: 'greeting',
storageKey: null,
},
]
return {
fragment: {
argumentDefinitions: [],
kind: 'Fragment',
metadata: null,
name: 'pagesQuery',
selections: v0 /*: any*/,
type: 'Query',
abstractKey: null,
},
kind: 'Request',
operation: {
argumentDefinitions: [],
kind: 'Operation',
name: 'pagesQuery',
selections: v0 /*: any*/,
},
params: {
cacheID: '167b6de16340efeb876a7787c90e7cec',
id: null,
metadata: {},
name: 'pagesQuery',
operationKind: 'query',
text: 'query pagesQuery {\n greeting\n}\n',
},
}
})()
;(node as any).hash = '4017856344f36f61252354e2eb442d98'
export default node

View file

@ -0,0 +1,5 @@
module.exports = {
experimental: {
relay: true,
},
}

View file

@ -0,0 +1,12 @@
import { NextApiRequest, NextApiResponse } from 'next'
export default function handler(
req: NextApiRequest,
res: NextApiResponse<{ data: { greeting: string } }>
) {
res.status(200).json({
data: {
greeting: 'Hello, World!',
},
})
}

View file

@ -0,0 +1,63 @@
import {
Environment,
FetchFunction,
fetchQuery,
graphql,
Network,
RecordSource,
Store,
} from 'relay-runtime'
import { GetServerSideProps } from 'next'
import { pagesQuery } from '../__generated__/pagesQuery.graphql'
type Props = { greeting: string }
export default function Index({ greeting }: Props) {
return <p>{greeting}</p>
}
function createGraphQLFetcher(host: string | undefined): FetchFunction {
return async function fetchGraphQL(params, variables) {
const url = host ? `http://${host}/api/query` : `/api/query`
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: params.text,
variables,
}),
})
return await response.json()
}
}
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const environment = new Environment({
store: new Store(new RecordSource({}), {}),
network: Network.create(createGraphQLFetcher(req.headers.host)),
})
const result = await fetchQuery<pagesQuery>(
environment,
graphql`
query pagesQuery {
greeting
}
`,
{}
).toPromise()
if (!result) {
throw new Error(
'Mock GraphQL Server network request finished without a response!'
)
}
return {
props: { greeting: result.greeting },
}
}

View file

@ -0,0 +1,6 @@
module.exports = {
src: 'pages',
schema: './schema.graphql',
artifactDirectory: './__generated__',
language: 'typescript',
}

View file

@ -0,0 +1,3 @@
type Query {
greeting: String!
}

View file

@ -0,0 +1,57 @@
/* eslint-env jest */
import { join } from 'path'
import { execSync } from 'child_process'
import {
findPort,
killApp,
launchApp,
nextBuild,
nextStart,
renderViaHTTP,
} from 'next-test-utils'
let app
let appPort
const appDir = join(__dirname, '../')
const runTests = () => {
it('should resolve index page correctly', async () => {
const html = await renderViaHTTP(appPort, '/')
expect(html).toContain('Hello, World!')
})
}
const runRelayCompiler = () => {
// Relay expects the current directory to contain a relay.json
// This ensures the CWD is the one with relay.json since running
// the relay-compiler through yarn would make the root of the repo the CWD.
execSync('../../../node_modules/relay-compiler/cli.js', {
cwd: './test/integration/relay-graphql-swc-single-project',
})
}
describe('Relay Compiler Transform - Single Project Config', () => {
describe('dev mode', () => {
beforeAll(async () => {
runRelayCompiler()
appPort = await findPort()
app = await launchApp(appDir, appPort, { cwd: appDir })
})
afterAll(() => killApp(app))
runTests()
})
describe('production mode', () => {
beforeAll(async () => {
runRelayCompiler()
await nextBuild(appDir, [], { cwd: appDir })
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(() => killApp(app))
runTests()
})
})

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"baseUrl": ".",
"esModuleInterop": true,
"module": "esnext",
"jsx": "preserve",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

View file

@ -2260,6 +2260,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.2":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d"
@ -4794,6 +4801,11 @@
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/relay-runtime@13.0.0":
version "13.0.0"
resolved "https://registry.yarnpkg.com/@types/relay-runtime/-/relay-runtime-13.0.0.tgz#d0009275522ff826f2e4dab40419f2db58417ecf"
integrity sha512-yzv6F8EZPWA2rtfFP2qMluS8tsz1q4lfdYxLegCshdAjX5uqxTR2pAliATj9wrzD6OMZF4fl9aU+Y+zmSfm2EA==
"@types/resolve@1.17.1":
version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
@ -7850,6 +7862,13 @@ cross-fetch@3.0.6, cross-fetch@^3.0.6:
dependencies:
node-fetch "2.6.1"
cross-fetch@^3.0.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39"
integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==
dependencies:
node-fetch "2.6.1"
cross-spawn-async@^2.1.1:
version "2.2.5"
resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc"
@ -9720,6 +9739,24 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
fbjs-css-vars@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8"
integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==
fbjs@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-3.0.2.tgz#dfae08a85c66a58372993ce2caf30863f569ff94"
integrity sha512-qv+boqYndjElAJHNN3NoM8XuwQZ1j2m3kEvTgdle8IDjr6oUbkEpvABWtj/rQl3vq4ew7dnElBxL4YJAwTVqQQ==
dependencies:
cross-fetch "^3.0.4"
fbjs-css-vars "^1.0.0"
loose-envify "^1.0.0"
object-assign "^4.1.0"
promise "^7.1.1"
setimmediate "^1.0.5"
ua-parser-js "^0.7.30"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
@ -11419,6 +11456,13 @@ interpret@^2.2.0:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
dependencies:
loose-envify "^1.0.0"
ip@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
@ -13379,7 +13423,7 @@ longest-streak@^2.0.0:
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4"
integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==
loose-envify@^1.1.0, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
dependencies:
@ -16615,6 +16659,13 @@ promise@8.0.1:
dependencies:
asap "~2.0.3"
promise@^7.1.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
dependencies:
asap "~2.0.3"
prompts@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.1.0.tgz#bf90bc71f6065d255ea2bdc0fe6520485c1b45db"
@ -17345,6 +17396,20 @@ rehype-retext@^2.0.1:
dependencies:
hast-util-to-nlcst "^1.0.0"
relay-compiler@13.0.1:
version "13.0.1"
resolved "https://registry.yarnpkg.com/relay-compiler/-/relay-compiler-13.0.1.tgz#09c713647aa7e1d8cf3de9f7fb4ee6a76b32cf26"
integrity sha512-C/qJ7IdfZ140b9JaNpuAP6WhV/Odt/tIq4sUZoTwsaOlhs+1Zu3fvIOoWKTnZT5PC6krRuw1hD7GSX6/paVpTQ==
relay-runtime@13.0.1:
version "13.0.1"
resolved "https://registry.yarnpkg.com/relay-runtime/-/relay-runtime-13.0.1.tgz#7e59a07c3b4e8c58d04bc94f6f978822d4128ffb"
integrity sha512-n/+J8PFfLFPVUcz9OG/z2i+adnfk0INwlTkVTw0V6KJe9NI9plc5eRCJwzzwspT4pdCkis5Lcjzvzp4H+0zn8g==
dependencies:
"@babel/runtime" "^7.0.0"
fbjs "^3.0.2"
invariant "^2.2.4"
release@6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/release/-/release-6.3.0.tgz#bbd351d7460948f1ed55ea02b4b2393f98a1637a"
@ -18103,7 +18168,7 @@ set-value@^2.0.0, set-value@^2.0.1:
is-plain-object "^2.0.3"
split-string "^3.0.1"
setimmediate@1.0.5, setimmediate@^1.0.4:
setimmediate@1.0.5, setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
@ -19751,6 +19816,11 @@ ua-parser-js@0.7.28:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
ua-parser-js@^0.7.30:
version "0.7.31"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
uglify-js@^3.1.4:
version "3.7.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.3.tgz#f918fce9182f466d5140f24bb0ff35c2d32dcc6a"