-
Notifications
You must be signed in to change notification settings - Fork 174
Add html rendering pathway
#3027
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,7 @@ | |
|
|
||
| import fs from 'fs-extra'; | ||
| import path from 'node:path'; | ||
| import { writeFileToFolder } from 'myst-cli-utils'; | ||
| import { writeFileToFolder, createNpmLogger, makeExecutable } from 'myst-cli-utils'; | ||
| import type { MystXRefs } from 'myst-transforms'; | ||
| import type { ISession } from '../../session/types.js'; | ||
| import type { StartOptions } from '../site/start.js'; | ||
|
|
@@ -70,10 +70,12 @@ export async function currentSiteRoutes( | |
| }; | ||
| }), | ||
| // Download other assets | ||
| ...['robots.txt', 'myst-theme.css', 'sitemap.xml', 'sitemap_style.xsl'].map((asset) => ({ | ||
| url: `${host}/${asset}`, | ||
| path: asset, | ||
| })), | ||
| ...['robots.txt', 'myst-theme.css', 'sitemap.xml', 'sitemap_style.xsl', 'objects.inv'].map( | ||
| (asset) => ({ | ||
| url: `${host}/${asset}`, | ||
| path: asset, | ||
| }), | ||
| ), | ||
| { | ||
| url: `${host}/favicon.ico`, | ||
| path: 'favicon.ico', | ||
|
|
@@ -167,64 +169,102 @@ function getBaseUrl(session: ISession): string | undefined { | |
| */ | ||
| export async function buildHtml(session: ISession, opts: StartOptions) { | ||
| const template = await getSiteTemplate(session, opts); | ||
| // Ask template for command to render itself into HTML | ||
| const renderCommand = (template.getValidatedTemplateYml().build as any)?.render; | ||
| // The BASE_URL env variable allows for mounting the site in a folder, e.g., github pages | ||
| const baseurl = getBaseUrl(session); | ||
| // Note, this process is really only for Remix templates | ||
| // We could add a flag in the future for other templates | ||
| const htmlDir = path.join(session.buildPath(), 'html'); | ||
| fs.rmSync(htmlDir, { recursive: true, force: true }); | ||
| fs.mkdirSync(htmlDir, { recursive: true }); | ||
| const appServer = await startServer(session, { ...opts, buildStatic: true, baseurl }); | ||
| const appServer = await startServer(session, { | ||
| ...opts, | ||
| buildStatic: true, | ||
| baseurl, | ||
| // We do not need a running site if the template knows how to render itself | ||
| headless: renderCommand !== undefined, | ||
| }); | ||
| if (!appServer) return; | ||
|
|
||
| const host = `http://localhost:${appServer.port}`; | ||
| const routes = await currentSiteRoutes(session, host, baseurl); | ||
|
|
||
| // Fetch all HTML pages and assets by the template | ||
| await Promise.all( | ||
| routes.map(async (route) => | ||
| limitConnections(async () => { | ||
| try { | ||
| const resp = await fetchWithRetry(session, route.url); | ||
| if (!resp.ok) { | ||
| session.log.error(`Error fetching ${route.url}`); | ||
| return; | ||
| } | ||
| if (route.binary && resp.body) { | ||
| await new Promise<void>((resolve, reject) => { | ||
| const filename = path.join(htmlDir, route.path); | ||
| if (!fs.existsSync(filename)) | ||
| fs.mkdirSync(path.dirname(filename), { recursive: true }); | ||
| const fileWriteStream = fs.createWriteStream(filename); | ||
| resp.body!.pipe(fileWriteStream); | ||
| resp.body!.on('error', reject); | ||
| fileWriteStream.on('error', reject); | ||
| fileWriteStream.on('finish', resolve); | ||
| }); | ||
| } else { | ||
| const content = await resp.text(); | ||
| writeFileToFolder(path.join(htmlDir, route.path), content); | ||
| // Use the template to render itself | ||
| if (renderCommand !== undefined) { | ||
| // Run pre-rendering | ||
| await makeExecutable(renderCommand, createNpmLogger(session), { | ||
| cwd: template.templatePath, | ||
| env: { ...process.env, BUILD_DIRECTORY: htmlDir, CONTENT_CDN: host }, | ||
| })(); | ||
| } | ||
| // Fallback on fetch-based rendering (deprecated) | ||
| else { | ||
| const routes = await currentSiteRoutes(session, host, baseurl); | ||
|
|
||
| // Fetch all HTML pages and assets by the template | ||
| await Promise.all( | ||
| routes.map(async (route) => | ||
| limitConnections(async () => { | ||
| try { | ||
| const resp = await fetchWithRetry(session, route.url); | ||
| if (!resp.ok) { | ||
| session.log.error(`Error fetching ${route.url}`); | ||
| return; | ||
| } | ||
| if (route.binary && resp.body) { | ||
| await new Promise<void>((resolve, reject) => { | ||
| const filename = path.join(htmlDir, route.path); | ||
| if (!fs.existsSync(filename)) | ||
| fs.mkdirSync(path.dirname(filename), { recursive: true }); | ||
| const fileWriteStream = fs.createWriteStream(filename); | ||
| resp.body!.pipe(fileWriteStream); | ||
| resp.body!.on('error', reject); | ||
| fileWriteStream.on('error', reject); | ||
| fileWriteStream.on('finish', resolve); | ||
| }); | ||
| } else { | ||
| const content = await resp.text(); | ||
| writeFileToFolder(path.join(htmlDir, route.path), content); | ||
| } | ||
| } catch (error) { | ||
| if (!route.optional) throw error; | ||
| session.log.warn( | ||
| `Could not fetch optional asset ${route.url}: ${(error as Error).message}`, | ||
| ); | ||
| } | ||
| } catch (error) { | ||
| if (!route.optional) throw error; | ||
| session.log.warn( | ||
| `Could not fetch optional asset ${route.url}: ${(error as Error).message}`, | ||
| ); | ||
| } | ||
| }), | ||
| ), | ||
| ); | ||
| await appServer.stop(); | ||
| }), | ||
| ), | ||
| ); | ||
|
|
||
| // Copy the files for the template used. | ||
| // | ||
| // This always includes the thebe JS chunks, even when no project enables | ||
| // `thebe`/`jupyter`. The myst-theme uses thebe-core to render Jupyter cell | ||
| // outputs, so these chunks are required for outputs to render at all. | ||
| const templateBuildDir = path.join(template.templatePath, 'public'); | ||
| fs.copySync(templateBuildDir, htmlDir); | ||
| // Copy all of the static assets | ||
| fs.copySync(session.publicPath(), path.join(htmlDir, 'build')); | ||
| fs.copySync( | ||
| path.join(session.sitePath(), 'myst.search.json'), | ||
| path.join(htmlDir, 'myst.search.json'), | ||
| ); | ||
|
|
||
| // Copy all of the static assets | ||
| fs.copySync(session.publicPath(), path.join(htmlDir, 'build')); | ||
| // NOTE: HTML static output needs to patch the contents, this is done on the fly by the server | ||
| const xrefs = JSON.parse( | ||
| fs.readFileSync(path.join(session.sitePath(), 'myst.xref.json')).toString(), | ||
| ) as MystXRefs; | ||
| xrefs.references?.forEach((ref) => { | ||
| ref.data = ref.data?.replace(/^\/content/, ''); | ||
| }); | ||
| fs.writeFileSync(path.join(htmlDir, 'myst.xref.json'), JSON.stringify(xrefs)); | ||
|
|
||
| // Copy the files for the template used. | ||
| // | ||
| // This always includes the thebe JS chunks, even when no project enables | ||
| // `thebe`/`jupyter`. The myst-theme uses thebe-core to render Jupyter cell | ||
| // outputs, so these chunks are required for outputs to render at all. | ||
| const templateBuildDir = path.join(template.templatePath, 'public'); | ||
| fs.copySync(templateBuildDir, htmlDir); | ||
| fs.copySync(path.join(session.sitePath(), 'config.json'), path.join(htmlDir, 'config.json')); | ||
|
|
||
| // We need to go through and change all links to the right folder | ||
| rewriteAssetsFolder(htmlDir, baseurl); | ||
| } | ||
| await appServer.stop(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the template is in charge of rendering html, then we shouldn't have to start the server (some could probably do it without a server, but it should be a template choice).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, we only want the content server. To-do! |
||
|
|
||
| // Copy user static files to html build root | ||
| const siteConfig = selectors.selectCurrentSiteConfig(session.store.getState()); | ||
|
|
@@ -233,27 +273,6 @@ export async function buildHtml(session: ISession, opts: StartOptions) { | |
| const projectConfig = selectors.selectLocalProjectConfig(session.store.getState(), proj.path); | ||
| copyStaticFiles(session, projectConfig?.static_files ?? [], htmlDir, proj.path); | ||
| } | ||
| fs.copySync(path.join(session.sitePath(), 'config.json'), path.join(htmlDir, 'config.json')); | ||
| fs.copySync(path.join(session.sitePath(), 'public.json'), path.join(htmlDir, 'public.json')); | ||
| fs.copySync(path.join(session.sitePath(), 'objects.inv'), path.join(htmlDir, 'objects.inv')); | ||
|
|
||
| // NOTE: HTML static output needs to patch the contents, this is done on the fly by the server | ||
| const xrefs = JSON.parse( | ||
| fs.readFileSync(path.join(session.sitePath(), 'myst.xref.json')).toString(), | ||
| ) as MystXRefs; | ||
| xrefs.references?.forEach((ref) => { | ||
| ref.data = ref.data?.replace(/^\/content/, ''); | ||
| }); | ||
| fs.writeFileSync(path.join(htmlDir, 'myst.xref.json'), JSON.stringify(xrefs)); | ||
|
|
||
| // Copy the search index | ||
| fs.copySync( | ||
| path.join(session.sitePath(), 'myst.search.json'), | ||
| path.join(htmlDir, 'myst.search.json'), | ||
| ); | ||
|
|
||
| // We need to go through and change all links to the right folder | ||
| rewriteAssetsFolder(htmlDir, baseurl); | ||
|
|
||
| // Explicitly close the process as the web server doesn't always stop? | ||
| process.exit(0); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be the responsibility of theme render function if provided of ensuring all assets are available. (Might need to pass template directory in as env variable?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code-path is only for the existing rendering (if a template doesn't support HTML rendering). My view is that we deprecate this, but remove it later.