Anki/build/configure/src/web.rs
2023-07-03 09:44:38 +10:00

617 lines
19 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use anyhow::Result;
use ninja_gen::action::BuildAction;
use ninja_gen::command::RunCommand;
use ninja_gen::glob;
use ninja_gen::hashmap;
use ninja_gen::input::BuildInput;
use ninja_gen::inputs;
use ninja_gen::node::node_archive;
use ninja_gen::node::CompileSass;
use ninja_gen::node::CompileTypescript;
use ninja_gen::node::DPrint;
use ninja_gen::node::EsbuildScript;
use ninja_gen::node::Eslint;
use ninja_gen::node::GenTypescriptProto;
use ninja_gen::node::JestTest;
use ninja_gen::node::SqlFormat;
use ninja_gen::node::SvelteCheck;
use ninja_gen::node::TypescriptCheck;
use ninja_gen::rsync::RsyncFiles;
use ninja_gen::Build;
pub fn build_and_check_web(build: &mut Build) -> Result<()> {
setup_node(build)?;
build_sass(build)?;
build_and_check_tslib(build)?;
declare_and_check_other_libraries(build)?;
build_and_check_pages(build)?;
build_and_check_editor(build)?;
build_and_check_reviewer(build)?;
build_and_check_mathjax(build)?;
check_web(build)?;
Ok(())
}
fn setup_node(build: &mut Build) -> Result<()> {
ninja_gen::node::setup_node(
build,
node_archive(build.host_platform),
&[
"dprint",
"svelte-check",
"eslint",
"sass",
"tsc",
"tsx",
"jest",
"protoc-gen-es",
],
hashmap! {
"jquery" => vec![
"jquery/dist/jquery.min.js".into()
],
"jquery-ui" => vec![
"jquery-ui-dist/jquery-ui.min.js".into()
],
"css-browser-selector" => vec![
"css-browser-selector/css_browser_selector.min.js".into(),
],
"bootstrap-dist" => vec![
"bootstrap/dist/js/bootstrap.bundle.min.js".into(),
],
"mathjax" => MATHJAX_FILES.iter().map(|&v| v.into()).collect(),
"mdi_unthemed" => [
// saved searches
"heart-outline.svg",
// today
"clock-outline.svg",
// state
"circle.svg",
"circle-outline.svg",
// flags
"flag-variant.svg",
"flag-variant-outline.svg",
"flag-variant-off-outline.svg",
// decks
"book-outline.svg",
"book-clock-outline.svg",
"book-cog-outline.svg",
// notetypes
"newspaper.svg",
// cardtype
"application-braces-outline.svg",
// fields
"form-textbox.svg",
// tags
"tag-outline.svg",
"tag-off-outline.svg",
].iter().map(|file| format!("@mdi/svg/svg/{file}").into()).collect(),
"mdi_themed" => [
// sidebar tools
"magnify.svg",
"selection-drag.svg",
// QComboBox arrows
"chevron-up.svg",
"chevron-down.svg",
// QHeaderView arrows
"menu-up.svg",
"menu-down.svg",
// drag handle
"drag-vertical.svg",
"drag-horizontal.svg",
// checkbox
"check.svg",
"minus-thick.svg",
// QRadioButton
"circle-medium.svg",
].iter().map(|file| format!("@mdi/svg/svg/{file}").into()).collect(),
},
)?;
Ok(())
}
fn build_and_check_tslib(build: &mut Build) -> Result<()> {
build.add_action(
"ts:lib:i18n",
RunCommand {
command: ":pyenv:bin",
args: "$script $strings $out",
inputs: hashmap! {
"script" => inputs!["ts/lib/genfluent.py"],
"strings" => inputs![":rslib:i18n:strings.json"],
"" => inputs!["pylib/anki/_vendor/stringcase.py"]
},
outputs: hashmap! {
"out" => vec![
"ts/lib/ftl.js",
"ts/lib/ftl.d.ts",
"ts/lib/i18n/modules.js",
"ts/lib/i18n/modules.d.ts"
]
},
},
)?;
build.add_action(
"ts:lib:proto",
GenTypescriptProto {
protos: inputs![glob!["proto/**/*.proto"]],
include_dirs: &["proto"],
out_dir: "out/ts/lib",
out_path_transform: |path| {
path.replace("proto/", "ts/lib/")
.replace("proto\\", "ts/lib\\")
},
py_transform_script: "pylib/tools/markpure.py",
},
)?;
// ensure _service files are generated by rslib
build.add_dependency("ts:lib:proto", inputs![":rslib:proto"]);
// 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(
"ts:lib:proto",
CompileTypescript {
ts_files: "ts/lib/post.ts".into(),
out_dir: "out/ts/lib",
out_path_transform: |path| path.into(),
},
)?;
let src_files = inputs![glob!["ts/lib/**"]];
eslint(build, "lib", "ts/lib", inputs![":ts:lib", &src_files])?;
build.add_action(
"check:jest:lib",
jest_test("ts/lib", inputs![":ts:lib", &src_files], true),
)?;
build.add_dependency("ts:lib", src_files);
Ok(())
}
fn jest_test(folder: &str, deps: BuildInput, jsdom: bool) -> impl BuildAction + '_ {
JestTest {
folder,
deps,
jest_rc: "ts/jest.config.js".into(),
jsdom,
}
}
fn declare_and_check_other_libraries(build: &mut Build) -> Result<()> {
for (library, inputs) in [
("sveltelib", inputs![":ts:lib", glob!("ts/sveltelib/**")]),
("domlib", inputs![":ts:lib", glob!("ts/domlib/**")]),
(
"components",
inputs![":ts:lib", ":ts:sveltelib", glob!("ts/components/**")],
),
("html-filter", inputs![glob!("ts/html-filter/**")]),
] {
let library_with_ts = format!("ts:{library}");
let folder = library_with_ts.replace(':', "/");
build.add_dependency(&library_with_ts, inputs.clone());
eslint(build, library, &folder, inputs.clone())?;
if matches!(library, "domlib" | "html-filter") {
build.add_action(
&format!("check:jest:{library}"),
jest_test(&folder, inputs, true),
)?;
}
}
eslint(
build,
"sql_format",
"ts/sql_format",
inputs![glob!("ts/sql_format/**")],
)?;
Ok(())
}
pub fn eslint(build: &mut Build, name: &str, folder: &str, deps: BuildInput) -> Result<()> {
let eslint_rc = inputs![".eslintrc.js"];
build.add_action(
format!("check:eslint:{name}"),
Eslint {
folder,
inputs: deps.clone(),
eslint_rc: eslint_rc.clone(),
fix: false,
},
)?;
build.add_action(
format!("fix:eslint:{name}"),
Eslint {
folder,
inputs: deps,
eslint_rc,
fix: true,
},
)?;
Ok(())
}
fn build_and_check_pages(build: &mut Build) -> Result<()> {
build.add_dependency("ts:tag-editor", inputs![glob!["ts/tag-editor/**"]]);
let mut build_page = |name: &str, html: bool, deps: BuildInput| -> Result<()> {
let group = format!("ts:pages:{name}");
let deps = inputs![deps, glob!(format!("ts/{name}/**"))];
let extra_exts = if html { &["css", "html"][..] } else { &["css"] };
build.add_action(
&group,
EsbuildScript {
script: inputs!["ts/bundle_svelte.mjs"],
entrypoint: inputs![format!("ts/{name}/index.ts")],
output_stem: &format!("ts/{name}/{name}"),
deps: deps.clone(),
extra_exts,
},
)?;
build.add_action(
format!("check:svelte:{name}"),
SvelteCheck {
tsconfig: inputs![format!("ts/{name}/tsconfig.json")],
inputs: deps.clone(),
},
)?;
let folder = format!("ts/{name}");
eslint(build, name, &folder, deps.clone())?;
if matches!(name, "deck-options" | "change-notetype") {
build.add_action(
&format!("check:jest:{name}"),
jest_test(&folder, deps, false),
)?;
}
Ok(())
};
build_page(
"congrats",
true,
inputs![
//
":ts:lib",
":ts:components",
":sass",
],
)?;
build_page(
"deck-options",
true,
inputs![
//
":ts:lib",
":ts:components",
":ts:sveltelib",
":sass",
],
)?;
build_page(
"graphs",
true,
inputs![
//
":ts:lib",
":ts:components",
":sass",
],
)?;
build_page(
"card-info",
true,
inputs![
//
":ts:lib",
":ts:components",
":sass",
],
)?;
build_page(
"change-notetype",
true,
inputs![
//
":ts:lib",
":ts:components",
":ts:sveltelib",
":sass",
],
)?;
build_page(
"import-csv",
true,
inputs![
//
":ts:lib",
":ts:components",
":ts:sveltelib",
":ts:tag-editor",
":sass"
],
)?;
// we use the generated .css file separately
build_page(
"editable",
false,
inputs![
//
":ts:lib",
":ts:components",
":ts:domlib",
":ts:sveltelib",
":sass"
],
)?;
build_page(
"image-occlusion",
true,
inputs![
//
":ts:lib",
":ts:components",
":ts:sveltelib",
":ts:tag-editor",
":sass"
],
)?;
Ok(())
}
fn build_and_check_editor(build: &mut Build) -> Result<()> {
let editor_deps = inputs![
//
":ts:lib",
":ts:components",
":ts:domlib",
":ts:sveltelib",
":ts:tag-editor",
":ts:html-filter",
":sass",
glob!("ts/{editable,editor}/**")
];
let mut build_editor_page = |name: &str, entrypoint: &str| -> Result<()> {
let stem = format!("ts/editor/{name}");
build.add_action(
"ts:editor",
EsbuildScript {
script: inputs!["ts/bundle_svelte.mjs"],
entrypoint: inputs![format!("ts/editor/{entrypoint}.ts")],
output_stem: &stem,
deps: editor_deps.clone(),
extra_exts: &["css"],
},
)
};
build_editor_page("browser_editor", "index_browser")?;
build_editor_page("reviewer_editor", "index_reviewer")?;
build_editor_page("note_creator", "index_creator")?;
let group = "ts/editor";
build.add_action(
"check:svelte:editor",
SvelteCheck {
tsconfig: inputs![format!("{group}/tsconfig.json")],
inputs: editor_deps.clone(),
},
)?;
eslint(build, "editor", group, editor_deps)?;
Ok(())
}
fn build_and_check_reviewer(build: &mut Build) -> Result<()> {
let reviewer_deps = inputs![":ts:lib", glob!("ts/{reviewer,image-occlusion}/**"),];
build.add_action(
"ts:reviewer:reviewer.js",
EsbuildScript {
script: inputs!["ts/bundle_ts.mjs"],
entrypoint: "ts/reviewer/index_wrapper.ts".into(),
output_stem: "ts/reviewer/reviewer",
deps: reviewer_deps.clone(),
extra_exts: &[],
},
)?;
build.add_action(
"ts:reviewer:reviewer.css",
CompileSass {
input: inputs!["ts/reviewer/reviewer.scss"],
output: "ts/reviewer/reviewer.css",
deps: inputs![":sass", "ts/image-occlusion/review.scss"],
load_paths: vec!["."],
},
)?;
build.add_action(
"ts:reviewer:reviewer_extras_bundle.js",
EsbuildScript {
script: inputs!["ts/bundle_ts.mjs"],
entrypoint: "ts/reviewer/reviewer_extras.ts".into(),
output_stem: "ts/reviewer/reviewer_extras_bundle",
deps: reviewer_deps.clone(),
extra_exts: &[],
},
)?;
build.add_action(
"check:typescript:reviewer",
TypescriptCheck {
tsconfig: inputs!["ts/reviewer/tsconfig.json"],
inputs: reviewer_deps.clone(),
},
)?;
eslint(build, "reviewer", "ts/reviewer", reviewer_deps)?;
build.add_action(
"check:jest:reviewer",
jest_test("ts/reviewer", inputs![":ts:reviewer"], false),
)?;
Ok(())
}
fn check_web(build: &mut Build) -> Result<()> {
let dprint_files = inputs![glob![
"**/*.{ts,mjs,js,md,json,toml,svelte,scss}",
"target/**"
]];
build.add_action(
"check:format:dprint",
DPrint {
inputs: dprint_files.clone(),
check_only: true,
},
)?;
build.add_action(
"format:dprint",
DPrint {
inputs: dprint_files,
check_only: false,
},
)?;
Ok(())
}
pub fn check_sql(build: &mut Build) -> Result<()> {
build.add_action(
"check:format:sql",
SqlFormat {
inputs: inputs![glob!["**/*.sql"]],
check_only: true,
},
)?;
build.add_action(
"format:sql",
SqlFormat {
inputs: inputs![glob!["**/*.sql"]],
check_only: false,
},
)?;
Ok(())
}
fn build_and_check_mathjax(build: &mut Build) -> Result<()> {
let files = inputs![glob!["ts/mathjax/*"]];
build.add_action(
"ts:mathjax",
EsbuildScript {
script: "ts/transform_ts.mjs".into(),
entrypoint: "ts/mathjax/index.ts".into(),
deps: files.clone(),
output_stem: "ts/mathjax/mathjax",
extra_exts: &[],
},
)?;
eslint(build, "mathjax", "ts/mathjax", files.clone())?;
build.add_action(
"check:typescript:mathjax",
TypescriptCheck {
tsconfig: "ts/mathjax/tsconfig.json".into(),
inputs: files,
},
)
}
pub const MATHJAX_FILES: &[&str] = &[
"mathjax/es5/a11y/assistive-mml.js",
"mathjax/es5/a11y/complexity.js",
"mathjax/es5/a11y/explorer.js",
"mathjax/es5/a11y/semantic-enrich.js",
"mathjax/es5/input/tex/extensions/action.js",
"mathjax/es5/input/tex/extensions/all-packages.js",
"mathjax/es5/input/tex/extensions/ams.js",
"mathjax/es5/input/tex/extensions/amscd.js",
"mathjax/es5/input/tex/extensions/autoload.js",
"mathjax/es5/input/tex/extensions/bbox.js",
"mathjax/es5/input/tex/extensions/boldsymbol.js",
"mathjax/es5/input/tex/extensions/braket.js",
"mathjax/es5/input/tex/extensions/bussproofs.js",
"mathjax/es5/input/tex/extensions/cancel.js",
"mathjax/es5/input/tex/extensions/centernot.js",
"mathjax/es5/input/tex/extensions/color.js",
"mathjax/es5/input/tex/extensions/colortbl.js",
"mathjax/es5/input/tex/extensions/colorv2.js",
"mathjax/es5/input/tex/extensions/configmacros.js",
"mathjax/es5/input/tex/extensions/enclose.js",
"mathjax/es5/input/tex/extensions/extpfeil.js",
"mathjax/es5/input/tex/extensions/gensymb.js",
"mathjax/es5/input/tex/extensions/html.js",
"mathjax/es5/input/tex/extensions/mathtools.js",
"mathjax/es5/input/tex/extensions/mhchem.js",
"mathjax/es5/input/tex/extensions/newcommand.js",
"mathjax/es5/input/tex/extensions/noerrors.js",
"mathjax/es5/input/tex/extensions/noundefined.js",
"mathjax/es5/input/tex/extensions/physics.js",
"mathjax/es5/input/tex/extensions/require.js",
"mathjax/es5/input/tex/extensions/setoptions.js",
"mathjax/es5/input/tex/extensions/tagformat.js",
"mathjax/es5/input/tex/extensions/textcomp.js",
"mathjax/es5/input/tex/extensions/textmacros.js",
"mathjax/es5/input/tex/extensions/unicode.js",
"mathjax/es5/input/tex/extensions/upgreek.js",
"mathjax/es5/input/tex/extensions/verb.js",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Zero.woff",
"mathjax/es5/tex-chtml.js",
"mathjax/es5/sre/mathmaps/de.json",
"mathjax/es5/sre/mathmaps/en.json",
"mathjax/es5/sre/mathmaps/es.json",
"mathjax/es5/sre/mathmaps/fr.json",
"mathjax/es5/sre/mathmaps/hi.json",
"mathjax/es5/sre/mathmaps/it.json",
"mathjax/es5/sre/mathmaps/nemeth.json",
];
pub fn copy_mathjax() -> impl BuildAction {
RsyncFiles {
inputs: inputs![":node_modules:mathjax"],
target_folder: "qt/_aqt/data/web/js/vendor/mathjax",
strip_prefix: "$builddir/node_modules/mathjax/es5",
extra_args: "",
}
}
fn build_sass(build: &mut Build) -> Result<()> {
build.add_dependency("sass", inputs![glob!("sass/**")]);
build.add_action(
"css:_root-vars",
CompileSass {
input: inputs!["sass/_root-vars.scss"],
output: "sass/_root-vars.css",
deps: inputs![glob!["sass/*"]],
load_paths: vec![],
},
)?;
Ok(())
}