Relay Support in Rust Compiler (#33702)

Reverts vercel/next.js#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>
This commit is contained in:
JJ Kasper 2022-02-01 12:18:55 -06:00 committed by GitHub
parent af0d082c49
commit 3e60c232a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1049 additions and 5 deletions

View file

@ -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`.

View file

@ -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",
@ -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",

View file

@ -800,6 +800,7 @@ dependencies = [
"easy-error",
"either",
"fxhash",
"once_cell",
"pathdiff",
"regex",
"serde",

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"
@ -30,5 +31,3 @@ regex = "1.5"
swc_ecma_transforms_testing = "0.59.0"
testing = "0.18.0"
walkdir = "2.3.2"

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,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>,
}
@ -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()),
@ -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())),

View file

@ -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,
}
}

View file

@ -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,
@ -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");

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,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"));
}

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: None,
shake_exports: None,
};

View file

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

View file

@ -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 = {

View file

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

View file

@ -0,0 +1,63 @@
/**
* @generated SignedSource<<afaeba3a661c4bb0d2a399327c82d32b>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime'
export type pagesAQuery$variables = {}
export type pagesAQueryVariables = pagesAQuery$variables
export type pagesAQuery$data = {
readonly greeting: string
}
export type pagesAQueryResponse = pagesAQuery$data
export type pagesAQuery = {
variables: pagesAQueryVariables
response: pagesAQuery$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: 'pagesAQuery',
selections: v0 /*: any*/,
type: 'Query',
abstractKey: null,
},
kind: 'Request',
operation: {
argumentDefinitions: [],
kind: 'Operation',
name: 'pagesAQuery',
selections: v0 /*: any*/,
},
params: {
cacheID: 'bc59dc1b50eecd19488f004d5cd93913',
id: null,
metadata: {},
name: 'pagesAQuery',
operationKind: 'query',
text: 'query pagesAQuery {\n greeting\n}\n',
},
}
})()
;(node as any).hash = '7f699085b71746bb18cb74e3a0776f46'
export default node

View file

@ -0,0 +1,12 @@
const relay = require('../relay.config')
module.exports = {
experimental: {
relay: {
src: './pages',
artifactDirectory: './__generated__',
language: relay.projects['project-b'].language,
},
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 { pagesAQuery } from '../__generated__/pagesAQuery.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<pagesAQuery>(
environment,
graphql`
query pagesAQuery {
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,63 @@
/**
* @generated SignedSource<<9f92ea3ccfda1f64fa269e68b912abae>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime'
export type pagesBQuery$variables = {}
export type pagesBQueryVariables = pagesBQuery$variables
export type pagesBQuery$data = {
readonly greeting: string
}
export type pagesBQueryResponse = pagesBQuery$data
export type pagesBQuery = {
variables: pagesBQueryVariables
response: pagesBQuery$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: 'pagesBQuery',
selections: v0 /*: any*/,
type: 'Query',
abstractKey: null,
},
kind: 'Request',
operation: {
argumentDefinitions: [],
kind: 'Operation',
name: 'pagesBQuery',
selections: v0 /*: any*/,
},
params: {
cacheID: 'e7cc6f8c55ef42783faec7a49b72ae71',
id: null,
metadata: {},
name: 'pagesBQuery',
operationKind: 'query',
text: 'query pagesBQuery {\n greeting\n}\n',
},
}
})()
;(node as any).hash = '83bf9452eafa7635d81bdc98603cd75f'
export default node

View file

@ -0,0 +1,12 @@
const relay = require('../relay.config')
module.exports = {
experimental: {
relay: {
src: './pages',
artifactDirectory: './__generated__',
language: relay.projects['project-b'].language,
},
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 { pagesBQuery } from '../__generated__/pagesBQuery.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<pagesBQuery>(
environment,
graphql`
query pagesBQuery {
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: 'project-a/__generated__',
},
'project-b': {
schema: 'schema.graphql',
language: 'typescript',
output: 'project-b/__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,7 @@
const relay = require('./relay.config')
module.exports = {
experimental: {
relay,
},
}

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"
@ -4848,6 +4855,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"
@ -7914,6 +7926,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"
@ -9848,6 +9867,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"
@ -11586,6 +11623,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"
@ -13566,7 +13610,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:
@ -16802,6 +16846,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"
@ -17549,6 +17600,20 @@ rehype-retext@^2.0.1:
dependencies:
hast-util-to-nlcst "^1.0.0"
relay-compiler@13.0.2:
version "13.0.2"
resolved "https://registry.yarnpkg.com/relay-compiler/-/relay-compiler-13.0.2.tgz#2ec086bcc55ac8cb94cc08a19be129e037ca8044"
integrity sha512-Gg/5FNj8gz03GRkG5KjMNpDB4nOeApDFZq+8CZehu3KkQQ+vNIurX6RiTY3UNUYvg5SX2A9DS5bq5iHJ8iEV3A==
relay-runtime@13.0.2:
version "13.0.2"
resolved "https://registry.yarnpkg.com/relay-runtime/-/relay-runtime-13.0.2.tgz#02adb73ddc159c57b92a23c52984bf73fae0d1fc"
integrity sha512-sP4lNGfFcOm7tQD12qlvwsiOREDgjkw4FQAjtemmtwivKOqI4qHAL22Ar62r5TPVlASn4iVWMk7rIdIJI20KGQ==
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"
@ -18307,7 +18372,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=
@ -19975,6 +20040,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"