Skip to content

Commit 1eacc6b

Browse files
committed
Export every type the generated bindings.d.ts references
1 parent 916b708 commit 1eacc6b

3 files changed

Lines changed: 99 additions & 2 deletions

File tree

crates/tmc-langs-cli/bindings.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,12 @@ export type PublicSpec = { type: ExerciseType, archive_name: string, stub_downlo
358358
*/
359359
browser_test: BrowserTestSpec | null, }
360360

361+
export type ExerciseType = "browser" | "editor";
362+
363+
export type BrowserTestSpec = { runtime: BrowserTestRuntime, script: string, error: string | null, }
364+
365+
export type BrowserTestRuntime = "python";
366+
361367
export type ModelSolutionSpec = { type: ExerciseType, solution_download_url: string, }
362368

363369
export type ExerciseTaskSubmissionResult = {
@@ -376,6 +382,8 @@ export type GradingProgress = "Failed" | "NotReady" | "PendingManual" | "Pending
376382

377383
export type ExerciseSlideSubmissionListItem = { id: string, exercise_id: string, created_at: string, score_given: number | null, grading_progress: GradingProgress | null, }
378384

385+
export type PasteResult = { paste_url: string, }
386+
379387
export type CourseProgress = { course_id: string, exercises: Array<ExerciseProgress>, }
380388

381389
export type ExerciseProgress = { exercise_id: string,

crates/tmc-langs-cli/src/app.rs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1341,7 +1341,6 @@ mod test {
13411341
}
13421342

13431343
/// Path to the committed TypeScript bindings artifact.
1344-
#[cfg(feature = "ts-rs")]
13451344
fn dts_path() -> PathBuf {
13461345
Path::new(env!("CARGO_MANIFEST_DIR")).join("bindings.d.ts")
13471346
}
@@ -1392,6 +1391,89 @@ mod test {
13921391
std::fs::write(dts_path(), generate_cli_bindings_dts()).unwrap();
13931392
}
13941393

1394+
/// `bindings.d.ts` is vendored into other repos as a standalone file
1395+
/// (sp331's `services/tmc/src/tmc/cli.d.ts`), so a type it references but
1396+
/// never declares makes it invalid TypeScript there. The byte-equality
1397+
/// gates compare bytes only and cannot see that; a type reachable from an
1398+
/// exported type but missing from `generate_cli_bindings_dts`'s
1399+
/// `export_to!` list is the way it happens.
1400+
#[test]
1401+
fn bindings_dts_declares_every_type_it_references() {
1402+
/// Types TypeScript provides; everything else must be declared in-file.
1403+
const TS_BUILTINS: [&str; 2] = ["Array", "Record"];
1404+
1405+
let src = strip_comments_and_strings(
1406+
&std::fs::read_to_string(dts_path()).expect("bindings.d.ts should exist"),
1407+
);
1408+
1409+
let mut declared = std::collections::HashSet::new();
1410+
for line in src.lines() {
1411+
let Some(rest) = line.strip_prefix("export type ") else {
1412+
continue;
1413+
};
1414+
let name_end = rest
1415+
.find(|c: char| !c.is_alphanumeric() && c != '_')
1416+
.unwrap_or(rest.len());
1417+
declared.insert(&rest[..name_end]);
1418+
// A generic alias declares its own parameters.
1419+
if let Some(params) = rest[name_end..]
1420+
.strip_prefix('<')
1421+
.and_then(|r| r.split_once('>'))
1422+
.map(|(params, _)| params)
1423+
{
1424+
declared.extend(params.split(',').map(str::trim));
1425+
}
1426+
}
1427+
1428+
let mut missing = src
1429+
.split(|c: char| !c.is_alphanumeric() && c != '_')
1430+
.filter(|word| word.starts_with(|c: char| c.is_ascii_uppercase()))
1431+
.filter(|word| !TS_BUILTINS.contains(word) && !declared.contains(word))
1432+
.collect::<Vec<_>>();
1433+
missing.sort_unstable();
1434+
missing.dedup();
1435+
1436+
assert!(
1437+
missing.is_empty(),
1438+
"bindings.d.ts references types it does not declare: {missing:?}; \
1439+
add them to generate_cli_bindings_dts's export_to! list and regenerate"
1440+
);
1441+
}
1442+
1443+
/// Blanks out doc comments and string literals so type references can be
1444+
/// picked out by identifier casing without prose or literal unions
1445+
/// (`"tar" | "zip"`) being mistaken for them.
1446+
fn strip_comments_and_strings(src: &str) -> String {
1447+
let mut out = String::with_capacity(src.len());
1448+
let mut chars = src.chars().peekable();
1449+
while let Some(c) = chars.next() {
1450+
match c {
1451+
'/' if chars.peek() == Some(&'*') => {
1452+
let mut prev = ' ';
1453+
for c in chars.by_ref() {
1454+
if prev == '*' && c == '/' {
1455+
break;
1456+
}
1457+
prev = c;
1458+
}
1459+
}
1460+
'"' => {
1461+
while let Some(c) = chars.next() {
1462+
match c {
1463+
'\\' => {
1464+
chars.next();
1465+
}
1466+
'"' => break,
1467+
_ => {}
1468+
}
1469+
}
1470+
}
1471+
_ => out.push(c),
1472+
}
1473+
}
1474+
out
1475+
}
1476+
13951477
/// Produces the TypeScript bindings as a string from the current types.
13961478
#[cfg(feature = "ts-rs")]
13971479
fn generate_cli_bindings_dts() -> String {
@@ -1492,11 +1574,15 @@ mod test {
14921574
tmc_langs::mooc::TmcExerciseSlide,
14931575
tmc_langs::mooc::TmcExerciseTask,
14941576
tmc_langs::mooc::PublicSpec,
1577+
tmc_langs::mooc::ExerciseType,
1578+
tmc_langs::mooc::BrowserTestSpec,
1579+
tmc_langs::mooc::BrowserTestRuntime,
14951580
tmc_langs::mooc::ModelSolutionSpec,
14961581
tmc_langs::mooc::ExerciseTaskSubmissionResult,
14971582
tmc_langs::mooc::ExerciseTaskSubmissionStatus,
14981583
tmc_langs::mooc::GradingProgress,
14991584
tmc_langs::mooc::ExerciseSlideSubmissionListItem,
1585+
tmc_langs::mooc::PasteResult,
15001586
tmc_langs::mooc::CourseProgress,
15011587
tmc_langs::mooc::ExerciseProgress,
15021588
tmc_langs::mooc::MoocClientUpdateData,

crates/tmc-mooc-client/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ pub use self::{
1313
poll_device_token, refresh_token,
1414
},
1515
error::{MoocClientError, MoocClientResult},
16-
exercise::{ExerciseType, ModelSolutionSpec, PublicSpec, TmcExerciseSlide, TmcExerciseTask},
16+
exercise::{
17+
BrowserTestRuntime, BrowserTestSpec, ExerciseType, ModelSolutionSpec, PublicSpec,
18+
TmcExerciseSlide, TmcExerciseTask,
19+
},
1720
};
1821
use bytes::Bytes;
1922
use chrono::{DateTime, Utc};

0 commit comments

Comments
 (0)