Bundling JS with ESBuild (#702)

* add npm dependencies used in this theme

* implement helper to configure JS and ESBuild

* migrate jquery popper.js bootstrap fontawesome to js bundle

* refactor main.js into smaller pieces, and moved navbar.js to assets

* remove list.js. It adjusts post card height to be the same, but is actually not needed.

* refactored notes.js, search.js, single.js into application.js

* move ityped to js asset, implement experiences horizontal vertical line in css

* align recent post height via css

* migrated home.js and refactored into various sections

* migrated darkMode feature to js bundle

* moved mermaid feature to js bundle

* migrate syntax highlight to js bundle

* migrate katex ( js portion ) to js bundle

* migrate pdf-js to js bundle by delegating to cdn

* set explicit comparisions for feature envvars so js can properly optimize

* removed goat-counter

* more fixes for broken achievements and small bugs

* more bug fixes

* allow configuration of hightlight.js, fix video-player shortcode

* remove jquery all together

* add null handling and fix merge conflicts

Co-authored-by: Aaron Qian <aaron@yeet.io>
This commit is contained in:
Aaron Qian 2023-01-05 10:42:54 -08:00 committed by GitHub
parent fe14b0fbf5
commit 02db3d3044
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
491 changed files with 4919 additions and 151344 deletions

View file

@ -1,6 +1,7 @@
<!-- Add Analytics if enabled in configuration -->
{{ with site.Params.features.analytics }}
{{ if .enabled }}
{{ with .services }}
<!-- Google Analytics -->
{{ with .google }}
{{ $privacyConfig:= dict (slice "Site" "Config" "Privacy" "GoogleAnalytics") $.Site.Config.Privacy.GoogleAnalytics }}
@ -21,7 +22,7 @@
<script
data-goatcounter="https://{{ .code }}.goatcounter.com/count"
async
src="/js/goat-counter.js"
src="//gc.zgo.at/count.js"
></script>
{{ end }}
@ -42,6 +43,7 @@
})();
</script>
{{ end }}
{{ end }}
{{ end }}
{{ end }}

View file

@ -42,8 +42,9 @@
{{ end }}
<div class="project-btn-holder">
{{ if .repo }}
<!-- Place this tag where you want the button to render. -->
<a
class="github-button-inactive project-btn"
class="github-button project-btn d-none"
href="{{ .repo }}"
data-icon="octicon-standard"
data-show-count="true"

View file

@ -7,17 +7,14 @@
<link rel="stylesheet" href="{{ "/css/layouts/main.css" | relURL }}"/>
<link rel="stylesheet" href="{{ "/css/navigators/navbar.css" | relURL }}"/>
<link rel="stylesheet" href="{{ "/css/plyr.css" | relURL }}"/>
{{ if ne site.Params.showFlags false }}
{{ if ne site.Params.features.flags.enable false }}
<link rel="stylesheet" href="{{ "/css/flag-icon.min.css" | relURL }}"/>
{{ end }}
<!--=================== fonts ==============================-->
<link rel="stylesheet" href="{{ "/google-fonts/Mulish/mulish.css" | relURL }}"/>
<!--=================== icons ==============================-->
<link rel="stylesheet" href="{{ "/fontawesome/css/all.min.css" | relURL }}"/>
<!--=================== dark mode ==========================-->
{{ if site.Params.darkMode.enable }}
{{ if site.Params.features.darkMode.enable }}
<link rel="stylesheet" href="{{ "/css/colortheme/colortheme.css" | relURL }}"/>
{{ end }}

View file

@ -14,7 +14,7 @@
{{/* if the user specify a country code for a language via "params.flagOverwrites" field, then use it. */}}
{{ range site.Params.flagOverwrites }}
{{ range site.Params.features.flags.flagOverwrites }}
{{ if eq $languageCode .languageCode }}
{{ $countryCode = .countryCode }}
{{ end }}

View file

@ -0,0 +1,209 @@
{{/*
## Overview
This helper returns options dictionary used to configure ESBuild.
The following configurations are set:
* Enable JS minification.
* Enable source map if not building for production.
* Prepare `process.env.<ENVIRONMENT VARIABLE>` defines based on enabled features.
This allows ESBuild to optimize JS bundle size by removing code related
to unused features.
* Added `process-shim.js` so `process.env` is defined.
This way we don't have to explicitly specify every environment
variable via `defines` value.
* Prepare `params` for feature and service configs used in JS.
For more details on ESBuild configuration, see: https://gohugo.io/hugo-pipes/js/
## Detailed Concepts
### `feature` and `service`
Features configured in site wide `config.yml` file under `params.features` section.
A **feature** provides a certain functionality to the user via one or more services.
A feature be can enabled or disabled.
A **service** is a 3rd party service, or JS library that implements a feature.
For example, `analytics` is considered a feature.
There are many services that can provide this feature, to name a few:
* Google Analytics
* Counter.Dev
* GoatCounter
To maximize extendibility and therefore usefulness as an open source project,
it is important to define a standard interface that is easy to understand,
configure, and extend.
In this file, I took the liberty of standardizing this interface under `params.features`.
Please note that this breaks compatibility with previous versions of `config.yaml`.
I will provide sample `config.yaml` files with fully documented features, as well as
documentation on migrating the `config.yaml` file to the new standard.
Here is a sample config file for the new `params.features` section. Notice that each `service`
is a dictionary with `enable` and `services` key. `services` is a dictionary with each key
corresponding to a concrete service, and value as configuration / settings. In the case of
services that need no configuration, an empty dictionary is created.
```yml
params:
features:
# This is the `analytics` feature
analytics:
# This feature is enabled
enable: true
# List of services to enable
services:
# Google Analytics is enabled
google:
# settings for Google Analytics
id: G-xxxxx
# # Counter Dev is disabled
# counterDev:
# id: foo
# name: bar
# The `darkMode` feature
darkmode:
enable: true
services:
# darkmode is realized by using DarkReader library
darkreader:
# options are 'system', 'dark', 'light'
defaultColorScheme: system
# For all available options, see `interface DynamicThemeFix`:
# https://github.com/darkreader/darkreader/blob/main/index.d.ts#L125
fixes:
invert: ['img[src$=".svg"]'] # inverts svg colors.
# For all available options, see `interface Theme` in:
# https://github.com/darkreader/darkreader/blob/main/index.d.ts#L45
theme:
brightness: 100
contrast: 100
sepia: 0
```
This helper will convert the above config into the following env vars:
* `FEATURE_ANALYTICS=1`
* `FEATURE_ANALYTICS_GOOGLE=1`
* `FEATURE_DARKMODE=1`
* `FEATURE_DARKMODE_DARKREADER=1`
In JS, you can use it like this:
```js
import * as params from '@params';
if (process.env.FEATURE_ANALYTICS) {
// Do things to enable this feature here
if (process.env.FEATURE_ANALYTICS_GOOGLE) {
// Do things things to enable google analytics
}
}
```
You can also access service configs via params:
```js
import * as params from '@params';
console.log(params);
```
You will see console output like below. Note, each service configuration is
namespaced by their corresponding feature.
```json
{
"analytics": {
"google": {
"id": "G-xxxxx"
}
},
"darkmode": {
"darkreader": {
"defaultColorScheme": "system",
"fixes": {
"invert": "['img[src$=\".svg\"]']"
},
"theme": {
"brightness": 100,
"contrast": 100,
"sepia": 0
}
}
}
}
```
*/}}
{{/* Holds all the feature flag environment variables for `process.env.*` in JS land */}}
{{ $defines := dict }}
{{/* Holds all the feature configuration variables exposed to JS side */}}
{{ $params := dict }}
{{/* set NODE_ENV depending on if we are building for production use. */}}
{{ $defines = $defines | merge (dict
"process.env.NODE_ENV" (cond hugo.IsProduction `"production"` `"development"` )
)}}
{{/* Go through each feature defined in our config.yml/config.toml/config.json file. */}}
{{ range $feature, $featureDef := site.Params.features }}
{{/* Initialize a dictionary that will hold all service configs for this specific feature */}}
{{ $featureParams := dict }}
{{ with $featureDef }}
{{/* convert feature name to uppercase and remove '_', e.g. `darkMode` becomes `DARKMODE` */}}
{{ $featureName := replace $feature "_" "" | upper }}
{{/* The feature is enabled if the `enable` key is absent, or is set to `true` */}}
{{ $featureEnabled := or (not (isset . "enable")) .enable }}
{{/* Sets `FEATURE_<FEATURE_NAME>` env var to "1" or "0" depending on if the feature is enabled. */}}
{{ $defines = $defines | merge (dict
(printf "process.env.FEATURE_%s" $featureName) (cond $featureEnabled `'1'` `'0'`)
) }}
{{ if $featureEnabled }}
{{/* Loop through each service under this feature */}}
{{ range $service, $config := .services }}
{{/*
We assume all services are enabled. To disable a service,
simply comment it out from `config.yaml`.
*/}}
{{/* Convert name to all uppercase, removing underscore */}}
{{ $serviceName := replace $service "_" "" | upper }}
{{/* let JS side know this service is enabled */}}
{{ $defines = $defines | merge (dict
(printf "process.env.FEATURE_%s_%s" $featureName $serviceName) `'1'`
) }}
{{/* add service configuration to feature params */}}
{{ $featureParams = $featureParams | merge (dict $service $config) }}
{{ end }}
{{/* add feature params to top level params */}}
{{ $params = $params | merge (dict $feature $featureParams) }}
{{ end }}
{{ end }}
{{ end }}
{{
return dict
"defines" $defines
"params" $params
"minify" true
"sourceMap" (cond hugo.IsProduction "" "inline")
"inject" "scripts/process-shim.js"
}}

View file

@ -0,0 +1,5 @@
{{- $options := partial "helpers/get-esbuild-options.html" -}}
{{- $options = $options | merge (dict "targetPath" "application.js") -}}
{{- $app := resources.Get "scripts/application.js" -}}
{{- $bundle := $app | js.Build $options | fingerprint -}}
<script src="{{ $bundle.RelPermalink }}" integrity="{{ $bundle.Data.Integrity }}" defer></script>

View file

@ -1,15 +1 @@
<link rel="stylesheet" href="{{ "/katex/katex.min.css" | relURL }}">
<script type="text/javascript" defer src="{{ "/katex/katex.min.js" | relURL }}"></script>
<script type="text/javascript" defer src="{{ "/katex/auto-render.min.js" | relURL }}" onload="renderMathInElement(document.body);">
renderMathInElement(
document.body,
{
delimiters: [
{left: "$$", right: "$$", display: true},
{left: "\\[", right: "\\]", display: true},
{left: "$", right: "$", display: false},
{left: "\\(", right: "\\)", display: false}
]
}
);
</script>

View file

@ -1,6 +0,0 @@
<script src="{{ "/js/mermaid-8.14.0.min.js" | relURL }}"></script>
<script>
mermaid.initialize({
startOnLoad:true
});
</script>

View file

@ -1,5 +1,6 @@
{{ with site.Params.features.support }}
{{ if .enabled }}
{{ if .enable }}
{{ with .services }}
<!-- Enable Ko-Fi floating button -->
{{ with .kofi }}
<script src='https://storage.ko-fi.com/cdn/scripts/overlay-widget.js'></script>
@ -16,5 +17,6 @@
{{ with .buymeacoffee }}
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="{{ .user }}" data-description="{{ .text }}" data-message="{{ .info }}" data-color="{{ .color }}" data-position="Right" data-x_margin="10" data-y_margin="18"></script>
{{ end }}
{{ end }}
{{ end }}
{{ end }}

View file

@ -5,7 +5,7 @@
<div class="dropdown languageSelector">
<a class="btn dropdown-toggle" href="#" id="languageSelector" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ if ne site.Params.showFlags false }}
{{ if ne site.Params.features.flags.enable false }}
{{ $countryCode := partial "helpers/country-code.html" . }}
<span class="flag-icon flag-icon-{{$countryCode}}"></span>
{{ end }}
@ -14,7 +14,7 @@
<div class="dropdown-menu" aria-labelledby="languageSelector">
{{ range .Translations }}
<a class="dropdown-item nav-link languages-item" href="{{ path.Join "/" (cond (eq .Language.Lang "en") "" .Language.Lang) $pageURL }}">
{{ if ne site.Params.showFlags false }}
{{ if ne site.Params.features.flags.enable false }}
{{ $countryCode := partial "helpers/country-code.html" . }}
<span class="flag-icon flag-icon-{{$countryCode}}"></span>
{{ end }}

View file

@ -5,7 +5,7 @@
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="languageSelector" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ if ne site.Params.showFlags false }}
{{ if ne site.Params.features.flags.enable false }}
{{ $countryCode := partial "helpers/country-code.html" . }}
<span class="flag-icon flag-icon-{{$countryCode}}"></span>
{{ end }}
@ -14,7 +14,7 @@
<div class="dropdown-menu" aria-labelledby="languageSelector">
{{ range .Translations }}
<a class="dropdown-item nav-link languages-item" href="{{ path.Join "/" (cond (eq .Language.Lang "en") "" .Language.Lang) $pageURL }}">
{{ if ne site.Params.showFlags false }}
{{ if ne site.Params.features.flags.enable false }}
{{ $countryCode := partial "helpers/country-code.html" . }}
<span class="flag-icon flag-icon-{{$countryCode}}"></span>
{{ end }}

View file

@ -1,6 +1,6 @@
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="languageSelector" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ if ne site.Params.showFlags false }}
{{ if ne site.Params.features.flags.enable false }}
{{ $countryCode := partial "helpers/country-code.html" . }}
<span class="flag-icon flag-icon-{{$countryCode}}"></span>
{{ end }}
@ -9,7 +9,7 @@
<div class="dropdown-menu" aria-labelledby="languageSelector">
{{ range site.Home.AllTranslations }}
<a class="dropdown-item nav-link languages-item" href="{{ .RelPermalink }}">
{{ if ne site.Params.showFlags false }}
{{ if ne site.Params.features.flags.enable false }}
{{ $countryCode := partial "helpers/country-code.html" . }}
<span class="flag-icon flag-icon-{{$countryCode}}"></span>
{{ end }}

View file

@ -29,7 +29,7 @@
<nav class="navbar navbar-expand-xl top-navbar final-navbar shadow">
<div class="container">
<button class="navbar-toggler navbar-light" id="sidebar-toggler" type="button" onclick="toggleSidebar()">
<button class="navbar-toggler navbar-light" id="sidebar-toggler" type="button">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="{{ site.BaseURL | relLangURL }}">
@ -38,7 +38,7 @@
{{ end }}
{{- site.Title -}}
</a>
<button class="navbar-toggler navbar-light" id="toc-toggler" type="button" onclick="toggleTOC()">
<button class="navbar-toggler navbar-light" id="toc-toggler" type="button">
<span class="navbar-toggler-icon"></span>
</button>
@ -47,7 +47,7 @@
{{ if .IsTranslated }}
{{ partial "navigators/lang-selector-2.html" . }}
{{ end }}
{{ if site.Params.darkMode.enable }}
{{ if site.Params.features.darkMode.enable }}
{{ partial "navigators/theme-selector.html" . }}
{{ end }}
</ul>

View file

@ -124,7 +124,7 @@
{{ if .IsTranslated }}
{{ partial "navigators/lang-selector.html" . }}
{{ end }}
{{ if site.Params.darkMode.enable }}
{{ if site.Params.features.darkMode.enable }}
{{ partial "navigators/theme-selector.html" . }}
{{ end }}
</ul>

View file

@ -1,20 +1,17 @@
<li class="nav-item dropdown">
<!-- This is for initializing the color scheme selection for new visitors. See /js/darkmode.js -->
<div id="theme-initialization" style="display: none;"
default-theme="{{ site.Params.darkMode.default }}"></div>
<a class="nav-link dropdown-toggle" href="#" id="themeSelector" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img id="navbar-theme-icon-svg" src="{{ "/icons/moon-svgrepo-com.svg" }}" width=20 alt="Dark Theme">
</a>
<div class="dropdown-menu dropdown-menu-icons-only" aria-labelledby="themeSelector">
<a class="dropdown-item nav-link" href="#" onclick="enableLightTheme()">
<div id="themeMenu" class="dropdown-menu dropdown-menu-icons-only" aria-labelledby="themeSelector">
<a class="dropdown-item nav-link" href="#" data-scheme="light">
<img class="menu-icon-center" src="{{ "/icons/sun-svgrepo-com.svg" }}" width=20 alt="Light Theme">
</a>
<a class="dropdown-item nav-link" href="#" onclick="enableDarkTheme()">
<a class="dropdown-item nav-link" href="#" data-scheme="dark">
<img class="menu-icon-center" src="{{ "/icons/moon-svgrepo-com.svg" }}" width=20 alt="Dark Theme">
</a>
<a class="dropdown-item nav-link" href="#" onclick="useSystemTheme()">
<a class="dropdown-item nav-link" href="#" data-scheme="system">
<img class="menu-icon-center" src="{{ "/icons/computer-svgrepo-com.svg" }}" width=20 alt="System Theme">
</a>
</div>
</li>
</li>

View file

@ -1,14 +1 @@
<script type="text/javascript" src="{{ "/js/jquery-3.4.1.min.js" | relURL }}"></script>
<script type="text/javascript" src="{{ "/js/popper.min.js" | relURL }}"></script>
<script type="text/javascript" src="{{ "/js/bootstrap.min.js" | relURL }}"></script>
<script type="text/javascript" src="{{ "/js/navbar.js" | relURL }}"></script>
<script type="text/javascript" src="{{ "/js/plyr.js" | relURL }}"></script>
<script type="text/javascript" src="{{ "/js/main.js" | relURL }}"></script>
{{ if site.Params.darkMode.enable }}
{{ if eq site.Params.darkMode.provider "darkreader" }}
<script type="text/javascript" src="{{ "/js/darkreader.js" | relURL }}"></script>
<script type="text/javascript" src="{{ "/js/darkmode-darkreader.js" | relURL }}"></script>
{{ end }}
{{ end }}
{{ partial "helpers/script-bundle.html" }}

View file

@ -14,7 +14,8 @@
class="achievement-entry text-center"
style="background-image: url('{{ $achievementImageSm }}');"
>
<i class="fas fa-search-plus" id="enlarge-icon"></i>
<i class="fa-solid fa-xmark hidden"></i>
<i class="fa-solid fa-magnifying-glass-plus" id="enlarge-icon"></i>
<h4 class="title" id="achievement-title">{{ .title }}</h4>
<div class="caption hidden col-lg-6 text-left" id="caption">
<h4>{{ .title }}</h4>

View file

@ -90,9 +90,9 @@
{{ end }}
{{ if gt (len .takenCourses.courses) $collapseAfter }}
<button type="button" class="btn btn-link show-more-btn pt-0 {{ if .takenCourses.showGrades }}ml-1{{ else }}ml-2{{ end }}"
onclick="toggleCourseVisibility(this);" id="show-more-btn" aria-label="{{ i18n "show_more"}}">{{ i18n "show_more"}}</button>
id="show-more-btn" aria-label="{{ i18n "show_more"}}">{{ i18n "show_more"}}</button>
<button type="button" class="btn btn-link show-more-btn hidden pt-0 {{ if .takenCourses.showGrades }}ml-1{{ else }}ml-2{{ end }}"
onclick="toggleCourseVisibility(this);" id="show-less-btn" aria-label="{{ i18n "show_less"}}">{{ i18n "show_less"}}</button>
id="show-less-btn" aria-label="{{ i18n "show_less"}}">{{ i18n "show_less"}}</button>
{{ end }}
</div>
{{ end }}

View file

@ -90,9 +90,9 @@
{{ end }}
{{ if gt (len .takenCourses.courses ) $collapseAfter }}
<button type="button" class="btn btn-link show-more-btn pt-0 {{ if .takenCourses.showGrades }}ml-1{{ else }}ml-2{{ end }}"
onclick="toggleCourseVisibility(this);" id="show-more-btn">{{ i18n "show_more"}}</button>
id="show-more-btn">{{ i18n "show_more"}}</button>
<button type="button" class="btn btn-link show-more-btn hidden pt-0 {{ if .takenCourses.showGrades }}ml-1{{ else }}ml-2{{ end }}"
onclick="toggleCourseVisibility(this);" id="show-less-btn">{{ i18n "show_less"}}</button>
id="show-less-btn">{{ i18n "show_less"}}</button>
{{ end }}
</div>
{{ end }}