diff --git a/.gitignore b/.gitignore index 279972d0..c0c542e6 100644 --- a/.gitignore +++ b/.gitignore @@ -142,6 +142,8 @@ src/*/_version.py .jekyll-cache/ .jekyll-metadata _site/ +docs/assets/js/repo-review-app.min.js +docs/assets/js/repo-review-app.min.js.map # NodeJS stuff, just in case (developer tooling) node_modules/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml index dfaba949..eb3d433f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -13,6 +13,7 @@ build: ruby: "3.4" commands: + - ./helpers/fetch_repo_review_app.sh - bundle install - > JEKYLL_ENV=production bundle exec jekyll build --destination diff --git a/docs/README.md b/docs/README.md index 12be755d..a191a7bd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,6 +14,7 @@ To build locally, install rbenv (remember to run `rbenv init` after installing, and `rbenv install 3.4.1`). Then: ```bash +./helpers/fetch_repo_review_app.sh bundle install bundle exec jekyll serve --livereload ``` diff --git a/docs/_includes/head_custom.html b/docs/_includes/head_custom.html index 4d7d2d88..96fd84d7 100644 --- a/docs/_includes/head_custom.html +++ b/docs/_includes/head_custom.html @@ -3,26 +3,7 @@ {%- if page.interactive_repo_review %} - - - - - - - @@ -35,10 +16,5 @@ rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" /> - - + {%- endif %} diff --git a/docs/_includes/interactive_repo_review.html b/docs/_includes/interactive_repo_review.html index 4b3fba79..b68a7414 100644 --- a/docs/_includes/interactive_repo_review.html +++ b/docs/_includes/interactive_repo_review.html @@ -1,6 +1,4 @@ -
- Loading (requires javascript and WebAssembly)... -
+
Loading (requires javascript and WebAssembly)...
- diff --git a/docs/assets/js/webapp.js b/docs/assets/js/webapp.js deleted file mode 100644 index 4a2d9060..00000000 --- a/docs/assets/js/webapp.js +++ /dev/null @@ -1,557 +0,0 @@ -const DEFAULT_MSG = - "Enter a GitHub repo and branch/tag to review. Runs Python entirely in your browser using WebAssembly. Built with React, MaterialUI, and Pyodide."; - -const urlParams = new URLSearchParams(window.location.search); -const baseurl = window.location.pathname; - -function Heading(props) { - return ( - - - - - Repo-Review - - - Source - - - - - ); -} - -function IfUrlLink({ name, url, color }) { - if (url) { - return ( - - {name} - - ); - } - return ( - - {name} - - ); -} - -function Results(props) { - const output = []; - for (const key in props.results) { - const inner_results = props.results[key]; - const results_components = inner_results.map((result) => { - const text_color = - result.state === false - ? "error.main" - : result.state === true - ? "text.primary" - : "info.main"; - const details = - result.state === false ? ( - - ) : null; - const color = - result.state === false - ? "error" - : result.state === true - ? "success" - : "info"; - const icon = ( - - {result.state === false - ? "report" - : result.state === true - ? "check_box" - : "info"} - - ); - - const skipped = ( - - {` [skipped] ${result.skip_reason}`} - - ); - const msg = ( - - - - - - {result.description} - - - {result.state === undefined && skipped} - - ); - return ( - - {icon} - - - ); - }); - - output.push( -
  • - -
  • , - ); - } - - return ( - - } overflow="auto"> - {output} - - - ); -} - -async function fetchRepoRefs(repo) { - if (!repo) return { branches: [], tags: [] }; - try { - // Fetch both branches and tags from GitHub API - const [branchesResponse, tagsResponse] = await Promise.all([ - fetch(`https://api.github.com/repos/${repo}/branches`), - fetch(`https://api.github.com/repos/${repo}/tags`), - ]); - - if (!branchesResponse.ok || !tagsResponse.ok) { - console.error("Error fetching repo data"); - return { branches: [], tags: [] }; - } - - const branches = await branchesResponse.json(); - const tags = await tagsResponse.json(); - - return { - branches: branches.map((branch) => ({ - name: branch.name, - type: "branch", - })), - tags: tags.map((tag) => ({ - name: tag.name, - type: "tag", - })), - }; - } catch (error) { - console.error("Error fetching repo references:", error); - return { branches: [], tags: [] }; - } -} - -async function prepare_pyodide(deps) { - const deps_str = deps.map((i) => `"${i}"`).join(", "); - const pyodide = await loadPyodide(); - - await pyodide.loadPackage("micropip"); - await pyodide.runPythonAsync(` - import micropip - await micropip.install([${deps_str}]) - `); - return pyodide; -} - -function MyThemeProvider(props) { - const prefersDarkMode = MaterialUI.useMediaQuery( - "(prefers-color-scheme: dark)", - ); - - const theme = React.useMemo( - () => - MaterialUI.createTheme({ - palette: { - mode: prefersDarkMode ? "dark" : "light", - }, - }), - [prefersDarkMode], - ); - - return ( - - {props.children} - - ); -} - -class App extends React.Component { - constructor(props) { - super(props); - const inner_deps_str = props.deps.join("\n"); - const deps_str = `
    ${inner_deps_str}
    `; - this.state = { - results: [], - repo: urlParams.get("repo") || "", - ref: urlParams.get("ref") || "", - refType: urlParams.get("refType") || "branch", - refs: { branches: [], tags: [] }, - msg: `

    ${DEFAULT_MSG}

    Packages:

    ${deps_str}`, - progress: false, - loadingRefs: false, - err_msg: "", - skip_reason: "", - url: "", - }; - this.pyodide_promise = prepare_pyodide(props.deps); - this.refInputDebounce = null; - } - - async fetchRepoReferences(repo) { - if (!repo) return; - - this.setState({ loadingRefs: true }); - const refs = await fetchRepoRefs(repo); - this.setState({ - refs: refs, - loadingRefs: false, - }); - } - - handleRepoChange(repo) { - this.setState({ repo }); - - // debounce the API call to avoid too many requests - clearTimeout(this.refInputDebounce); - this.refInputDebounce = setTimeout(() => { - this.fetchRepoReferences(repo); - }, 500); - } - - handleRefChange(ref, refType) { - this.setState({ ref, refType }); - } - - handleCompute() { - if (!this.state.repo || !this.state.ref) { - this.setState({ results: [], msg: DEFAULT_MSG }); - window.history.replaceState(null, "", baseurl); - alert( - `Please enter a repo (${this.state.repo}) and branch/tag (${this.state.ref})`, - ); - return; - } - const local_params = new URLSearchParams({ - repo: this.state.repo, - ref: this.state.ref, - refType: this.state.refType, - }); - window.history.replaceState(null, "", `${baseurl}?${local_params}`); - this.setState({ - results: [], - msg: "Running Python via Pyodide", - progress: true, - }); - const state = this.state; - this.pyodide_promise.then((pyodide) => { - var families_checks; - try { - families_checks = pyodide.runPython(` - from repo_review.processor import process, md_as_html - from repo_review.ghpath import GHPath - from dataclasses import replace - - package = GHPath(repo="${state.repo}", branch="${state.ref}") - families, checks = process(package) - - for v in families.values(): - if v.get("description"): - v["description"] = md_as_html(v["description"]) - checks = [res.md_as_html() for res in checks] - - (families, checks) - `); - } catch (e) { - if (e.message.includes("KeyError: 'tree'")) { - this.setState({ - msg: DEFAULT_MSG, - progress: false, - err_msg: "Invalid repository or branch/tag. Please try again.", - }); - return; - } - this.setState({ - progress: false, - err_msg: `
    ${e.message}
    `,
    -        });
    -        return;
    -      }
    -
    -      const families_dict = families_checks.get(0);
    -      const results_list = families_checks.get(1);
    -
    -      const results = {};
    -      const families = {};
    -      for (const val of families_dict) {
    -        const descr = families_dict.get(val).get("description");
    -        results[val] = [];
    -        families[val] = {
    -          name: families_dict.get(val).get("name").toString(),
    -          description: descr && descr.toString(),
    -        };
    -      }
    -      console.log(families);
    -      for (const val of results_list) {
    -        results[val.family].push({
    -          name: val.name.toString(),
    -          description: val.description.toString(),
    -          state: val.result,
    -          err_msg: val.err_msg.toString(),
    -          url: val.url.toString(),
    -          skip_reason: val.skip_reason.toString(),
    -        });
    -      }
    -
    -      this.setState({
    -        results: results,
    -        families: families,
    -        msg: `Results for ${state.repo}@${state.ref} (${state.refType})`,
    -        progress: false,
    -        err_msg: "",
    -        url: "",
    -      });
    -
    -      results_list.destroy();
    -      families_dict.destroy();
    -    });
    -  }
    -
    -  componentDidMount() {
    -    if (urlParams.get("repo")) {
    -      this.fetchRepoReferences(urlParams.get("repo"));
    -
    -      if (urlParams.get("ref")) {
    -        this.handleCompute();
    -      }
    -    }
    -  }
    -
    -  render() {
    -    const priorityBranches = ["HEAD", "main", "master", "develop", "stable"];
    -    const branchMap = new Map(
    -      this.state.refs.branches.map((branch) => [branch.name, branch]),
    -    );
    -
    -    let availableOptions = [];
    -
    -    // If no repo is entered or API hasn't returned any branches/tags yet,
    -    // show all five priority branches.
    -    if (
    -      this.state.repo === "" ||
    -      (this.state.refs.branches.length === 0 &&
    -        this.state.refs.tags.length === 0)
    -    ) {
    -      availableOptions = [
    -        { label: "HEAD (default branch)", value: "HEAD", type: "branch" },
    -        { label: "main (branch)", value: "main", type: "branch" },
    -        { label: "master (branch)", value: "master", type: "branch" },
    -        { label: "develop (branch)", value: "develop", type: "branch" },
    -        { label: "stable (branch)", value: "stable", type: "branch" },
    -      ];
    -    } else {
    -      const prioritizedBranches = [
    -        { label: "HEAD (default branch)", value: "HEAD", type: "branch" },
    -      ];
    -
    -      priorityBranches.slice(1).forEach((branchName) => {
    -        if (branchMap.has(branchName)) {
    -          prioritizedBranches.push({
    -            label: `${branchName} (branch)`,
    -            value: branchName,
    -            type: "branch",
    -          });
    -          // Remove from map so it doesn't get added twice.
    -          branchMap.delete(branchName);
    -        }
    -      });
    -
    -      const otherBranches = [];
    -      branchMap.forEach((branch) => {
    -        otherBranches.push({
    -          label: `${branch.name} (branch)`,
    -          value: branch.name,
    -          type: "branch",
    -        });
    -      });
    -      otherBranches.sort((a, b) => a.value.localeCompare(b.value));
    -
    -      const tagOptions = this.state.refs.tags.map((tag) => ({
    -        label: `${tag.name} (tag)`,
    -        value: tag.name,
    -        type: "tag",
    -      }));
    -      tagOptions.sort((a, b) => a.value.localeCompare(b.value));
    -
    -      availableOptions = [
    -        ...prioritizedBranches,
    -        ...otherBranches,
    -        ...tagOptions,
    -      ];
    -    }
    -
    -    return (
    -      
    -        
    -        
    -          {this.props.header && }
    -          
    -             {
    -                if (e.keyCode === 13)
    -                  document.getElementById("ref-select").focus();
    -              }}
    -              onInput={(e) => this.handleRepoChange(e.target.value)}
    -              defaultValue={urlParams.get("repo")}
    -              sx={{ flexGrow: 3 }}
    -            />
    -             {
    -                if (e.keyCode === 13) this.handleCompute();
    -              }}
    -              getOptionLabel={(option) =>
    -                typeof option === "string" ? option : option.label
    -              }
    -              renderOption={(props, option) => (
    -                
  • {option.label}
  • - )} - onInputChange={(e, value) => { - // If the user enters free text, treat it as a branch - if (typeof value === "string") { - this.handleRefChange(value, "branch"); - } - }} - onChange={(e, option) => { - if (option) { - if (typeof option === "object") { - this.handleRefChange(option.value, option.type); - } else { - this.handleRefChange(option, "branch"); - } - } - }} - defaultValue={urlParams.get("ref")} - renderInput={(params) => ( - - {this.state.loadingRefs ? ( - - ) : null} - {params.InputProps.endAdornment} - - ), - }} - /> - )} - /> - - this.handleCompute()} - variant="contained" - size="large" - disabled={ - this.state.progress || !this.state.repo || !this.state.ref - } - > - start - -
    - - - - - - {this.state.progress && } - {this.state.err_msg && ( - - - - )} - - - -
    -
    - ); - } -} diff --git a/helpers/fetch_repo_review_app.sh b/helpers/fetch_repo_review_app.sh new file mode 100755 index 00000000..f9b5588c --- /dev/null +++ b/helpers/fetch_repo_review_app.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +version="${1:-v1.0.0rc3}" +archive_url="https://github.com/scientific-python/repo-review/releases/download/${version}/repo-review-app.zip" +dest_dir="docs/assets/js" +tmp_zip="$(mktemp)" + +trap 'rm -f "$tmp_zip"' EXIT + +mkdir -p "$dest_dir" +curl -fsSL "$archive_url" -o "$tmp_zip" +unzip -o -j "$tmp_zip" repo-review-app.min.js repo-review-app.min.js.map -d "$dest_dir" >/dev/null diff --git a/pyproject.toml b/pyproject.toml index 69e51b6e..43157882 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ scikit-hep = "sp_repo_review.families:get_families" [project.entry-points."repo_review.prefetch_files"] root = "sp_repo_review.files:prefetch_root" -package = "repo_review.files:prefetch_package" +package = "sp_repo_review.files:prefetch_package" [dependency-groups]