use std::collections::{BTreeMap, BTreeSet};
use serde::Serialize;
use super::super::graphql::CompiledOperation;
use super::super::manifest::{canonical_json_value, ClientManifest};
use super::super::{
ClientCompileError, GeneratedClientFile, GeneratedClientProject, GeneratedOperationSummary,
GeneratedRoutePlan,
};
use super::commands::render_commands;
use super::common::json_string;
use super::operation::render_operation_module;
pub(crate) fn render_project(
manifest: &ClientManifest,
operations: Vec<CompiledOperation>,
) -> Result<GeneratedClientProject, ClientCompileError> {
let mut files = Vec::new();
let mut summaries = Vec::with_capacity(operations.len());
let mut routes = Vec::new();
for operation in &operations {
files.push(GeneratedClientFile {
path: operation.module_path.clone(),
contents: render_operation_module(operation, manifest)?,
});
summaries.push(GeneratedOperationSummary {
name: operation.name.clone(),
source_path: operation.source_path.clone(),
module_path: operation.module_path.clone(),
export_name: operation.export_name.clone(),
operation_hash: operation.query_hash.clone(),
live_operation_hash: operation.live.as_ref().map(|live| live.hash.clone()),
});
if let Some(route) = &operation.route {
routes.push(route.clone());
}
}
routes.sort_by(|left, right| {
left.route
.cmp(&right.route)
.then_with(|| left.operation.cmp(&right.operation))
});
files.push(GeneratedClientFile {
path: "commands.ts".into(),
contents: render_commands(manifest)?,
});
let pures = super::commands::render_pures(manifest)?;
let has_pures = pures.is_some();
if let Some(pures) = pures {
files.push(GeneratedClientFile {
path: "pures.ts".into(),
contents: pures,
});
}
files.push(GeneratedClientFile {
path: "protocol.ts".into(),
contents: render_protocol(manifest)?,
});
files.push(GeneratedClientFile {
path: "routes.ts".into(),
contents: render_routes(&routes, &operations)?,
});
files.push(GeneratedClientFile {
path: "sveltekit.ts".into(),
contents: render_sveltekit(manifest, &operations)?,
});
files.push(GeneratedClientFile {
path: "index.ts".into(),
contents: render_index(&operations, has_pures),
});
files.push(GeneratedClientFile {
path: "manifest.json".into(),
contents: render_compiler_manifest(manifest, &summaries, &routes)?,
});
files.sort_by(|left, right| left.path.cmp(&right.path));
Ok(GeneratedClientProject {
files,
operations: summaries,
routes,
schema_fingerprint: manifest.schema_fingerprint.clone(),
protocol_fingerprint: manifest.protocol_fingerprint.clone(),
})
}
fn render_protocol(manifest: &ClientManifest) -> Result<String, ClientCompileError> {
let operations =
serde_json::to_string_pretty(&manifest.protocol_operations).map_err(|error| {
ClientCompileError::manifest(
"client.render.protocol",
format!("failed to render protocol artifacts: {error}"),
)
})?;
let trusted_presets =
serde_json::to_string_pretty(&manifest.trusted_presets).map_err(|error| {
ClientCompileError::manifest(
"client.render.trusted_presets",
format!("failed to render trusted-preset inventory: {error}"),
)
})?;
let mut sections =
vec!["/** GENERATED by distributed client. Exact framework-owned operation bytes. */".to_string()];
if manifest.protocol_operations.command_status.is_some() {
sections.push(
"import type { ReplicaCommandStatusArtifact } from '@hops-ops/distributed/replica';"
.into(),
);
}
sections.push(format!(
"export const CLIENT_PROTOCOL = {{\n\
\tversion: 1,\n\
\tserviceId: {},\n\
\tschemaHash: {},\n\
\tprotocolHash: {},\n\
\tsurface: {},\n\
\ttrustedPresets: {trusted_presets},\n\
\toperations: {operations}\n\
}} as const;",
json_string(&manifest.service_id)?,
json_string(&manifest.schema_fingerprint)?,
json_string(&manifest.protocol_fingerprint)?,
serde_json::to_string(&manifest.surface).map_err(|error| {
ClientCompileError::manifest(
"client.render.protocol",
format!("failed to render client surface selector: {error}"),
)
})?,
));
if let Some(status) = &manifest.protocol_operations.command_status {
let artifact = canonical_json_value(serde_json::json!({
"name": &status.name,
"document": &status.operation,
"operationHash": &status.operation_hash,
"protocol": {
"version": 1,
"schemaHash": &manifest.schema_fingerprint,
"protocolHash": &manifest.protocol_fingerprint,
"surface": &manifest.surface,
"operation": &status.operation_hash,
"trustedPresets": &manifest.trusted_presets,
}
}));
let artifact = serde_json::to_string_pretty(&artifact).map_err(|error| {
ClientCompileError::manifest(
"client.render.command_status",
format!("failed to render command-status artifact: {error}"),
)
})?;
sections.push(format!(
"/** Exact compiler-owned operation used to recover ambiguous command outcomes. */\n\
export const COMMAND_STATUS: ReplicaCommandStatusArtifact = {artifact};"
));
}
Ok(format!("{}\n", sections.join("\n\n")))
}
#[derive(Serialize)]
struct CompilerManifest<'a> {
compiler_manifest_version: u32,
distributed_manifest_version: u32,
protocol_version: u32,
service_id: &'a str,
surface: &'a super::super::manifest::ManifestSurface,
schema_fingerprint: &'a str,
protocol_fingerprint: &'a str,
scalar_codecs: &'a BTreeMap<String, String>,
commands_requiring_revalidation: &'a BTreeSet<String>,
operations: &'a [GeneratedOperationSummary],
routes: &'a [GeneratedRoutePlan],
}
fn render_compiler_manifest(
manifest: &ClientManifest,
operations: &[GeneratedOperationSummary],
routes: &[GeneratedRoutePlan],
) -> Result<String, ClientCompileError> {
let provenance = CompilerManifest {
compiler_manifest_version: 1,
distributed_manifest_version: 2,
protocol_version: 1,
service_id: &manifest.service_id,
surface: &manifest.surface,
schema_fingerprint: &manifest.schema_fingerprint,
protocol_fingerprint: &manifest.protocol_fingerprint,
scalar_codecs: &manifest.scalar_codecs,
commands_requiring_revalidation: &manifest.commands_requiring_revalidation,
operations,
routes,
};
serde_json::to_string_pretty(&provenance)
.map(|rendered| format!("{rendered}\n"))
.map_err(|error| {
ClientCompileError::manifest(
"client.render.manifest",
format!("failed to render compiler provenance manifest: {error}"),
)
})
}
fn render_routes(
routes: &[GeneratedRoutePlan],
operations: &[CompiledOperation],
) -> Result<String, ClientCompileError> {
let routes_json = serde_json::to_string_pretty(routes).map_err(|error| {
ClientCompileError::manifest(
"client.render.routes",
format!("failed to render route plan: {error}"),
)
})?;
let mut imports = Vec::new();
let mut bindings = Vec::new();
for (index, route) in routes.iter().enumerate() {
let operation = operations
.iter()
.find(|operation| operation.name == route.operation)
.ok_or_else(|| {
ClientCompileError::manifest(
"client.render.routes",
format!(
"route `{}` references missing operation `{}`",
route.route, route.operation
),
)
})?;
let module = operation
.module_path
.strip_suffix(".ts")
.expect("compiler module paths end in .ts");
imports.push(format!(
"import {{ {} }} from './{module}.js';",
operation.export_name
));
bindings.push(format!(
" {{ plan: DISTRIBUTED_ROUTES[{index}], artifact: {} }}",
operation.export_name
));
}
let import_section = if imports.is_empty() {
String::new()
} else {
format!("{}\n\n", imports.join("\n"))
};
let bindings = if bindings.is_empty() {
"[]".to_string()
} else {
format!("[\n{}\n]", bindings.join(",\n"))
};
Ok(format!(
"{import_section}\
/** GENERATED framework-neutral `@load` ownership plan. */\n\
export const DISTRIBUTED_ROUTES = {routes_json} as const;\n\
\n\
/** Static route-to-artifact bindings consumed by framework SSR adapters. */\n\
export const DISTRIBUTED_ROUTE_OPERATIONS = {bindings} as const;\n\
\n\
export type DistributedRoutePlan = (typeof DISTRIBUTED_ROUTES)[number];\n\
export type DistributedRouteOperation = (typeof DISTRIBUTED_ROUTE_OPERATIONS)[number];\n"
))
}
fn render_index(operations: &[CompiledOperation], has_pures: bool) -> String {
let mut lines = vec![
"/** GENERATED public entrypoint. */".to_string(),
"export * from './commands.js';".into(),
"export * from './protocol.js';".into(),
"export * from './routes.js';".into(),
];
if has_pures {
lines.push("export * from './pures.js';".into());
}
for operation in operations {
let module = operation
.module_path
.strip_suffix(".ts")
.expect("compiler module paths end in .ts");
lines.push(format!("export * from './{module}.js';"));
}
format!("{}\n", lines.join("\n"))
}
fn render_sveltekit(
manifest: &ClientManifest,
operations: &[CompiledOperation],
) -> Result<String, ClientCompileError> {
if let Some(operation) = operations
.iter()
.find(|operation| !typescript_value_binding(&operation.name))
{
return Err(ClientCompileError::source(
"client.operation.sveltekit_identifier",
format!(
"operation `{}` cannot be exported as a `$distributed` value because it is reserved in JavaScript/TypeScript; rename the operation",
operation.name
),
&operation.source_path,
operation.source_line,
operation.source_column,
));
}
let mut value_exports = BTreeSet::from([
"COMMAND_ARTIFACTS".to_string(),
"COMMANDS".to_string(),
"DISTRIBUTED_ROUTES".to_string(),
"DISTRIBUTED_ROUTE_OPERATIONS".to_string(),
"PROJECTOR_ARTIFACTS".to_string(),
"provideDistributed".to_string(),
"useCommands".to_string(),
]);
if !manifest.commands.is_empty() {
value_exports.insert("createCommands".into());
}
for command in &manifest.commands {
value_exports.insert(format!("Command_{}", command.mutation_field));
value_exports.insert(format!("prepareCommand_{}", command.mutation_field));
}
for operation in operations {
value_exports.insert(operation.export_name.clone());
value_exports.insert(format!("{}Document", operation.export_name));
}
if let Some(operation) = operations
.iter()
.find(|operation| value_exports.contains(&operation.name))
{
return Err(ClientCompileError::source(
"client.operation.sveltekit_export_collision",
format!(
"operation `{}` collides with the generated `$distributed` export namespace; rename the operation",
operation.name
),
&operation.source_path,
operation.source_line,
operation.source_column,
));
}
let mut sections = vec![
"/** GENERATED by distributed client. Do not edit. */".to_string(),
[
"import {",
" createDistributedSvelteKit,",
" defineDistributedSvelteKitOperation,",
" provideDistributedSvelteKitClient,",
" useDistributedSvelteKitCommands",
"} from '@hops-ops/distributed/sveltekit';",
"",
"import type {",
" CreateDistributedSvelteKitOptions,",
" DistributedSvelteKitClient",
"} from '@hops-ops/distributed/sveltekit';",
]
.join("\n"),
];
if manifest.commands.is_empty() {
sections
.push("export type GeneratedCommands = Readonly<Record<never, never>>;".to_string());
} else {
sections.push(
[
"import {",
" createCommands as createGeneratedCommands,",
" type GeneratedCommands",
"} from './commands.js';",
"",
"export type { GeneratedCommands } from './commands.js';",
]
.join("\n"),
);
}
for (index, operation) in operations.iter().enumerate() {
let module = operation
.module_path
.strip_suffix(".ts")
.expect("compiler module paths end in .ts");
sections.push(format!(
"import {{ {} as DistributedOperation_{index} }} from './{module}.js';",
operation.export_name
));
}
sections.push(
[
"/** Inspectable framework-neutral artifacts remain available here. */",
"export * from './index.js';",
]
.join("\n"),
);
for (index, operation) in operations.iter().enumerate() {
sections.push(format!(
"/** Tree-local Svelte binding for the generated `{}` artifact. */\nexport const {} = defineDistributedSvelteKitOperation(DistributedOperation_{index});",
operation.name, operation.name
));
}
let mut bindings = vec![
"/**".to_string(),
" * Create and install one component-tree/request-local generated client.".to_string(),
" * No client or command proxy is retained by this module.".to_string(),
" */".to_string(),
"export function provideDistributed(".to_string(),
" options: Omit<CreateDistributedSvelteKitOptions<GeneratedCommands>, 'createCommands'>"
.to_string(),
"): DistributedSvelteKitClient<GeneratedCommands> {".to_string(),
" return provideDistributedSvelteKitClient(".to_string(),
" createDistributedSvelteKit<GeneratedCommands>({".to_string(),
];
if manifest.commands.is_empty() {
bindings.push(" ...options".to_string());
} else {
bindings.extend([
" ...options,".to_string(),
" createCommands: createGeneratedCommands".to_string(),
]);
}
bindings.extend(
[
" })",
" );",
"}",
"",
"/** Resolve the nearest generated command surface during component initialization. */",
"export function useCommands(): GeneratedCommands {",
" return useDistributedSvelteKitCommands<GeneratedCommands>();",
"}",
]
.into_iter()
.map(str::to_string),
);
sections.push(bindings.join("\n"));
Ok(format!("{}\n", sections.join("\n\n")))
}
fn typescript_value_binding(name: &str) -> bool {
!matches!(
name,
"abstract"
| "any"
| "arguments"
| "as"
| "asserts"
| "async"
| "await"
| "bigint"
| "boolean"
| "break"
| "case"
| "catch"
| "class"
| "const"
| "constructor"
| "continue"
| "debugger"
| "declare"
| "default"
| "delete"
| "do"
| "else"
| "enum"
| "eval"
| "export"
| "extends"
| "false"
| "finally"
| "for"
| "from"
| "function"
| "get"
| "global"
| "if"
| "implements"
| "import"
| "in"
| "infer"
| "instanceof"
| "interface"
| "intrinsic"
| "is"
| "keyof"
| "let"
| "module"
| "namespace"
| "never"
| "new"
| "null"
| "number"
| "object"
| "of"
| "out"
| "override"
| "package"
| "private"
| "protected"
| "public"
| "readonly"
| "require"
| "return"
| "satisfies"
| "set"
| "static"
| "string"
| "super"
| "switch"
| "symbol"
| "this"
| "throw"
| "true"
| "try"
| "type"
| "typeof"
| "undefined"
| "unique"
| "unknown"
| "using"
| "var"
| "void"
| "while"
| "with"
| "yield"
)
}