Skip to content

Commit

Permalink
Asset improvements (#1990)
Browse files Browse the repository at this point in the history
* Load templates via plugin

* Refactor assets build script

* Remove [watch] log prefix

* Remove unneeded npx prefixes in package scripts
  • Loading branch information
liamcmitchell authored Dec 31, 2024
1 parent cb937ca commit 8c763fa
Show file tree
Hide file tree
Showing 17 changed files with 110 additions and 189 deletions.
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ jobs:
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('asssets/package-lock.json') }}
- run: mkdir -p tmp/handlebars
- run: npm ci --prefix assets
- run: npm run build --prefix assets
- name: Push updated assets
Expand Down
3 changes: 0 additions & 3 deletions assets/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,5 @@ module.exports = {
'no-throw-literal': 0,
'no-useless-escape': 0,
'object-curly-spacing': 0
},
globals: {
'Handlebars': 'readonly'
}
}
228 changes: 81 additions & 147 deletions assets/build/build.js
Original file line number Diff line number Diff line change
@@ -1,155 +1,89 @@
const path = require('node:path')
const process = require('node:process')
const child_process = require('node:child_process')
const cp = require('node:child_process')
const esbuild = require('esbuild')
const util = require('./utilities')

const watchMode = Boolean(process.env.npm_config_watch)


/**
* Configuration variables
*/

// Basic build configuration and values
const commonOptions = {
entryNames: '[name]-[hash]',
bundle: true,
minify: true,
logLevel: watchMode ? 'warning' : 'info',
}
const epubOutDir = path.resolve('../formatters/epub/dist')
const htmlOutDir = path.resolve('../formatters/html/dist')

// Handlebars template paths
const templates = {
sourceDir: path.resolve('js/handlebars/templates'),
compiledDir: path.resolve('../tmp/handlebars'),
filename: 'handlebars.templates.js',
}
templates.compiledPath = path.join(templates.compiledDir, templates.filename)


/**
* Build: Plugins
*/

// Empty outdir directories before both normal and watch-mode builds
const epubOnStartPlugin = {
name: 'epubOnStart',
setup(build) { build.onStart(() => util.ensureEmptyDirsExistSync([epubOutDir])) },
}
const htmlOnStartPlugin = {
name: 'htmlOnStart',
setup(build) { build.onStart(() => util.ensureEmptyDirsExistSync([htmlOutDir])) },
}
const fsExtra = require('fs-extra')
const fs = require('node:fs/promises')
const handlebars = require('handlebars')
const util = require('node:util')

const exec = util.promisify(cp.exec)

/**
* Build
*/

// ePub: esbuild options
const epubBuildOptions = {
...commonOptions,
outdir: epubOutDir,
plugins: [epubOnStartPlugin],
entryPoints: [
'js/entry/epub.js',
'css/entry/epub-elixir.css',
'css/entry/epub-erlang.css',
],
}

// ePub: esbuild (conditionally configuring watch mode and rebuilding of docs)
if (!watchMode) {
esbuild.build(epubBuildOptions).catch(() => process.exit(1))
} else {
esbuild.build({
...epubBuildOptions,
watch: {
onRebuild(error, result) {
if (error) {
console.error('[watch] epub build failed:', error)
} else {
console.log('[watch] epub assets rebuilt')
if (result.errors.length > 0) console.log('[watch] epub build errors:', result.errors)
if (result.warnings.length > 0) console.log('[watch] epub build warnings:', result.warnings)
generateDocs("epub")
}
},
},
}).then(() => generateDocs("epub")).catch(() => process.exit(1))
}

// HTML: Precompile Handlebars templates
util.runShellCmdSync(`npx handlebars ${templates.sourceDir} --output ${templates.compiledPath}`)
const watchMode = Boolean(process.env.npm_config_watch)

// HTML: esbuild options
const htmlBuildOptions = {
...commonOptions,
outdir: htmlOutDir,
plugins: [htmlOnStartPlugin],
entryPoints: [
templates.compiledPath,
'js/entry/html.js',
'css/entry/html-elixir.css',
'css/entry/html-erlang.css',
],
loader: {
'.woff2': 'file',
// TODO: Remove when @fontsource/* removes legacy .woff
'.woff': 'file',
/** @type {import('esbuild').BuildOptions[]} */
const formatters = [
{
formatter: 'epub',
outdir: path.resolve('../formatters/epub/dist'),
entryPoints: [
'js/entry/epub.js',
'css/entry/epub-elixir.css',
'css/entry/epub-erlang.css'
]
},
}

// HTML: esbuild (conditionally configuring watch mode and rebuilding of docs)
if (!watchMode) {
esbuild.build(htmlBuildOptions).then(() => buildTemplatesRuntime()).catch(() => process.exit(1))
} else {
esbuild.build({
...htmlBuildOptions,
watch: {
onRebuild(error, result) {
if (error) {
console.error('[watch] html build failed:', error)
} else {
console.log('[watch] html assets rebuilt')
if (result.errors.length > 0) console.log('[watch] html build errors:', result.errors)
if (result.warnings.length > 0) console.log('[watch] html build warnings:', result.warnings)
buildTemplatesRuntime()
generateDocs("html")
{
formatter: 'html',
outdir: path.resolve('../formatters/html/dist'),
entryPoints: [
'js/entry/html.js',
'css/entry/html-elixir.css',
'css/entry/html-erlang.css'
],
loader: {
'.woff2': 'file',
// TODO: Remove when @fontsource/* removes legacy .woff
'.woff': 'file'
}
}
]

Promise.all(formatters.map(async ({formatter, ...options}) => {
// Clean outdir.
await fsExtra.emptyDir(options.outdir)

await esbuild.build({
entryNames: watchMode ? '[name]-dev' : '[name]-[hash]',
bundle: true,
minify: !watchMode,
logLevel: watchMode ? 'warning' : 'info',
watch: watchMode,
...options,
plugins: [{
name: 'ex_doc',
setup (build) {
// Pre-compile handlebars templates.
build.onLoad({
filter: /\.handlebars$/
}, async ({ path: filename }) => {
try {
const source = await fs.readFile(filename, 'utf-8')
const template = handlebars.precompile(source)
const contents = [
"import * as Handlebars from 'handlebars/runtime'",
"import '../helpers'",
`export default Handlebars.template(${template})`
].join('\n')
return { contents }
} catch (error) {
return { errors: [{ text: error.message }] }
}
})

// Generate docs with new assets (watch mode only).
if (watchMode) {
build.onEnd(async result => {
if (result.errors.length) return
console.log(`${formatter} assets built`)
await exec('mix compile --force', {cwd: '../'})
await exec(`mix docs --formatter ${formatter}`, {cwd: '../'})
console.log(`${formatter} docs built`)
})
}
},
},
}).then(() => {
buildTemplatesRuntime()
generateDocs("html")
}).catch(() => process.exit(1))
}

/**
* Functions
*/

// HTML: Handlebars runtime
// The Handlebars runtime from the local module dist directory is used to ensure
// the version matches that which was used to compile the templates.
// 'bundle' must be false in order for 'Handlebar' to be available at runtime.
function buildTemplatesRuntime() {
esbuild.build({
...commonOptions,
outdir: htmlOutDir,
entryPoints: ['node_modules/handlebars/dist/handlebars.runtime.js'],
bundle: false,
}).catch(() => process.exit(1))
}

// Docs generation (used in watch mode only)
function generateDocs(formatter) {
console.log(`Building ${formatter} docs`)
process.chdir('../')
child_process.execSync('mix compile --force')
child_process.execSync(`mix docs --formatter ${formatter}`)
process.chdir('./assets/')
}
}
}]
})
})).catch((error) => {
console.error(error)
process.exit(1)
})
21 changes: 0 additions & 21 deletions assets/build/utilities.js

This file was deleted.

3 changes: 2 additions & 1 deletion assets/js/autocomplete/autocomplete-list.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getSuggestions } from './suggestions'
import { isBlank, qs } from '../helpers'
import { currentTheme } from '../theme'
import autocompleteSuggestionsTemplate from '../handlebars/templates/autocomplete-suggestions.handlebars'

export const AUTOCOMPLETE_CONTAINER_SELECTOR = '.autocomplete'
export const AUTOCOMPLETE_SUGGESTION_LIST_SELECTOR = '.autocomplete-suggestions'
Expand Down Expand Up @@ -56,7 +57,7 @@ export function updateAutocompleteList (searchTerm) {

// Updates list of suggestions inside the autocomplete.
function renderSuggestions ({ term, suggestions }) {
const autocompleteContainerHtml = Handlebars.templates['autocomplete-suggestions']({ suggestions, term })
const autocompleteContainerHtml = autocompleteSuggestionsTemplate({ suggestions, term })

const autocompleteContainer = qs(AUTOCOMPLETE_CONTAINER_SELECTOR)
autocompleteContainer.innerHTML = autocompleteContainerHtml
Expand Down
2 changes: 2 additions & 0 deletions assets/js/handlebars/helpers.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as Handlebars from 'handlebars/runtime'

Handlebars.registerHelper('groupChanged', function (context, nodeGroup, options) {
const group = nodeGroup || ''
if (context.group !== group) {
Expand Down
3 changes: 2 additions & 1 deletion assets/js/modal.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { qs } from './helpers'
import modalLayoutTemplate from './handlebars/templates/modal-layout.handlebars'

const MODAL_SELECTOR = '.modal'
const MODAL_CLOSE_BUTTON_SELECTOR = '.modal .modal-close'
Expand All @@ -22,7 +23,7 @@ export function initialize () {
* Adds the modal to DOM, initially it's hidden.
*/
function renderModal () {
const modalLayoutHtml = Handlebars.templates['modal-layout']()
const modalLayoutHtml = modalLayoutTemplate()
document.body.insertAdjacentHTML('beforeend', modalLayoutHtml)

qs(MODAL_SELECTOR).addEventListener('keydown', event => {
Expand Down
6 changes: 4 additions & 2 deletions assets/js/quick-switch.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { debounce, qs, qsAll } from './helpers'
import { openModal } from './modal'
import quickSwitchModalBodyTemplate from './handlebars/templates/quick-switch-modal-body.handlebars'
import quickSwitchResultsTemplate from './handlebars/templates/quick-switch-results.handlebars'

const HEX_DOCS_ENDPOINT = 'https://hexdocs.pm/%%'
const OTP_DOCS_ENDPOINT = 'https://www.erlang.org/doc/apps/%%'
Expand Down Expand Up @@ -115,7 +117,7 @@ function handleInput (event) {
export function openQuickSwitchModal () {
openModal({
title: 'Go to package docs',
body: Handlebars.templates['quick-switch-modal-body']()
body: quickSwitchModalBodyTemplate()
})

qs(QUICK_SWITCH_INPUT_SELECTOR).focus()
Expand Down Expand Up @@ -185,7 +187,7 @@ function queryForAutocomplete (packageSlug) {

function renderResults ({ results }) {
const resultsContainer = qs(QUICK_SWITCH_RESULTS_SELECTOR)
const resultsHtml = Handlebars.templates['quick-switch-results']({ results })
const resultsHtml = quickSwitchResultsTemplate({ results })
resultsContainer.innerHTML = resultsHtml

qsAll(QUICK_SWITCH_RESULT_SELECTOR).forEach(result => {
Expand Down
3 changes: 2 additions & 1 deletion assets/js/search-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lunr from 'lunr'
import { qs, escapeHtmlEntities, isBlank, getQueryParamByName, getProjectNameAndVersion } from './helpers'
import { setSearchInputValue } from './search-bar'
import searchResultsTemplate from './handlebars/templates/search-results.handlebars'

const EXCERPT_RADIUS = 80
const SEARCH_CONTAINER_SELECTOR = '#search'
Expand Down Expand Up @@ -48,7 +49,7 @@ async function search (value) {

function renderResults ({ value, results, errorMessage }) {
const searchContainer = qs(SEARCH_CONTAINER_SELECTOR)
const resultsHtml = Handlebars.templates['search-results']({ value, results, errorMessage })
const resultsHtml = searchResultsTemplate({ value, results, errorMessage })
searchContainer.innerHTML = resultsHtml
}

Expand Down
3 changes: 2 additions & 1 deletion assets/js/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { qs, qsAll } from './helpers'
import { openModal } from './modal'
import { settingsStore } from './settings-store'
import { keyboardShortcuts } from './keyboard-shortcuts'
import settingsModalBodyTemplate from './handlebars/templates/settings-modal-body.handlebars'

const SETTINGS_LINK_SELECTOR = '.display-settings'
const SETTINGS_MODAL_BODY_SELECTOR = '#settings-modal-content'
Expand Down Expand Up @@ -53,7 +54,7 @@ function showKeyboardShortcutsTab () {
export function openSettingsModal () {
openModal({
title: modalTabs.map(({id, title}) => `<button id="${id}">${title}</button>`).join(''),
body: Handlebars.templates['settings-modal-body']({ shortcuts: keyboardShortcuts })
body: settingsModalBodyTemplate({ shortcuts: keyboardShortcuts })
})

const modal = qs(SETTINGS_MODAL_BODY_SELECTOR)
Expand Down
3 changes: 2 additions & 1 deletion assets/js/sidebar/sidebar-list.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { qs, getCurrentPageSidebarType, getLocationHash, findSidebarCategory } from '../helpers'
import { getSidebarNodes } from '../globals'
import sidebarItemsTemplate from '../handlebars/templates/sidebar-items.handlebars'

const SIDEBAR_TYPE = {
search: 'search',
Expand Down Expand Up @@ -42,7 +43,7 @@ function renderSidebarNodeList (nodesByType, type) {
// Render the list
const nodeList = qs(sidebarNodeListSelector(type))
if (!nodeList) { return }
const listContentHtml = Handlebars.templates['sidebar-items']({ nodes, group: '' })
const listContentHtml = sidebarItemsTemplate({ nodes, group: '' })
nodeList.innerHTML = listContentHtml

// Removes the "expand" class from links belonging to single-level sections
Expand Down
Loading

0 comments on commit 8c763fa

Please sign in to comment.