diff --git a/packages/myst-cli/src/process/loadReferences.ts b/packages/myst-cli/src/process/loadReferences.ts index e347d99ee..177586353 100644 --- a/packages/myst-cli/src/process/loadReferences.ts +++ b/packages/myst-cli/src/process/loadReferences.ts @@ -15,13 +15,11 @@ import { } from '../session/cache.js'; import type { ISession } from '../session/types.js'; import { selectors } from '../store/index.js'; -import { XREF_MAX_AGE } from '../transforms/crossReferences.js'; +import { XREF_MAX_AGE, mystXRefsCacheFilename } from '../transforms/crossReferences.js'; import { dirname } from 'node:path'; -function inventoryCacheFilename(refKind: 'intersphinx' | 'myst', id: string, path: string) { - const hashcontent = `${id}${path}`; - const ext = refKind === 'intersphinx' ? 'inv' : 'json'; - return `xrefs-${refKind}-${computeHash(hashcontent)}.${ext}`; +function sphinxInventoryCacheFilename(path: string) { + return `xrefs-intersphinx-${computeHash(path)}.inv`; } async function preloadReference(session: ISession, key: string, reference: ExternalReference) { @@ -31,11 +29,11 @@ async function preloadReference(session: ISession, key: string, reference: Exter kind: reference.kind, }; const toc = tic(); - const mystXRefFilename = inventoryCacheFilename('myst', key, reference.url); + const mystXRefFilename = mystXRefsCacheFilename(reference.url); const mystXRefData = loadFromCache(session, mystXRefFilename, { maxAge: XREF_MAX_AGE, }); - const intersphinxFilename = inventoryCacheFilename('intersphinx', key, reference.url); + const intersphinxFilename = sphinxInventoryCacheFilename(reference.url); if ((!ref.kind || ref.kind === 'myst') && !!mystXRefData) { session.log.debug(`Loading cached inventory file for ${reference.url}: ${mystXRefFilename}`); const xrefs = JSON.parse(mystXRefData); @@ -93,11 +91,7 @@ async function loadReference( reference.kind = 'myst'; const mystXRefs = (await mystXRefsResp?.json()) as MystXRefs; session.log.debug(`Saving remote myst xref file to cache: ${reference.url}`); - writeToCache( - session, - inventoryCacheFilename('myst', reference.key, reference.url), - JSON.stringify(mystXRefs), - ); + writeToCache(session, mystXRefsCacheFilename(reference.url), JSON.stringify(mystXRefs)); reference.value = mystXRefs; session.log.info( toc(`🏫 Read ${mystXRefs.references.length} myst references for "${reference.key}" in %s.`), @@ -132,10 +126,7 @@ async function loadReference( reference.kind = 'intersphinx'; reference.value = inventory; if (inventory.id && inventory.path && isUrl(inventory.path)) { - const intersphinxPath = cachePath( - session, - inventoryCacheFilename('intersphinx', inventory.id, inventory.path), - ); + const intersphinxPath = cachePath(session, sphinxInventoryCacheFilename(inventory.path)); if (!fs.existsSync(intersphinxPath)) { session.log.debug(`Saving remote inventory file to cache: ${inventory.path}`); fs.mkdirSync(dirname(intersphinxPath), { recursive: true }); diff --git a/packages/myst-cli/src/transforms/crossReferences.ts b/packages/myst-cli/src/transforms/crossReferences.ts index 470c1a956..e2909dd4d 100644 --- a/packages/myst-cli/src/transforms/crossReferences.ts +++ b/packages/myst-cli/src/transforms/crossReferences.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import type { VFile } from 'vfile'; import { selectAll } from 'unist-util-select'; import type { FrontmatterParts, GenericNode, GenericParent, References } from 'myst-common'; @@ -7,7 +8,7 @@ import { addChildrenFromTargetNode } from 'myst-transforms'; import type { PageFrontmatter } from 'myst-frontmatter'; import type { CrossReference, Dependency, Link, SourceFileKind } from 'myst-spec-ext'; import type { ISession } from '../session/types.js'; -import { loadFromCache, writeToCache } from '../session/cache.js'; +import { loadFromCache, writeToCache, checkCache, cachePath } from '../session/cache.js'; import type { SiteAction, SiteExport } from 'myst-config'; export const XREF_MAX_AGE = 1; // in days @@ -16,6 +17,10 @@ function mystDataFilename(dataUrl: string) { return `myst-${computeHash(dataUrl)}.json`; } +export function mystXRefsCacheFilename(url: string) { + return `xrefs-myst-${computeHash(url)}.json`; +} + export type MystData = { kind?: SourceFileKind; sha256?: string; @@ -74,12 +79,93 @@ export async function fetchMystLinkData(session: ISession, node: Link, vfile: VF return fetchMystData(session, node.dataUrl, node.urlSource, vfile); } +const MYST_SPEC_VERSION = '0'; + +function upgradeMystData(version: string, data: any): MystData { + return data; +} + +async function stepwiseDowngrade( + session: ISession, + fromVersion: string, + toVersion: string, + vfile: VFile, + data: any, +): Promise { + const downgradeCachePath = `myst-downgrade-${fromVersion}-${toVersion}.mjs`; + if ( + !checkCache(session, downgradeCachePath, { + maxAge: 7, // days + }) + ) { + try { + const response = await fetch(`http://localhost:9000/${downgradeCachePath}`); + const body = await response.text(); + fs.writeFileSync(cachePath(session, downgradeCachePath), body); + } catch (err) { + fileWarn( + vfile, + `Unable to load utility for downgrading XRef from ${fromVersion} to ${toVersion}`, + ); + return data; + } + } + + const module = await import(cachePath(session, downgradeCachePath)); + return module.default(data); +} + +async function downgradeMystData( + session: ISession, + version: string, + vfile: VFile, + data: any, +): Promise { + const fromVersion = parseInt(version); + const toVersion = parseInt(MYST_SPEC_VERSION); + for (let stepVersion = fromVersion; stepVersion !== toVersion; stepVersion--) { + data = await stepwiseDowngrade(session, `${version}`, `${stepVersion - 1}`, vfile, data); + } + return data; +} + export async function fetchMystXRefData(session: ISession, node: CrossReference, vfile: VFile) { let dataUrl: string | undefined; if (node.remoteBaseUrl && node.dataUrl) { dataUrl = `${node.remoteBaseUrl}${node.dataUrl}`; } - return fetchMystData(session, dataUrl, node.urlSource, vfile); + const rawData = await fetchMystData(session, dataUrl, node.urlSource, vfile); + let data: MystData | undefined; + if (node.remoteBaseUrl && !!rawData) { + // Retrieve the external xref information to determine the spec version + const cachePath = mystXRefsCacheFilename(node.remoteBaseUrl); + const mystXRefData = loadFromCache(session, cachePath, { + maxAge: XREF_MAX_AGE, + }); + if (!mystXRefData) { + fileWarn(vfile, `Unable to load external MyST reference data: ${node.remoteBaseUrl}`); + } + // Bring potentially incompatible schema into-alignment + else { + const { version } = JSON.parse(mystXRefData) as { version: string }; + if (version === MYST_SPEC_VERSION) { + data = rawData; + } else if (parseInt(version) < parseInt(MYST_SPEC_VERSION)) { + data = upgradeMystData(version, rawData); + } else { + console.log(`Upgrading xref ${node.urlSource} with version ${version}`); + data = await downgradeMystData(session, version, vfile, rawData); + } + } + data = rawData; + } else { + fileWarn( + vfile, + `Unable to determine XRef AST version for external MyST reference: ${node.urlSource}`, + ); + data = rawData; + } + return data; } export function nodesFromMystXRefData(