Combine all backend methods into a single js/d.ts file, like in Python

Easier to import from, and allows us to declare the output of the build
action without having to iterate over all the proto filenames. Have
confirmed it doesn't break esbuild's tree shaking.
This commit is contained in:
Damien Elmes 2023-07-03 13:42:20 +10:00
parent ffadbf577b
commit f3b6deefe9
20 changed files with 74 additions and 116 deletions

View file

@ -26,6 +26,7 @@ pub fn build_pylib(build: &mut Build) -> Result<()> {
proto_files: inputs![glob!["proto/anki/*.proto"]],
},
)?;
build.add_dependency("pylib:anki:proto", ":rslib:proto:py".into());
build.add_action(
"pylib:anki:_fluent.py",

View file

@ -109,10 +109,6 @@ impl BuildAction for GenPythonProto {
build.add_inputs("protoc", inputs![":protoc_binary"]);
build.add_inputs("protoc-gen-mypy", inputs![":pyenv:protoc-gen-mypy"]);
build.add_outputs("", python_outputs);
// not a direct dependency, but we include the output interface in our declared
// outputs
build.add_inputs("", inputs![":rslib:proto"]);
build.add_outputs("", vec!["pylib/anki/_backend_generated.py"]);
}
}

View file

@ -21,7 +21,7 @@ use crate::platform::overriden_rust_target_triple;
pub fn build_rust(build: &mut Build) -> Result<()> {
prepare_translations(build)?;
prepare_proto_descriptors(build)?;
build_proto_descriptors_and_interfaces(build)?;
build_rsbridge(build)
}
@ -81,16 +81,18 @@ fn prepare_translations(build: &mut Build) -> Result<()> {
Ok(())
}
fn prepare_proto_descriptors(build: &mut Build) -> Result<()> {
// build anki_proto and spit out descriptors/Python interface
fn build_proto_descriptors_and_interfaces(build: &mut Build) -> Result<()> {
let outputs = vec![
RustOutput::Data("descriptors.bin", "rslib/proto/descriptors.bin"),
RustOutput::Data("py", "pylib/anki/_backend_generated.py"),
RustOutput::Data("ts", "ts/lib/backend.d.ts"),
RustOutput::Data("ts", "ts/lib/backend.js"),
];
build.add_action(
"rslib:proto",
CargoBuild {
inputs: inputs![glob!["{proto,rslib/proto}/**"], ":protoc_binary",],
outputs: &[RustOutput::Data(
"descriptors.bin",
"$builddir/rslib/proto/descriptors.bin",
)],
outputs: &outputs,
target: None,
extra_args: "-p anki_proto",
release_override: None,
@ -110,8 +112,8 @@ fn build_rsbridge(build: &mut Build) -> Result<()> {
CargoBuild {
inputs: inputs![
glob!["{pylib/rsbridge/**,rslib/**}"],
// declare a dependency on i18n/proto so it gets built first, allowing
// things depending on strings.json to build faster, and ensuring
// declare a dependency on i18n/proto so they get built first, allowing
// things depending on them to build faster, and ensuring
// changes to the ftl files trigger a rebuild
":rslib:i18n",
":rslib:proto",

View file

@ -149,7 +149,7 @@ fn build_and_check_tslib(build: &mut Build) -> Result<()> {
},
)?;
// ensure _service files are generated by rslib
build.add_dependency("ts:lib:proto", inputs![":rslib:proto"]);
build.add_dependency("ts:lib:proto", inputs![":rslib:proto:ts"]);
// the generated _service.js files import @tslib/post, and esbuild won't be able
// to import the .ts file, so we need to generate a .js file for it
build.add_action(

View file

@ -3,81 +3,72 @@
use std::collections::HashSet;
use std::fmt::Write as WriteFmt;
use std::io::BufWriter;
use std::io::Write;
use std::path::Path;
use anki_io::create_dir_all;
use anki_io::create_file;
use anki_io::write_file_if_changed;
use anki_proto_gen::BackendService;
use anki_proto_gen::Method;
use anyhow::Result;
use inflections::Inflect;
pub(crate) fn write_ts_interface(services: &[BackendService]) -> Result<()> {
let root = Path::new("../../out/ts/lib/anki");
let root = Path::new("../../out/ts/lib");
create_dir_all(root)?;
let mut dts_out = String::new();
let mut js_out = String::new();
let mut referenced_packages = HashSet::new();
for service in services {
if service.name == "BackendAnkidroidService" {
continue;
}
let service_name = service
.name
.replace("Service", "")
.replace("Backend", "")
.to_snake_case();
write_dts_file(root, &service_name, service)?;
write_js_file(root, &service_name, service)?;
}
Ok(())
}
fn write_dts_file(root: &Path, service_name: &str, service: &BackendService) -> Result<()> {
let output_path = root.join(format!("{service_name}_service.d.ts"));
let mut out = BufWriter::new(create_file(output_path)?);
write_dts_header(&mut out)?;
let mut referenced_packages = HashSet::new();
let mut method_text = String::new();
for method in service.all_methods() {
let method = MethodDetails::from_method(method);
record_referenced_type(&mut referenced_packages, &method.input_type)?;
record_referenced_type(&mut referenced_packages, &method.output_type)?;
write_dts_method(&method, &mut method_text)?;
record_referenced_type(&mut referenced_packages, &method.input_type);
record_referenced_type(&mut referenced_packages, &method.output_type);
write_dts_method(&method, &mut dts_out);
write_js_method(&method, &mut js_out);
}
}
write_imports(referenced_packages, &mut out)?;
write!(out, "{}", method_text)?;
let imports = imports(referenced_packages);
write_file_if_changed(
root.join("backend.d.ts"),
format!("{}{}{}", dts_header(), imports, dts_out),
)?;
write_file_if_changed(
root.join("backend.js"),
format!("{}{}{}", js_header(), imports, js_out),
)?;
Ok(())
}
fn write_dts_header(out: &mut impl std::io::Write) -> Result<()> {
out.write_all(
br#"// Copyright: Ankitects Pty Ltd and contributors
fn dts_header() -> String {
r#"// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; https://www.gnu.org/licenses/agpl.html
import type { PlainMessage } from "@bufbuild/protobuf";
import type { PostProtoOptions } from "../post";
"#,
)?;
Ok(())
import type { PostProtoOptions } from "./post";
"#
.into()
}
fn write_imports(referenced_packages: HashSet<String>, out: &mut impl Write) -> Result<()> {
fn imports(referenced_packages: HashSet<String>) -> String {
let mut out = String::new();
for package in referenced_packages {
writeln!(
out,
"import * as {} from \"./{}_pb\";",
&mut out,
"import * as {} from \"./anki/{}_pb\";",
package,
package.to_snake_case()
)?;
)
.unwrap();
}
Ok(())
out
}
fn write_dts_method(
@ -88,43 +79,21 @@ fn write_dts_method(
comments,
}: &MethodDetails,
out: &mut String,
) -> Result<()> {
) {
let comments = format_comments(comments);
writeln!(
out,
r#"{comments}export declare function {method_name}(input: PlainMessage<{input_type}>, options?: PostProtoOptions): Promise<{output_type}>;"#
)?;
Ok(())
).unwrap()
}
fn write_js_file(root: &Path, service_name: &str, service: &BackendService) -> Result<()> {
let output_path = root.join(format!("{service_name}_service.js"));
let mut out = BufWriter::new(create_file(output_path)?);
write_js_header(&mut out)?;
let mut referenced_packages = HashSet::new();
let mut method_text = String::new();
for method in service.all_methods() {
let method = MethodDetails::from_method(method);
record_referenced_type(&mut referenced_packages, &method.input_type)?;
record_referenced_type(&mut referenced_packages, &method.output_type)?;
write_js_method(&method, &mut method_text)?;
}
write_imports(referenced_packages, &mut out)?;
write!(out, "{}", method_text)?;
Ok(())
}
fn write_js_header(out: &mut impl std::io::Write) -> Result<()> {
out.write_all(
br#"// Copyright: Ankitects Pty Ltd and contributors
fn js_header() -> String {
r#"// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; https://www.gnu.org/licenses/agpl.html
import { postProto } from "../post";
"#,
)?;
Ok(())
import { postProto } from "./post";
"#
.into()
}
fn write_js_method(
@ -135,15 +104,15 @@ fn write_js_method(
..
}: &MethodDetails,
out: &mut String,
) -> Result<()> {
) {
write!(
out,
r#"export async function {method_name}(input, options = {{}}) {{
return await postProto("{method_name}", new {input_type}(input), {output_type}, options);
}}
"#
)?;
Ok(())
)
.unwrap();
}
fn format_comments(comments: &Option<String>) -> String {
@ -175,12 +144,8 @@ impl MethodDetails {
}
}
fn record_referenced_type(
referenced_packages: &mut HashSet<String>,
type_name: &str,
) -> Result<()> {
fn record_referenced_type(referenced_packages: &mut HashSet<String>, type_name: &str) {
referenced_packages.insert(type_name.split('.').next().unwrap().to_string());
Ok(())
}
// e.g. anki.import_export.ImportResponse ->

View file

@ -7,7 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
CardStatsResponse,
CardStatsResponse_StatsRevlogEntry,
} from "@tslib/anki/stats_pb";
import { cardStats } from "@tslib/anki/stats_service";
import { cardStats } from "@tslib/backend";
import Container from "../components/Container.svelte";
import Row from "../components/Row.svelte";

View file

@ -3,7 +3,7 @@
import "./change-notetype-base.scss";
import { getChangeNotetypeInfo, getNotetypeNames } from "@tslib/anki/notetypes_service";
import { getChangeNotetypeInfo, getNotetypeNames } from "@tslib/backend";
import { ModuleName, setupI18n } from "@tslib/i18n";
import { checkNightMode } from "@tslib/nightmode";

View file

@ -2,7 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { ChangeNotetypeInfo, ChangeNotetypeRequest, NotetypeNames } from "@tslib/anki/notetypes_pb";
import { changeNotetype, getChangeNotetypeInfo } from "@tslib/anki/notetypes_service";
import { changeNotetype, getChangeNotetypeInfo } from "@tslib/backend";
import * as tr from "@tslib/ftl";
import { isEqual } from "lodash-es";
import type { Readable } from "svelte/store";

View file

@ -3,7 +3,7 @@
import "./congrats-base.scss";
import { congratsInfo } from "@tslib/anki/scheduler_service";
import { congratsInfo } from "@tslib/backend";
import { ModuleName, setupI18n } from "@tslib/i18n";
import { checkNightMode } from "@tslib/nightmode";

View file

@ -46,7 +46,7 @@ export async function setupDeckOptions(did_: number): Promise<DeckOptionsPage> {
});
}
import { getDeckConfigsForUpdate } from "@tslib/anki/deck_config_service";
import { getDeckConfigsForUpdate } from "@tslib/backend";
import TitledContainer from "../components/TitledContainer.svelte";
import EnumSelectorRow from "./EnumSelectorRow.svelte";

View file

@ -8,7 +8,7 @@ import type {
UpdateDeckConfigsRequest,
} from "@tslib/anki/deck_config_pb";
import { DeckConfig, DeckConfig_Config, DeckConfigsForUpdate_CurrentDeck_Limits } from "@tslib/anki/deck_config_pb";
import { updateDeckConfigs } from "@tslib/anki/deck_config_service";
import { updateDeckConfigs } from "@tslib/backend";
import { localeCompare } from "@tslib/i18n";
import { cloneDeep, isEqual } from "lodash-es";
import type { Readable, Writable } from "svelte/store";

View file

@ -4,11 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import type { GraphsResponse } from "@tslib/anki/stats_pb";
import {
getGraphPreferences,
graphs,
setGraphPreferences,
} from "@tslib/anki/stats_service";
import { getGraphPreferences, graphs, setGraphPreferences } from "@tslib/backend";
import type { Writable } from "svelte/store";
import { autoSavingPrefs } from "../sveltelib/preferences";

View file

@ -2,7 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { OpChanges } from "@tslib/anki/collection_pb";
import { addImageOcclusionNote, updateImageOcclusionNote } from "@tslib/anki/image_occlusion_service";
import { addImageOcclusionNote, updateImageOcclusionNote } from "@tslib/backend";
import * as tr from "@tslib/ftl";
import { get } from "svelte/store";

View file

@ -2,7 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { protoBase64 } from "@bufbuild/protobuf";
import { getImageForOcclusion, getImageOcclusionNote } from "@tslib/anki/image_occlusion_service";
import { getImageForOcclusion, getImageOcclusionNote } from "@tslib/backend";
import * as tr from "@tslib/ftl";
import { fabric } from "fabric";
import type { PanZoom } from "panzoom";

View file

@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import type { CsvMetadata_MappedNotetype } from "@tslib/anki/import_export_pb";
import { getFieldNames } from "@tslib/anki/notetypes_service";
import { getFieldNames } from "@tslib/backend";
import * as tr from "@tslib/ftl";
import Spacer from "../components/Spacer.svelte";

View file

@ -11,8 +11,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
CsvMetadata_MappedNotetype,
CsvMetadata_MatchScope,
} from "@tslib/anki/import_export_pb";
import { getCsvMetadata, importCsv } from "@tslib/anki/import_export_service";
import type { NotetypeNameId } from "@tslib/anki/notetypes_pb";
import { getCsvMetadata, importCsv } from "@tslib/backend";
import * as tr from "@tslib/ftl";
import Col from "../components/Col.svelte";

View file

@ -3,9 +3,7 @@
import "./import-csv-base.scss";
import { getDeckNames } from "@tslib/anki/decks_service";
import { getCsvMetadata } from "@tslib/anki/import_export_service";
import { getNotetypeNames } from "@tslib/anki/notetypes_service";
import { getCsvMetadata, getDeckNames, getNotetypeNames } from "@tslib/backend";
import { ModuleName, setupI18n } from "@tslib/i18n";
import { checkNightMode } from "@tslib/nightmode";

View file

@ -4,7 +4,7 @@
import "intl-pluralrules";
import { FluentBundle, FluentResource } from "@fluent/bundle";
import { i18nResources } from "@tslib/anki/i18n_service";
import { i18nResources } from "@tslib/backend";
import { firstLanguage, setBundles } from "./bundles";
import type { ModuleName } from "./modules";

View file

@ -4,7 +4,7 @@
import type { JsonValue } from "@bufbuild/protobuf";
import type { SchedulingContext, SchedulingStatesWithContext } from "@tslib/anki/scheduler_pb";
import { SchedulingStates } from "@tslib/anki/scheduler_pb";
import { getSchedulingStatesWithContext, setSchedulingStates } from "@tslib/anki/scheduler_service";
import { getSchedulingStatesWithContext, setSchedulingStates } from "@tslib/backend";
interface CustomDataStates {
again: Record<string, unknown>;

View file

@ -6,7 +6,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { createEventDispatcher, setContext, tick } from "svelte";
import type { Writable } from "svelte/store";
import { writable } from "svelte/store";
import { completeTag } from "@tslib/anki/tags_service";
import { completeTag } from "@tslib/backend";
import Shortcut from "../components/Shortcut.svelte";
import { execCommand } from "../domlib";
import { tagActionsShortcutsKey } from "@tslib/context-keys";