-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1695 from weather-gov/mgwalker/bundling
Modify our Javascript to enable bundling
- Loading branch information
Showing
31 changed files
with
885 additions
and
354 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
runs: | ||
using: composite | ||
steps: | ||
- name: install dependencies | ||
shell: bash | ||
run: | | ||
cd web/themes/new_weather_theme | ||
npm ci | ||
- name: bundle | ||
uses: actions/github-script@v7 | ||
with: | ||
script: | | ||
const { chdir } = require("node:process"); | ||
chdir("web/themes/new_weather_theme"); | ||
const fs = require("node:fs/promises"); | ||
const esbuild = require("esbuild"); | ||
const yaml = require("js-yaml"); | ||
const libs = yaml.load(await fs.readFile("new_weather_theme.libraries.yml")); | ||
const targets = Object.entries(libs).filter(([key, config]) => { | ||
if(key.endsWith("-page")) { | ||
const sources = Object.keys(config.js); | ||
if(sources.length === 1 && sources[0].startsWith('assets/js')) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
}).map(([, {js}])=>Object.keys(js)[0]); | ||
for await(const target of targets) { | ||
await esbuild.build({ | ||
entryPoints: [target], | ||
bundle: true, | ||
minify: true, | ||
sourcemap: true, | ||
outfile: target, | ||
allowOverwrite: true, | ||
}); | ||
} | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# Javascript bundling | ||
|
||
We use esbuild to bundle our custom Javascript into single files for each kind | ||
of page. For example, a point location page would be served `point.js` which | ||
has been bundled to include all of the code necessary to display that page | ||
propertly. This allows us to minimize the number of network calls required to | ||
load the page as well as giving us control over minification. | ||
|
||
We use ESM modules for our custom code. This helps create clean scope boundaries | ||
so we don't have to worry about polluting the global scope. It also helps us | ||
manage import order, since esbuild can just figure it out for us. | ||
|
||
## Drupal libraries | ||
|
||
We define what Javascript to deploy in the Drupal theme's libraries file at | ||
`web/themes/new_weather_theme/new_weather_theme.libraries.yml`. For custom code, | ||
there should be one entry per page type. The same code may be bundled in multiple | ||
page types, but we only want to deliver a single bundled file per page type. | ||
|
||
In order for an entrypoint script to be enabled for bundling, its top-level key | ||
must end in `-page`. For example: | ||
|
||
```yaml | ||
front-page: | ||
version: 2 | ||
js: | ||
assets/js/front.page.js: | ||
attributes: | ||
type: module | ||
defer: true | ||
|
||
cmi: | ||
version: 3 | ||
js: | ||
https://cmi: | ||
attributes: | ||
defer: true | ||
data-wx-radar-cmi: true | ||
``` | ||
This will cause the `front-page` library to be built into a bundle. There must | ||
be only **_ONE_** Javascript file here, and it is the entrypoint into the | ||
module. In its current form, our entrypoints just `import` the sub-components | ||
that are relevant for each page type. | ||
|
||
> [!CAUTION] | ||
> If there are any changes in any of the files that get bundled into a page's | ||
> library, the version **_MUST_** be updated. If the version is not updated, | ||
> users may use an older, cached version of the library. | ||
|
||
## Development | ||
|
||
Currently, we are not using the bundler to import npm modules, only local | ||
modules or wholly-contained ESM modules from CDNs. As a result, we do not need | ||
to run the bundler in order for our scripts to work. In development, we do not | ||
run the bundler so we can get more useful stack traces and debugging. | ||
|
||
## Testing and deployment | ||
|
||
For testing and deployment, we use esbuild to bundle each of the page scripts. | ||
The entrypoint scripts are replaced by their bundles. This reduces the number of | ||
network calls necessary to load the scripts and reduces the data transfer | ||
requirements. We use the bundles for testing in CI/CD to better reflect our | ||
deployed environment. | ||
|
||
For a deployment, we bundle the scripts before deploying them. As a result, the | ||
entrypoint scripts we ship to the cloud are single self-contained files. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
import("./components/combo-box.js"); |
150 changes: 69 additions & 81 deletions
150
web/themes/new_weather_theme/assets/js/charts/hourly-pops.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,98 +1,86 @@ | ||
/* global Chart ChartDataLabels */ | ||
(() => { | ||
const styles = getComputedStyle(document.body); | ||
|
||
const fontMono = styles.getPropertyValue("--font-family-mono"); | ||
const colors = { | ||
base: styles.getPropertyValue("--color-base"), | ||
baseLighter: styles.getPropertyValue("--color-base-lighter"), | ||
baseLightest: styles.getPropertyValue("--color-base-lightest"), | ||
primary: styles.getPropertyValue("--color-primary"), | ||
primaryDark: styles.getPropertyValue("--color-primary-dark"), | ||
primaryLight: styles.getPropertyValue("--color-primary-light"), | ||
cyan50: styles.getPropertyValue("--color-cyan-50"), | ||
}; | ||
import styles from "../styles.js"; | ||
|
||
Chart.register(ChartDataLabels); | ||
Chart.register(ChartDataLabels); | ||
|
||
// These are applied globally to all charts. Unclear if that's okay, or if | ||
// what we really want is to set them per-chart, but this is what I've got | ||
// for now. | ||
Chart.defaults.font.family = fontMono; | ||
Chart.defaults.font.size = 12; | ||
// These are applied globally to all charts. Unclear if that's okay, or if | ||
// what we really want is to set them per-chart, but this is what I've got | ||
// for now. | ||
Chart.defaults.font.family = styles.font.mono; | ||
Chart.defaults.font.size = 12; | ||
|
||
const chartContainers = Array.from( | ||
document.querySelectorAll(".wx-hourly-pops-chart-container"), | ||
const chartContainers = Array.from( | ||
document.querySelectorAll(".wx-hourly-pops-chart-container"), | ||
); | ||
|
||
for (const container of chartContainers) { | ||
const times = JSON.parse(container.dataset.times); | ||
const pops = JSON.parse(container.dataset.pops).map((v) => | ||
Number.parseInt(v, 10), | ||
); | ||
|
||
for (const container of chartContainers) { | ||
const times = JSON.parse(container.dataset.times); | ||
const pops = JSON.parse(container.dataset.pops).map((v) => | ||
Number.parseInt(v, 10), | ||
); | ||
// We don't need to keep a reference to the chart object. We only need the | ||
// side-effects of creating it. This is not ideal, but it's how Chart.js | ||
// works, so it's what we've got. | ||
// eslint-disable-next-line no-new | ||
new Chart(container.querySelector("canvas"), { | ||
type: "bar", | ||
plugins: [ChartDataLabels], | ||
|
||
// We don't need to keep a reference to the chart object. We only need the | ||
// side-effects of creating it. This is not ideal, but it's how Chart.js | ||
// works, so it's what we've got. | ||
// eslint-disable-next-line no-new | ||
new Chart(container.querySelector("canvas"), { | ||
type: "bar", | ||
plugins: [ChartDataLabels], | ||
options: { | ||
animation: false, | ||
responsive: true, | ||
maintainAspectRatio: false, | ||
interaction: { | ||
intersect: false, | ||
mode: "index", | ||
}, | ||
|
||
options: { | ||
animation: false, | ||
responsive: true, | ||
maintainAspectRatio: false, | ||
interaction: { | ||
intersect: false, | ||
mode: "index", | ||
plugins: { | ||
legend: { | ||
display: false, | ||
}, | ||
|
||
plugins: { | ||
legend: { | ||
display: false, | ||
}, | ||
tooltip: { | ||
xAlign: "center", | ||
yAlign: "bottom", | ||
}, | ||
tooltip: { | ||
xAlign: "center", | ||
yAlign: "bottom", | ||
}, | ||
scales: { | ||
x: { | ||
ticks: { | ||
maxRotation: 0, | ||
color: colors.base, | ||
}, | ||
grid: { display: false }, | ||
}, | ||
scales: { | ||
x: { | ||
ticks: { | ||
maxRotation: 0, | ||
color: styles.colors.base, | ||
}, | ||
y: { | ||
min: 0, | ||
max: 100, | ||
ticks: { | ||
autoSkip: true, | ||
color: colors.base, | ||
maxTicksLimit: 6, | ||
callback: (v) => `${v}%`, | ||
}, | ||
grid: { display: false }, | ||
}, | ||
y: { | ||
min: 0, | ||
max: 100, | ||
ticks: { | ||
autoSkip: true, | ||
color: styles.colors.base, | ||
maxTicksLimit: 6, | ||
callback: (v) => `${v}%`, | ||
}, | ||
}, | ||
}, | ||
}, | ||
|
||
data: { | ||
labels: times.map((v) => (Number.parseInt(v, 10) % 2 === 0 ? v : "")), | ||
datasets: [ | ||
{ | ||
label: "Chance of precipitation", | ||
data: pops, | ||
datalabels: { | ||
align: "end", | ||
anchor: "end", | ||
color: colors.cyan50, | ||
}, | ||
backgroundColor: colors.cyan50, | ||
data: { | ||
labels: times.map((v) => (Number.parseInt(v, 10) % 2 === 0 ? v : "")), | ||
datasets: [ | ||
{ | ||
label: "Chance of precipitation", | ||
data: pops, | ||
datalabels: { | ||
align: "end", | ||
anchor: "end", | ||
color: styles.colors.cyan50, | ||
}, | ||
], | ||
}, | ||
}); | ||
} | ||
})(); | ||
backgroundColor: styles.colors.cyan50, | ||
}, | ||
], | ||
}, | ||
}); | ||
} |
Oops, something went wrong.