Skip to content

Commit

Permalink
Merge pull request #1695 from weather-gov/mgwalker/bundling
Browse files Browse the repository at this point in the history
Modify our Javascript to enable bundling
  • Loading branch information
greg-does-weather authored Sep 10, 2024
2 parents ff7dbd6 + 7b0da32 commit 13c770b
Show file tree
Hide file tree
Showing 31 changed files with 885 additions and 354 deletions.
41 changes: 41 additions & 0 deletions .github/actions/javascript-bundle/action.yml
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,
});
}
3 changes: 3 additions & 0 deletions .github/actions/setup-site/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ runs:
chmod a+rwx -R .coverage
docker load --input /tmp/image.tar
docker compose up -d
- name: build JS bundle
uses: ./.github/actions/javascript-bundle

# Give the containers a moment to settle.
- name: wait a tick
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/deploy-sandbox.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD
steps:
- uses: actions/checkout@v4
- name: bundle javascript
uses: ./.github/actions/javascript-bundle
- name: Deploy application in ${{ github.event.inputs.environment }} space
uses: cloud-gov/cg-cli-tools@main
with:
Expand Down
10 changes: 10 additions & 0 deletions docs/dev/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,13 @@ complete `launch.json`:
]
}
```

Our container will also happily let you step into Drupal source code, if you
have it locally on your machine and setup path mappings for it. We recommend
cloning the Drupal source and checking out the appropriate tag that matches the
version we're using. Then, add these as additional items to the `pathMappings`
property above:

```json
"/opt/drupal/web/core": "/{path/to/drupal/source}/core"
```
67 changes: 67 additions & 0 deletions docs/dev/javascript-bundling.md
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.
1 change: 1 addition & 0 deletions web/themes/new_weather_theme/assets/js/afd.page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import("./components/combo-box.js");
150 changes: 69 additions & 81 deletions web/themes/new_weather_theme/assets/js/charts/hourly-pops.js
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,
},
],
},
});
}
Loading

0 comments on commit 13c770b

Please sign in to comment.