Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions PROJECT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Relay

GraphQL framework for React. Two main components: Rust compiler and JavaScript runtime.

# Compiler

Rust implementation in `compiler/`. Cargo workspace with crates in `compiler/crates/`.

## Formatting

All non-generated Rust code is autoformatted using `rust fmt`. Run this command before committing changes:

```bash
# Fix formatting
grep -r --include "*.rs" --files-without-match '@generated' crates | xargs rustfmt --config="skip_children=true"
```

## Fixture Tests

Compiler tests are generated from fixture files: input files with corresponding generated `.expected` output files in `crates/*/tests/*/fixtures/`.

**Never modify or create generated files directly**

**Regenerate test harness files** (after adding/removing fixtures):

Fixture `_test.rs` files are generated. After adding or removing an input fixture file regenerate the test file:

```bash
./scripts/update-fixtures.sh
```

**Update snapshots when output changes intentionally:**

To regenerate `*.expected` snapshots run:

```bash
UPDATE_SNAPSHOTS=1 cargo test
```



# Runtime

JavaScript packages in `packages/`. Main packages: `relay-runtime`, `react-relay`.

## Commands (from project root)

```bash
yarn jest # Run all tests
yarn jest <pattern> # Run matching tests
yarn jest -u # Update Jest snapshots
yarn typecheck # Flow type checking
yarn lint # ESLint
yarn prettier # Format code
./scripts/compile-test.js # Update generated Relay artifacts for test files
```
4 changes: 4 additions & 0 deletions compiler/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions compiler/crates/relay-bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ edition = "2024"
repository = "https://github.com/facebook/relay"
license = "MIT"

[[test]]
name = "relay_subschema_extraction_test"
path = "tests/subschema_extraction_test.rs"

[dependencies]
clap = { version = "4.5.42", features = ["derive", "env", "string", "unicode", "wrap_help"] }
common = { path = "../common" }
Expand All @@ -17,9 +21,15 @@ program-with-dependencies = { path = "../program-with-dependencies" }
relay-codemod = { path = "../relay-codemod" }
relay-compiler = { path = "../relay-compiler" }
relay-lsp = { path = "../relay-lsp" }
relay-transforms = { path = "../relay-transforms" }
schema = { path = "../schema" }
schema-documentation = { path = "../schema-documentation" }
schema-set = { path = "../schema-set" }
simplelog = "0.12.2"
thiserror = "2.0.12"
tokio = { version = "1.47.1", features = ["full", "test-util", "tracing"] }

[dev-dependencies]
fixture-tests = { path = "../fixture-tests" }
futures-util = { version = "0.3.30", features = ["compat"] }
graphql-test-helpers = { path = "../graphql-test-helpers" }
195 changes: 195 additions & 0 deletions compiler/crates/relay-bin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

//! Shared utilities for the Relay compiler binary.

use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;

use common::ConsoleLogger;
use program_with_dependencies::ProgramWithDependencies;
use relay_compiler::SchemaLocation;
use relay_compiler::config::Config;
use relay_compiler::get_programs;
use relay_transforms::Programs;
use schema_set::SchemaSet;
use schema_set::UsedSchemaCollectionOptions;
use thiserror::Error;

/// Errors that can occur during subschema extraction.
#[derive(Debug, Error)]
pub enum SubschemaError {
#[error("Expected exactly one project, but found {0}")]
MultipleProjects(usize),

#[error("Expected a single file schema, but found a directory schema location")]
DirectorySchemaNotSupported,

#[error("Full schema path not found: {0}")]
FullSchemaNotFound(String),

#[error("Full schema path exists but is neither a file nor directory: {0}")]
InvalidFullSchemaPath(String),

#[error("Failed to canonicalize full schema path: {0}")]
CanonicalizeFailed(String),

#[error("Full schema path must be under root dir")]
FullSchemaOutsideRoot,

#[error("Compilation failed: {0}")]
CompilationFailed(String),
}

/// Result of subschema extraction.
pub struct SubschemaResult {
/// The extracted subschema content as a GraphQL SDL string.
pub schema_content: String,
/// The path where the subschema should be written (original schema location).
pub original_schema_path: PathBuf,
}

/// Compile a project against a full schema and extract the used subschema.
///
/// This is the high-level function that:
/// 1. Validates the config has exactly one project
/// 2. Gets the original schema location
/// 3. Swaps the schema to point to the full schema
/// 4. Compiles the project
/// 5. Extracts the used subschema
///
/// Returns the extracted schema content and the path where it should be written.
pub async fn compile_and_extract_subschema(
mut config: Config,
full_schema_path: &Path,
) -> Result<SubschemaResult, SubschemaError> {
// Verify exactly one project
if config.projects.len() != 1 {
return Err(SubschemaError::MultipleProjects(config.projects.len()));
}

let project_name = config.projects.keys().next().unwrap().clone();

// Get the original schema location (where we'll write the subschema)
let original_schema_path = match &config.projects[&project_name].schema_location {
SchemaLocation::File(file) => file.clone(),
SchemaLocation::Directory(_) => {
return Err(SubschemaError::DirectorySchemaNotSupported);
}
};

// Normalize the full schema path relative to root_dir
let absolute_full_schema = if full_schema_path.is_absolute() {
full_schema_path.to_path_buf()
} else {
config.root_dir.join(full_schema_path)
};

let relative_full_schema = absolute_full_schema
.canonicalize()
.map_err(|e| SubschemaError::CanonicalizeFailed(e.to_string()))?
.strip_prefix(&config.root_dir)
.map(|p| p.to_path_buf())
.map_err(|_| SubschemaError::FullSchemaOutsideRoot)?;

// Determine schema location type
let schema_location = match fs::metadata(&absolute_full_schema) {
Ok(metadata) => {
if metadata.is_dir() {
SchemaLocation::Directory(relative_full_schema)
} else if metadata.is_file() {
SchemaLocation::File(relative_full_schema)
} else {
return Err(SubschemaError::InvalidFullSchemaPath(
full_schema_path.display().to_string(),
));
}
}
Err(_) => {
return Err(SubschemaError::FullSchemaNotFound(
full_schema_path.display().to_string(),
));
}
};

// Swap the schema location to point to the full schema
config
.projects
.get_mut(&project_name)
.unwrap()
.schema_location = schema_location;

// Compile the project to get IR
let programs_result = get_programs(config, Arc::new(ConsoleLogger))
.await
.map(|(programs, _, _)| programs.values().cloned().collect::<Vec<_>>());

let programs_vec =
programs_result.map_err(|e| SubschemaError::CompilationFailed(format!("{}", e)))?;

// Expect exactly one program based on exactly one project asserted earlier
let programs = programs_vec
.into_iter()
.next()
.expect("Expected exactly one program");

// Extract the used subschema
let schema_content = extract_subschema(&programs)?;

Ok(SubschemaResult {
schema_content,
original_schema_path,
})
}

/// Extract the used subschema from compiled programs.
///
/// This takes the compiled programs and extracts only the schema types
/// that are actually used by the project's operations and fragments.
pub fn extract_subschema(programs: &Programs) -> Result<String, SubschemaError> {
let program_with_deps = ProgramWithDependencies::from_full_program(
&programs.source.schema,
// Pass the operation text program since it has had all the Relay-specific
// features stripped out and should pass validation
&programs.operation_text,
);

let mut used_schema = SchemaSet::from_ir(
&program_with_deps,
UsedSchemaCollectionOptions {
include_implementations_when_typename_requested: None,
include_all_overlapping_concrete_types: false,
include_directives_on_schema_definitions: true,
include_directive_definitions: true,
include_implicit_output_enum_values: true,
include_implicit_input_fields_and_enum_values: true,
},
);

used_schema.fix_all_types();

let (printed_base_schema, printed_client_schema) =
used_schema.print_base_and_client_definitions();

// We should never end up with client schema in the subschema extraction process.
// This would indicate that client schema definitions are being included in the used
// schema, which shouldn't happen for server-only schema extraction. If this occurs,
// it's a bug in the schema collection logic.
if !printed_client_schema.is_empty() {
panic!("Unexpected client schema in extracted subschema. This is a bug.");
}

let mut output = printed_base_schema
.into_iter()
.collect::<Vec<_>>()
.join("\n\n");
output.push('\n');

Ok(output)
}
Loading
Loading