1use anyhow::{Context, Result};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone)]
9pub struct SdkOptions {
10 pub wit_dir: PathBuf,
11 pub world: Option<String>,
12 pub out_dir: PathBuf,
13 pub project_root: Option<PathBuf>,
14 pub entry: PathBuf,
15 pub initialize_tsconfig: bool,
16}
17
18impl Default for SdkOptions {
19 fn default() -> Self {
20 Self {
21 wit_dir: PathBuf::from("wit"),
22 world: None,
23 out_dir: PathBuf::from(".perry/types"),
24 project_root: None,
25 entry: PathBuf::from("src/index.ts"),
26 initialize_tsconfig: true,
27 }
28 }
29}
30
31#[derive(Debug, Clone)]
33pub struct SdkResult {
34 pub types_path: PathBuf,
35 pub check_path: PathBuf,
36 pub tsconfig_path: Option<PathBuf>,
37}
38
39pub fn generate_sdk_files(
41 options: &SdkOptions,
42) -> std::result::Result<SdkResult, crate::CompileError> {
43 generate_sdk_files_impl(options)
44 .map_err(|error| crate::CompileError::from_error(error, crate::DiagnosticCode::Io))
45}
46
47fn generate_sdk_files_impl(options: &SdkOptions) -> Result<SdkResult> {
48 let (resolve, package) = crate::component::wit::resolve_wit(&options.wit_dir)?;
49 let world = crate::component::wit::select_world(&resolve, package, options.world.as_deref())?;
50 let artifacts = declarations(&resolve, world).map_err(|error| {
51 crate::PerryDiagnostic::from_error(error, crate::DiagnosticCode::WitResolution)
52 })?;
53 fs::create_dir_all(&options.out_dir).with_context(|| {
54 format!(
55 "Failed to create SDK output directory at {}",
56 options.out_dir.display()
57 )
58 })?;
59
60 for artifact in artifacts {
61 let path = options.out_dir.join(artifact.name);
62 fs::write(&path, artifact.contents)
63 .with_context(|| format!("Failed to write declaration file at {}", path.display()))?;
64 }
65 let dts_path = options.out_dir.join("world.d.ts");
66
67 let project_root = options.project_root.clone().unwrap_or_else(|| {
69 if options.out_dir.ends_with(".perry/types") {
70 options
71 .out_dir
72 .parent()
73 .and_then(|p| p.parent())
74 .filter(|p| !p.as_os_str().is_empty())
75 .unwrap_or(Path::new("."))
76 .to_path_buf()
77 } else {
78 PathBuf::from(".")
79 }
80 });
81
82 let tsconfig_path = project_root.join("tsconfig.json");
83 let entry = project_root.join(&options.entry);
84 let entry_import = relative_path(&options.out_dir, &entry.with_extension(""))?;
85 let check_path = options.out_dir.join("implementation-check.ts");
86 let check = format!(
87 "import * as implementation from {};\nimport type {{ ComponentImplementation }} from './world';\nconst checked: ComponentImplementation = implementation;\nexport {{ checked }};\n",
88 serde_json::to_string(&entry_import)?
89 );
90 fs::write(&check_path, check)?;
91 let compiler_options = || {
92 serde_json::json!({
93 "target": "ES2022",
94 "lib": ["ES2022", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
95 "module": "ESNext",
96 "moduleDetection": "force",
97 "moduleResolution": "bundler",
98 "allowImportingTsExtensions": true,
99 "strict": true,
100 "noEmit": true,
101 "skipLibCheck": false,
102 "types": []
103 })
104 };
105 let generated_tsconfig = if !tsconfig_path.exists() && options.initialize_tsconfig {
106 let config = serde_json::json!({
107 "compilerOptions": compiler_options(),
108 "include": ["src/**/*", "examples/**/*", "*.ts", ".perry/types/**/*"],
109 "files": [relative_path(&project_root, &check_path)?]
110 });
111 fs::write(&tsconfig_path, serde_json::to_string_pretty(&config)?).with_context(|| {
112 format!(
113 "Failed to write tsconfig.json at {}",
114 tsconfig_path.display()
115 )
116 })?;
117 Some(tsconfig_path)
118 } else if tsconfig_path.exists() {
119 Some(tsconfig_path)
120 } else {
121 None
122 };
123
124 let mut check_config = serde_json::json!({
125 "compilerOptions": compiler_options(),
126 "files": ["implementation-check.ts"],
127 "include": [],
128 "exclude": []
129 });
130 if let Some(path) = &generated_tsconfig {
131 check_config["extends"] = serde_json::json!(relative_path(&options.out_dir, path)?);
132 }
133 fs::write(
134 options.out_dir.join("tsconfig.json"),
135 serde_json::to_string_pretty(&check_config)?,
136 )?;
137
138 Ok(SdkResult {
139 types_path: dts_path,
140 check_path,
141 tsconfig_path: generated_tsconfig,
142 })
143}
144
145pub(crate) fn declarations(
147 resolve: &wit_parser::Resolve,
148 world: wit_parser::WorldId,
149) -> Result<Vec<crate::api::Artifact>> {
150 Ok(perry_frontend::sdk::declarations(resolve, world)?
151 .into_iter()
152 .map(|declaration| crate::api::Artifact {
153 name: declaration.name,
154 contents: declaration.contents,
155 })
156 .collect())
157}
158
159pub fn write_runtime_declarations(out_dir: &Path) -> Result<()> {
161 fs::create_dir_all(out_dir)?;
162 fs::write(
163 out_dir.join("runtime.d.ts"),
164 perry_frontend::sdk::RUNTIME_DECLARATIONS,
165 )?;
166 Ok(())
167}
168
169fn relative_path(from: &Path, to: &Path) -> Result<String> {
170 fn normalized(path: &Path) -> Result<PathBuf> {
171 let mut result = PathBuf::new();
172 for component in std::path::absolute(path)?.components() {
173 if component == std::path::Component::ParentDir {
174 result.pop();
175 } else {
176 result.push(component);
177 }
178 }
179 Ok(result)
180 }
181 let from = normalized(from)?;
182 let to = normalized(to)?;
183 let common = from
184 .components()
185 .zip(to.components())
186 .take_while(|(a, b)| a == b)
187 .count();
188 let mut relative = PathBuf::from(".");
189 for _ in from.components().skip(common) {
190 relative.push("..");
191 }
192 relative.extend(to.components().skip(common));
193 Ok(relative.to_string_lossy().replace('\\', "/"))
194}