Skip to content

Commit 8c763fa

Browse files
Asset improvements (#1990)
* Load templates via plugin * Refactor assets build script * Remove [watch] log prefix * Remove unneeded npx prefixes in package scripts
1 parent cb937ca commit 8c763fa

File tree

17 files changed

+110
-189
lines changed

17 files changed

+110
-189
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ jobs:
9494
with:
9595
path: ~/.npm
9696
key: ${{ runner.os }}-node-${{ hashFiles('asssets/package-lock.json') }}
97-
- run: mkdir -p tmp/handlebars
9897
- run: npm ci --prefix assets
9998
- run: npm run build --prefix assets
10099
- name: Push updated assets

assets/.eslintrc.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,5 @@ module.exports = {
1616
'no-throw-literal': 0,
1717
'no-useless-escape': 0,
1818
'object-curly-spacing': 0
19-
},
20-
globals: {
21-
'Handlebars': 'readonly'
2219
}
2320
}

assets/build/build.js

Lines changed: 81 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,155 +1,89 @@
11
const path = require('node:path')
22
const process = require('node:process')
3-
const child_process = require('node:child_process')
3+
const cp = require('node:child_process')
44
const esbuild = require('esbuild')
5-
const util = require('./utilities')
6-
7-
const watchMode = Boolean(process.env.npm_config_watch)
8-
9-
10-
/**
11-
* Configuration variables
12-
*/
13-
14-
// Basic build configuration and values
15-
const commonOptions = {
16-
entryNames: '[name]-[hash]',
17-
bundle: true,
18-
minify: true,
19-
logLevel: watchMode ? 'warning' : 'info',
20-
}
21-
const epubOutDir = path.resolve('../formatters/epub/dist')
22-
const htmlOutDir = path.resolve('../formatters/html/dist')
23-
24-
// Handlebars template paths
25-
const templates = {
26-
sourceDir: path.resolve('js/handlebars/templates'),
27-
compiledDir: path.resolve('../tmp/handlebars'),
28-
filename: 'handlebars.templates.js',
29-
}
30-
templates.compiledPath = path.join(templates.compiledDir, templates.filename)
31-
32-
33-
/**
34-
* Build: Plugins
35-
*/
36-
37-
// Empty outdir directories before both normal and watch-mode builds
38-
const epubOnStartPlugin = {
39-
name: 'epubOnStart',
40-
setup(build) { build.onStart(() => util.ensureEmptyDirsExistSync([epubOutDir])) },
41-
}
42-
const htmlOnStartPlugin = {
43-
name: 'htmlOnStart',
44-
setup(build) { build.onStart(() => util.ensureEmptyDirsExistSync([htmlOutDir])) },
45-
}
5+
const fsExtra = require('fs-extra')
6+
const fs = require('node:fs/promises')
7+
const handlebars = require('handlebars')
8+
const util = require('node:util')
469

10+
const exec = util.promisify(cp.exec)
4711

48-
/**
49-
* Build
50-
*/
51-
52-
// ePub: esbuild options
53-
const epubBuildOptions = {
54-
...commonOptions,
55-
outdir: epubOutDir,
56-
plugins: [epubOnStartPlugin],
57-
entryPoints: [
58-
'js/entry/epub.js',
59-
'css/entry/epub-elixir.css',
60-
'css/entry/epub-erlang.css',
61-
],
62-
}
63-
64-
// ePub: esbuild (conditionally configuring watch mode and rebuilding of docs)
65-
if (!watchMode) {
66-
esbuild.build(epubBuildOptions).catch(() => process.exit(1))
67-
} else {
68-
esbuild.build({
69-
...epubBuildOptions,
70-
watch: {
71-
onRebuild(error, result) {
72-
if (error) {
73-
console.error('[watch] epub build failed:', error)
74-
} else {
75-
console.log('[watch] epub assets rebuilt')
76-
if (result.errors.length > 0) console.log('[watch] epub build errors:', result.errors)
77-
if (result.warnings.length > 0) console.log('[watch] epub build warnings:', result.warnings)
78-
generateDocs("epub")
79-
}
80-
},
81-
},
82-
}).then(() => generateDocs("epub")).catch(() => process.exit(1))
83-
}
84-
85-
// HTML: Precompile Handlebars templates
86-
util.runShellCmdSync(`npx handlebars ${templates.sourceDir} --output ${templates.compiledPath}`)
12+
const watchMode = Boolean(process.env.npm_config_watch)
8713

88-
// HTML: esbuild options
89-
const htmlBuildOptions = {
90-
...commonOptions,
91-
outdir: htmlOutDir,
92-
plugins: [htmlOnStartPlugin],
93-
entryPoints: [
94-
templates.compiledPath,
95-
'js/entry/html.js',
96-
'css/entry/html-elixir.css',
97-
'css/entry/html-erlang.css',
98-
],
99-
loader: {
100-
'.woff2': 'file',
101-
// TODO: Remove when @fontsource/* removes legacy .woff
102-
'.woff': 'file',
14+
/** @type {import('esbuild').BuildOptions[]} */
15+
const formatters = [
16+
{
17+
formatter: 'epub',
18+
outdir: path.resolve('../formatters/epub/dist'),
19+
entryPoints: [
20+
'js/entry/epub.js',
21+
'css/entry/epub-elixir.css',
22+
'css/entry/epub-erlang.css'
23+
]
10324
},
104-
}
105-
106-
// HTML: esbuild (conditionally configuring watch mode and rebuilding of docs)
107-
if (!watchMode) {
108-
esbuild.build(htmlBuildOptions).then(() => buildTemplatesRuntime()).catch(() => process.exit(1))
109-
} else {
110-
esbuild.build({
111-
...htmlBuildOptions,
112-
watch: {
113-
onRebuild(error, result) {
114-
if (error) {
115-
console.error('[watch] html build failed:', error)
116-
} else {
117-
console.log('[watch] html assets rebuilt')
118-
if (result.errors.length > 0) console.log('[watch] html build errors:', result.errors)
119-
if (result.warnings.length > 0) console.log('[watch] html build warnings:', result.warnings)
120-
buildTemplatesRuntime()
121-
generateDocs("html")
25+
{
26+
formatter: 'html',
27+
outdir: path.resolve('../formatters/html/dist'),
28+
entryPoints: [
29+
'js/entry/html.js',
30+
'css/entry/html-elixir.css',
31+
'css/entry/html-erlang.css'
32+
],
33+
loader: {
34+
'.woff2': 'file',
35+
// TODO: Remove when @fontsource/* removes legacy .woff
36+
'.woff': 'file'
37+
}
38+
}
39+
]
40+
41+
Promise.all(formatters.map(async ({formatter, ...options}) => {
42+
// Clean outdir.
43+
await fsExtra.emptyDir(options.outdir)
44+
45+
await esbuild.build({
46+
entryNames: watchMode ? '[name]-dev' : '[name]-[hash]',
47+
bundle: true,
48+
minify: !watchMode,
49+
logLevel: watchMode ? 'warning' : 'info',
50+
watch: watchMode,
51+
...options,
52+
plugins: [{
53+
name: 'ex_doc',
54+
setup (build) {
55+
// Pre-compile handlebars templates.
56+
build.onLoad({
57+
filter: /\.handlebars$/
58+
}, async ({ path: filename }) => {
59+
try {
60+
const source = await fs.readFile(filename, 'utf-8')
61+
const template = handlebars.precompile(source)
62+
const contents = [
63+
"import * as Handlebars from 'handlebars/runtime'",
64+
"import '../helpers'",
65+
`export default Handlebars.template(${template})`
66+
].join('\n')
67+
return { contents }
68+
} catch (error) {
69+
return { errors: [{ text: error.message }] }
70+
}
71+
})
72+
73+
// Generate docs with new assets (watch mode only).
74+
if (watchMode) {
75+
build.onEnd(async result => {
76+
if (result.errors.length) return
77+
console.log(`${formatter} assets built`)
78+
await exec('mix compile --force', {cwd: '../'})
79+
await exec(`mix docs --formatter ${formatter}`, {cwd: '../'})
80+
console.log(`${formatter} docs built`)
81+
})
12282
}
123-
},
124-
},
125-
}).then(() => {
126-
buildTemplatesRuntime()
127-
generateDocs("html")
128-
}).catch(() => process.exit(1))
129-
}
130-
131-
/**
132-
* Functions
133-
*/
134-
135-
// HTML: Handlebars runtime
136-
// The Handlebars runtime from the local module dist directory is used to ensure
137-
// the version matches that which was used to compile the templates.
138-
// 'bundle' must be false in order for 'Handlebar' to be available at runtime.
139-
function buildTemplatesRuntime() {
140-
esbuild.build({
141-
...commonOptions,
142-
outdir: htmlOutDir,
143-
entryPoints: ['node_modules/handlebars/dist/handlebars.runtime.js'],
144-
bundle: false,
145-
}).catch(() => process.exit(1))
146-
}
147-
148-
// Docs generation (used in watch mode only)
149-
function generateDocs(formatter) {
150-
console.log(`Building ${formatter} docs`)
151-
process.chdir('../')
152-
child_process.execSync('mix compile --force')
153-
child_process.execSync(`mix docs --formatter ${formatter}`)
154-
process.chdir('./assets/')
155-
}
83+
}
84+
}]
85+
})
86+
})).catch((error) => {
87+
console.error(error)
88+
process.exit(1)
89+
})

assets/build/utilities.js

Lines changed: 0 additions & 21 deletions
This file was deleted.

assets/js/autocomplete/autocomplete-list.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getSuggestions } from './suggestions'
22
import { isBlank, qs } from '../helpers'
33
import { currentTheme } from '../theme'
4+
import autocompleteSuggestionsTemplate from '../handlebars/templates/autocomplete-suggestions.handlebars'
45

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

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

6162
const autocompleteContainer = qs(AUTOCOMPLETE_CONTAINER_SELECTOR)
6263
autocompleteContainer.innerHTML = autocompleteContainerHtml

assets/js/handlebars/helpers.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as Handlebars from 'handlebars/runtime'
2+
13
Handlebars.registerHelper('groupChanged', function (context, nodeGroup, options) {
24
const group = nodeGroup || ''
35
if (context.group !== group) {

assets/js/modal.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { qs } from './helpers'
2+
import modalLayoutTemplate from './handlebars/templates/modal-layout.handlebars'
23

34
const MODAL_SELECTOR = '.modal'
45
const MODAL_CLOSE_BUTTON_SELECTOR = '.modal .modal-close'
@@ -22,7 +23,7 @@ export function initialize () {
2223
* Adds the modal to DOM, initially it's hidden.
2324
*/
2425
function renderModal () {
25-
const modalLayoutHtml = Handlebars.templates['modal-layout']()
26+
const modalLayoutHtml = modalLayoutTemplate()
2627
document.body.insertAdjacentHTML('beforeend', modalLayoutHtml)
2728

2829
qs(MODAL_SELECTOR).addEventListener('keydown', event => {

assets/js/quick-switch.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { debounce, qs, qsAll } from './helpers'
22
import { openModal } from './modal'
3+
import quickSwitchModalBodyTemplate from './handlebars/templates/quick-switch-modal-body.handlebars'
4+
import quickSwitchResultsTemplate from './handlebars/templates/quick-switch-results.handlebars'
35

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

121123
qs(QUICK_SWITCH_INPUT_SELECTOR).focus()
@@ -185,7 +187,7 @@ function queryForAutocomplete (packageSlug) {
185187

186188
function renderResults ({ results }) {
187189
const resultsContainer = qs(QUICK_SWITCH_RESULTS_SELECTOR)
188-
const resultsHtml = Handlebars.templates['quick-switch-results']({ results })
190+
const resultsHtml = quickSwitchResultsTemplate({ results })
189191
resultsContainer.innerHTML = resultsHtml
190192

191193
qsAll(QUICK_SWITCH_RESULT_SELECTOR).forEach(result => {

assets/js/search-page.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import lunr from 'lunr'
44
import { qs, escapeHtmlEntities, isBlank, getQueryParamByName, getProjectNameAndVersion } from './helpers'
55
import { setSearchInputValue } from './search-bar'
6+
import searchResultsTemplate from './handlebars/templates/search-results.handlebars'
67

78
const EXCERPT_RADIUS = 80
89
const SEARCH_CONTAINER_SELECTOR = '#search'
@@ -48,7 +49,7 @@ async function search (value) {
4849

4950
function renderResults ({ value, results, errorMessage }) {
5051
const searchContainer = qs(SEARCH_CONTAINER_SELECTOR)
51-
const resultsHtml = Handlebars.templates['search-results']({ value, results, errorMessage })
52+
const resultsHtml = searchResultsTemplate({ value, results, errorMessage })
5253
searchContainer.innerHTML = resultsHtml
5354
}
5455

assets/js/settings.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { qs, qsAll } from './helpers'
22
import { openModal } from './modal'
33
import { settingsStore } from './settings-store'
44
import { keyboardShortcuts } from './keyboard-shortcuts'
5+
import settingsModalBodyTemplate from './handlebars/templates/settings-modal-body.handlebars'
56

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

5960
const modal = qs(SETTINGS_MODAL_BODY_SELECTOR)

0 commit comments

Comments
 (0)