diff --git a/app/components/crate-row.hbs b/app/components/crate-row.hbs
index 9b864218530..383cfdd82e6 100644
--- a/app/components/crate-row.hbs
+++ b/app/components/crate-row.hbs
@@ -61,12 +61,12 @@
{{#if @crate.homepage}}
Homepage
{{/if}}
- {{#if @crate.documentation}}
- Documentation
+ {{#if @crate.documentationLink}}
+ Documentation
{{/if}}
{{#if @crate.repository}}
Repository
{{/if}}
-
\ No newline at end of file
+
diff --git a/app/controllers/search.js b/app/controllers/search.js
index 65c393b0fb8..a00ea45ae8e 100644
--- a/app/controllers/search.js
+++ b/app/controllers/search.js
@@ -3,9 +3,10 @@ import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
-import { restartableTask } from 'ember-concurrency';
+import { all, restartableTask } from 'ember-concurrency';
import { bool, reads } from 'macro-decorators';
+import { AjaxError } from '../utils/ajax';
import { pagination } from '../utils/pagination';
import { CATEGORY_PREFIX, processSearchQuery } from '../utils/search';
@@ -73,6 +74,22 @@ export default class SearchController extends Controller {
? { page, per_page, sort, q: query, all_keywords }
: { page, per_page, sort, ...processSearchQuery(query) };
- return await this.store.query('crate', searchOptions);
+ const crates = await this.store.query('crate', searchOptions);
+
+ // Prime the docs for the most recent versions of each crate.
+ const docTasks = [];
+ for (const crate of crates) {
+ docTasks.push(crate.loadDocsStatusTask.perform());
+ }
+ try {
+ await all(docTasks);
+ } catch (e) {
+ // report unexpected errors to Sentry and ignore `ajax()` errors
+ if (!didCancel(error) && !(error instanceof AjaxError)) {
+ this.sentry.captureException(error);
+ }
+ }
+
+ return crates;
});
}
diff --git a/app/models/crate.js b/app/models/crate.js
index 92864803e83..b5b6870af73 100644
--- a/app/models/crate.js
+++ b/app/models/crate.js
@@ -1,9 +1,12 @@
import Model, { attr, hasMany } from '@ember-data/model';
import { waitForPromise } from '@ember/test-waiters';
+import { task } from 'ember-concurrency';
import { apiAction } from '@mainmatter/ember-api-actions';
import { cached } from 'tracked-toolbox';
+import ajax from '../utils/ajax';
+
export default class Crate extends Model {
@attr name;
@attr downloads;
@@ -42,6 +45,45 @@ export default class Crate extends Model {
}
}
+ get documentationLink() {
+ let crateDocsLink = this.documentation;
+
+ // if this is *not* a docs.rs link we'll return it directly
+ if (crateDocsLink && !crateDocsLink.startsWith('https://docs.rs/')) {
+ return crateDocsLink;
+ }
+
+ // if we know about a successful docs.rs build, we'll return a link to that
+ let { docsRsLink } = this;
+ if (docsRsLink) {
+ return docsRsLink;
+ }
+
+ // finally, we'll return the specified documentation link, whatever it is
+ if (crateDocsLink) {
+ return crateDocsLink;
+ }
+
+ return null;
+ }
+
+ loadDocsStatusTask = task(async () => {
+ if (!this.documentation) {
+ return await ajax(`https://docs.rs/crate/${this.name}/=${this.defaultVersion}/status.json`);
+ }
+ });
+
+ get hasDocsRsLink() {
+ let docsStatus = this.loadDocsStatusTask.lastSuccessful?.value;
+ return docsStatus?.doc_status === true;
+ }
+
+ get docsRsLink() {
+ if (this.hasDocsRsLink) {
+ return `https://docs.rs/${this.name}`;
+ }
+ }
+
@cached get versionIdsBySemver() {
let versions = this.versions.toArray() ?? [];
return versions.sort(compareVersionBySemver).map(v => v.id);