Cache npm dependencies in Buildkite

- Add twemoji as npm dependency instead of build download
- Package build assets with a Vite plugin instead of scripts
- Keep the eslint and prettier caches in one directory
This commit is contained in:
Daniel Norman
2026-08-26 17:44:04 +02:00
parent 51edb93b7d
commit 8f2ddcf489
18 changed files with 213 additions and 103 deletions
-57
View File
@@ -1,57 +0,0 @@
# Buildkite CI
The Garden Buildkite integration stores a bare `buildkite-agent pipeline
upload`, so the agent finds `pipeline.yml` by its default name. The integration
makes one pipeline per repo, so all suites land in the same build.
## Cache volumes and artifacts
A **cache volume** carries state between builds. It is an input: content that a
step needs, but that the repo does not contain. Volume names are scoped to the
pipeline.
An **artifact** is a file that one build produces. It is an output: evidence for
a person to read in the Buildkite UI. The agent uploads artifacts whatever the
exit code of the step, so a failed step still reports its results.
The visual suite uses both. The volume `visual-snapshots` holds the baseline
images, which are gitignored and exist nowhere else. `artifact_paths` uploads
the actual, expected, and diff images that a failed comparison writes. The two
cannot be swapped: a volume is not visible in the UI, and an artifact does not
persist as an input to the next build.
## One writer per volume
A job does not read its volume in place. It gets a private copy at start. That
copy becomes the version that the next job sees, but only when the job exits 0.
Every successful job is therefore a writer, whether or not it changed a file.
Two consequences follow.
Keep the volume names distinct across the uploaded files. A step that commits a
version discards what a concurrent step wrote to the same volume. This is why
`rust.yml` gives test, doc, and lint a separate volume each.
The visual suite keeps exactly one writer. `master` updates the baselines, and
`concurrency: 1` serializes the group. Other branches always exit non-zero, so
Buildkite discards their copy and no branch can republish a stale baseline set.
The branch step then cannot use the exit code to report the test result, so two
codes carry both: 100 means no differences, which `soft_fail` keeps green, and
any other non-zero code fails the build. An annotation reports the outcome in
both cases.
## Docker images
Two images cover all Node steps:
- `node:24-bookworm` tracks the major version in `.nvmrc` and the `engines`
field in `package.json`.
- `mcr.microsoft.com/playwright:v1.61.1-noble` ships the browsers at
`/ms-playwright`, so no step installs them. The tag must match
`@playwright/test` in `package.json`. Playwright refuses browsers built for
another version.
Node steps use no cache volume. Cache paths are relative to the step working
directory, and the agent home is not visible inside a container, so npm
downloads its dependencies on every run. The Rust steps run on the agent
instead, so their `~`-rooted paths work.
+17
View File
@@ -1,14 +1,27 @@
# No explicit build step: tests/support/globalSetup.ts runs `npm run build` in # No explicit build step: tests/support/globalSetup.ts runs `npm run build` in
# parallel with fixture creation, and asserts the pinned rad and radicle-httpd # parallel with fixture creation, and asserts the pinned rad and radicle-httpd
# versions are on PATH. # versions are on PATH.
#
# This step shares the `deps` volume with the steps in node-checks.yml. Keep the
# cache paths the same as the ones there, and see that file for how the volume
# works and why this step waits for check.
env: env:
CI: "true" CI: "true"
NPM_CONFIG_CACHE: ".npm-cache"
steps: steps:
- label: "🎭 e2e" - label: "🎭 e2e"
key: e2e key: e2e
# The check step in node-checks.yml seeds the shared volume.
depends_on: check
timeout_in_minutes: 30 timeout_in_minutes: 30
cache:
name: deps
paths:
- ".npm-cache"
- "tests/tmp/bin"
- ".lintcache"
artifact_paths: artifact_paths:
- "tests/artifacts/**/*" - "tests/artifacts/**/*"
plugins: plugins:
@@ -19,8 +32,12 @@ steps:
image: "mcr.microsoft.com/playwright:v1.61.1-noble" image: "mcr.microsoft.com/playwright:v1.61.1-noble"
# Chromium exhausts Docker's default 64MB /dev/shm. # Chromium exhausts Docker's default 64MB /dev/shm.
ipc: host ipc: host
# Without this the cache volume links point at nothing.
volumes:
- "/cache/bkcache:/cache/bkcache"
environment: environment:
- CI - CI
- NPM_CONFIG_CACHE
commands: commands:
- npm ci - npm ci
# The image ships no decompressors: install-binaries unpacks .tar.xz # The image ships no decompressors: install-binaries unpacks .tar.xz
+50 -3
View File
@@ -1,17 +1,38 @@
# No cache volume: cache paths are relative to the step working directory, and # One `deps` cache volume serves every step here and the e2e step in e2e.yml.
# the agent's home is not visible inside a container, so npm downloads its # The volume is mounted at /cache/bkcache on the agent, and the configured paths
# dependencies on every run. # become links into it. A container step must therefore mount that directory, or
# every link dangles and the job persists nothing.
#
# A job does not write the volume in place. It gets a private copy at start, and
# that copy becomes the version the next job sees if the job exits 0. Concurrent
# jobs are never merged: the last one to exit 0 publishes its whole copy, and
# whatever a job that finished earlier committed is gone.
#
# check therefore runs first, and is the only step that writes content no other
# step produces: the lint caches, and the pinned binaries. The four steps that
# follow all copy its version and add nothing unique to it — `npm ci` against the
# same lockfile fetches the same tarballs, and install-binaries does nothing once
# the pinned version is there. Whichever of them commits last keeps everything.
#
# Stale content is safe. The npm cache is content-addressed, and install-binaries
# keys its download on the pinned version.
env: env:
CI: "true" CI: "true"
# Applies to every npm and npx call in the step, not just to `npm ci`.
NPM_CONFIG_CACHE: ".npm-cache"
__defs: __defs:
# Tracks the major version in .nvmrc and the engines field in package.json. # Tracks the major version in .nvmrc and the engines field in package.json.
node_image: &node_image node_image: &node_image
docker#v5.14.0: docker#v5.14.0:
image: "node:24-bookworm" image: "node:24-bookworm"
# Without this the cache volume links point at nothing.
volumes:
- "/cache/bkcache:/cache/bkcache"
environment: environment:
- CI - CI
- NPM_CONFIG_CACHE
# Ships the browsers at /ms-playwright, which is why no step installs them. # Ships the browsers at /ms-playwright, which is why no step installs them.
# The tag must match @playwright/test in package.json — Playwright refuses to # The tag must match @playwright/test in package.json — Playwright refuses to
@@ -21,22 +42,44 @@ __defs:
image: "mcr.microsoft.com/playwright:v1.61.1-noble" image: "mcr.microsoft.com/playwright:v1.61.1-noble"
# Chromium exhausts Docker's default 64MB /dev/shm. # Chromium exhausts Docker's default 64MB /dev/shm.
ipc: host ipc: host
volumes:
- "/cache/bkcache:/cache/bkcache"
environment: environment:
- CI - CI
- NPM_CONFIG_CACHE
# Keep this list the same in every step that attaches the volume, here and in
# e2e.yml.
deps_cache: &deps_cache
name: deps
paths:
- ".npm-cache"
- "tests/tmp/bin"
# The eslint and prettier caches, which scripts/check keeps here rather
# than under node_modules so that `npm ci` cannot delete them. A cache
# path is always a directory, so the two files share one.
- ".lintcache"
steps: steps:
- label: "🔎 check" - label: "🔎 check"
key: check key: check
timeout_in_minutes: 10 timeout_in_minutes: 10
cache: *deps_cache
plugins: plugins:
- *node_image - *node_image
commands: commands:
- npm ci - npm ci
# check does not use the binaries. It installs them while it is the only
# writer of the volume, so that no later step has to win a race to keep
# them. The node image ships curl and xz, so nothing is installed first.
- ./scripts/install-binaries
- npm run check - npm run check
- label: "🧪 unit-tests" - label: "🧪 unit-tests"
key: unit-tests key: unit-tests
depends_on: check
timeout_in_minutes: 10 timeout_in_minutes: 10
cache: *deps_cache
plugins: plugins:
- *node_image - *node_image
commands: commands:
@@ -47,7 +90,9 @@ steps:
# binaries on PATH even though it runs under vitest rather than Playwright. # binaries on PATH even though it runs under vitest rather than Playwright.
- label: "🔌 http-client-tests" - label: "🔌 http-client-tests"
key: http-client-tests key: http-client-tests
depends_on: check
timeout_in_minutes: 15 timeout_in_minutes: 15
cache: *deps_cache
artifact_paths: artifact_paths:
- "tests/artifacts/**/*" - "tests/artifacts/**/*"
plugins: plugins:
@@ -62,7 +107,9 @@ steps:
# needs a browser but no Radicle binaries or fixtures. # needs a browser but no Radicle binaries or fixtures.
- label: "🏗 build-smoke" - label: "🏗 build-smoke"
key: build-smoke key: build-smoke
depends_on: check
timeout_in_minutes: 30 timeout_in_minutes: 30
cache: *deps_cache
artifact_paths: artifact_paths:
- "tests/artifacts/**/*" - "tests/artifacts/**/*"
plugins: plugins:
+7
View File
@@ -6,6 +6,13 @@
# suite lands in this single build. Keep cache volume names distinct across the # suite lands in this single build. Keep cache volume names distinct across the
# uploaded files: volumes are scoped to the pipeline, and a step that commits a # uploaded files: volumes are scoped to the pipeline, and a step that commits a
# volume version discards what a concurrent step wrote to the same volume. # volume version discards what a concurrent step wrote to the same volume.
#
# Upload node-checks.yml before e2e.yml: the e2e step depends on the `check` key
# defined there, and Buildkite rejects a step whose dependency is not yet part of
# the build.
#
# Superseded builds are not cancelled. Buildkite has no YAML equivalent of the
# GitHub Actions cancel-in-progress.
steps: steps:
- label: ":pipeline: upload" - label: ":pipeline: upload"
+15 -2
View File
@@ -22,11 +22,21 @@ __defs:
if ! command -v rustup >/dev/null 2>&1; then if ! command -v rustup >/dev/null 2>&1; then
curl -fsSL https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal curl -fsSL https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal
fi fi
rustup component add rustfmt clippy
rustc --version rustc --version
# Only the parts of ~/.cargo that are expensive to fetch again. registry/src
# and git/checkouts are left out on purpose: cargo re-creates both from the
# cache and the git db, so caching them would double the size of the volume
# for no gain.
#
# Nothing prunes the target directory, so it keeps the artifacts of every
# dependency version the branch has ever built. Deleting the volume is the
# only cleanup, and it costs one full rebuild.
cargo_cache: &cargo_cache cargo_cache: &cargo_cache
- "~/.cargo" - "~/.cargo/bin"
- "~/.cargo/registry/index"
- "~/.cargo/registry/cache"
- "~/.cargo/git/db"
- "~/.rustup" - "~/.rustup"
- "~/.cache/cargo-target" - "~/.cache/cargo-target"
@@ -62,5 +72,8 @@ steps:
paths: *cargo_cache paths: *cargo_cache
commands: commands:
- *install_rust - *install_rust
# The minimal profile ships neither, and rust-toolchain names no
# components. Only this step needs them.
- rustup component add rustfmt clippy
- RUSTFLAGS="-D warnings" cargo clippy --workspace --all-features --tests - RUSTFLAGS="-D warnings" cargo clippy --workspace --all-features --tests
- cargo fmt --all -- --check - cargo fmt --all -- --check
+19 -5
View File
@@ -4,6 +4,10 @@
# volume. Master updates them with --update-snapshots; every other branch # volume. Master updates them with --update-snapshots; every other branch
# compares against them. # compares against them.
# #
# A step can attach only one volume, so these two steps cannot also read the
# `deps` volume from node-checks.yml. This volume carries the npm cache and the
# Radicle binaries as well as the baselines.
#
# A job does not read the volume in place: it gets a private copy at start, and # A job does not read the volume in place: it gets a private copy at start, and
# that copy becomes the version the next job sees if the job exits 0. So every # that copy becomes the version the next job sees if the job exits 0. So every
# successful job is a writer, whether or not it changed a file. The two steps # successful job is a writer, whether or not it changed a file. The two steps
@@ -16,9 +20,15 @@
# --update-snapshots the mode is `missing`, under which Playwright still # --update-snapshots the mode is `missing`, under which Playwright still
# writes a baseline that does not exist yet. A branch therefore needs no # writes a baseline that does not exist yet. A branch therefore needs no
# concurrency limit, and any number of branch builds can run at once. # concurrency limit, and any number of branch builds can run at once.
#
# The cost of that trick is that a branch build never adds to this volume, and
# so never warms the npm cache it shares with the baselines. A branch that
# changes package-lock.json therefore downloads the full dependency set on every
# build until it lands on master.
env: env:
CI: "true" CI: "true"
NPM_CONFIG_CACHE: ".npm-cache"
steps: steps:
- label: "📸 visual update snapshots" - label: "📸 visual update snapshots"
@@ -29,10 +39,12 @@ steps:
# jobs were created, so the newest master build commits last. # jobs were created, so the newest master build commits last.
concurrency: 1 concurrency: 1
concurrency_group: "radicle-explorer/visual-snapshots" concurrency_group: "radicle-explorer/visual-snapshots"
cache: cache: &visual_cache
name: visual-snapshots name: visual-snapshots
paths: paths:
- "tests/visual/snapshots" - "tests/visual/snapshots"
- ".npm-cache"
- "tests/tmp/bin"
artifact_paths: artifact_paths:
- "tests/artifacts/**/*.png" - "tests/artifacts/**/*.png"
plugins: plugins:
@@ -47,8 +59,13 @@ steps:
ipc: host ipc: host
# buildkite-agent annotate runs inside the container. # buildkite-agent annotate runs inside the container.
mount-buildkite-agent: true mount-buildkite-agent: true
# Without this the cache volume links point at nothing, which is why
# the baselines never persisted before.
volumes:
- "/cache/bkcache:/cache/bkcache"
environment: environment:
- CI - CI
- NPM_CONFIG_CACHE
commands: commands:
- npm ci - npm ci
# The image ships no decompressors: install-binaries unpacks .tar.xz # The image ships no decompressors: install-binaries unpacks .tar.xz
@@ -78,10 +95,7 @@ steps:
# produces, so only the success path below is soft. # produces, so only the success path below is soft.
soft_fail: soft_fail:
- exit_status: 100 - exit_status: 100
cache: cache: *visual_cache
name: visual-snapshots
paths:
- "tests/visual/snapshots"
artifact_paths: artifact_paths:
- "tests/artifacts/**/*.png" - "tests/artifacts/**/*.png"
plugins: plugins:
+4 -2
View File
@@ -1,5 +1,7 @@
/build/ /build/
node_modules/ node_modules/
.npm-cache/
.lintcache/
NOTES NOTES
config/local* config/local*
crates/radicle-httpd/build/artifacts crates/radicle-httpd/build/artifacts
@@ -12,8 +14,8 @@ KaTeX_**.ttf
KaTeX_**.woff KaTeX_**.woff
KaTeX_**.woff2 KaTeX_**.woff2
# Twemoji Assets # Twemoji Assets, copied in by vite-plugin-assets
public/twemoji/*.svg public/twemoji/
# Integration Tests # Integration Tests
tests/tmp/**/* tests/tmp/**/*
+2 -2
View File
@@ -27,8 +27,8 @@ WORKDIR /work
# Playwright version in package-lock.json so the baked browser matches the # Playwright version in package-lock.json so the baked browser matches the
# @playwright/test that npm ci installs into the volume at runtime. Copying # @playwright/test that npm ci installs into the volume at runtime. Copying
# only the lockfiles keeps this layer cached across source changes. # only the lockfiles keeps this layer cached across source changes.
# --ignore-scripts: the repo's postinstall (copy-katex/twemoji assets) needs # --ignore-scripts: this layer only warms the browser cache, so dependency
# scripts/ which isn't in this layer; those run at runtime instead. # install scripts are not necessary.
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts \ RUN npm ci --ignore-scripts \
&& npx playwright install --with-deps chromium \ && npx playwright install --with-deps chromium \
+1
View File
@@ -161,6 +161,7 @@ export default [
{ {
ignores: [ ignores: [
"node_modules/**/*", "node_modules/**/*",
".npm-cache/**/*",
"build/**/*", "build/**/*",
"public/**/*", "public/**/*",
"radicle-httpd/**/*", "radicle-httpd/**/*",
+1 -11
View File
@@ -81,13 +81,6 @@
packages = { packages = {
default = self.packages.${system}.radicle-explorer; default = self.packages.${system}.radicle-explorer;
twemoji-assets = pkgs.fetchFromGitHub {
owner = "twitter";
repo = "twemoji";
rev = "v14.0.2";
hash = "sha256-YoOnZ5uVukzi/6bLi22Y8U5TpplPzB7ji42l+/ys5xI=";
};
radicle-explorer = pkgs.callPackage ({ radicle-explorer = pkgs.callPackage ({
lib, lib,
buildNpmPackage, buildNpmPackage,
@@ -97,12 +90,9 @@
pname = "radicle-explorer"; pname = "radicle-explorer";
version = (builtins.fromJSON (builtins.readFile ./package.json)).version; version = (builtins.fromJSON (builtins.readFile ./package.json)).version;
src = ./.; src = ./.;
npmDepsHash = "sha256-RC+QQXtvXC48uM0oOAFA0ni5AU/l9m8k1LgrxykSu5M="; npmDepsHash = "sha256-pUyB56EJjuMYU//oMaFNV27Fg+Ga5jD0HQOB/v+gHa8=";
postPatch = '' postPatch = ''
patchShebangs --build ./scripts patchShebangs --build ./scripts
mkdir -p "public/twemoji"
cp -t public/twemoji -r -- ${self.packages.${system}.twemoji-assets}/assets/svg/*
: >scripts/install-twemoji-assets
''; '';
dontConfigure = true; dontConfigure = true;
+7 -1
View File
@@ -7,8 +7,8 @@
"": { "": {
"name": "radicle-explorer", "name": "radicle-explorer",
"version": "1.0.0", "version": "1.0.0",
"hasInstallScript": true,
"dependencies": { "dependencies": {
"@twemoji/svg": "^15.0.0",
"@wooorm/starry-night": "^3.10.0", "@wooorm/starry-night": "^3.10.0",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"baconjs": "^3.0.23", "baconjs": "^3.0.23",
@@ -1183,6 +1183,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@twemoji/svg": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/@twemoji/svg/-/svg-15.0.0.tgz",
"integrity": "sha512-ZSPef2B6nBaYnfgdTbAy4jgW95o7pi2xPGwGCU+WMTxo7J6B1lMPTWwSq/wTuiMq+N0khQ90CcvYp1wFoQpo/w==",
"license": "MIT"
},
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.3", "version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+2 -2
View File
@@ -6,8 +6,7 @@
"start": "vite", "start": "vite",
"start:httpd": "cargo run -p radicle-httpd --release -- --listen 127.0.0.1:8080", "start:httpd": "cargo run -p radicle-httpd --release -- --listen 127.0.0.1:8080",
"serve": "vite preview", "serve": "vite preview",
"build": "vite build && scripts/copy-katex-assets && scripts/install-twemoji-assets", "build": "vite build",
"postinstall": "scripts/copy-katex-assets && scripts/install-twemoji-assets",
"check": "scripts/check", "check": "scripts/check",
"check:radicle-httpd": "cargo clippy --all --tests && cargo fmt --all --check", "check:radicle-httpd": "cargo clippy --all --tests && cargo fmt --all --check",
"format": "npx prettier '**/*.@(ts|js|svelte|json|css|html|yml)' --write", "format": "npx prettier '**/*.@(ts|js|svelte|json|css|html|yml)' --write",
@@ -63,6 +62,7 @@
"wait-on": "^9.0.10" "wait-on": "^9.0.10"
}, },
"dependencies": { "dependencies": {
"@twemoji/svg": "^15.0.0",
"@wooorm/starry-night": "^3.10.0", "@wooorm/starry-night": "^3.10.0",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"baconjs": "^3.0.23", "baconjs": "^3.0.23",
View File
+7 -2
View File
@@ -3,5 +3,10 @@ set -e
npx tsc --noEmit npx tsc --noEmit
npx svelte-check --tsconfig tsconfig.json --config ./vite.config.ts --fail-on-warnings --compiler-warnings options_missing_custom_element:ignore npx svelte-check --tsconfig tsconfig.json --config ./vite.config.ts --fail-on-warnings --compiler-warnings options_missing_custom_element:ignore
npx eslint --cache --cache-location node_modules/.cache/eslint --max-warnings 0 . # Both caches sit outside node_modules, which `npm ci` deletes, and both are
npx prettier "**/*.@(ts|js|svelte|json|css|html|yml)" --ignore-path .gitignore --ignore-path .prettierignore --check --cache # keyed on file contents rather than mtimes, which a fresh checkout changes.
# CI can then carry them between builds. They share one directory because a
# Buildkite cache path is always a directory, never a file.
mkdir -p .lintcache
npx eslint --cache --cache-location .lintcache/eslint --cache-strategy content --max-warnings 0 .
npx prettier "**/*.@(ts|js|svelte|json|css|html|yml)" --ignore-path .gitignore --ignore-path .prettierignore --check --cache --cache-location .lintcache/prettier
-7
View File
@@ -1,7 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
echo "Copying katex assets into bundle directory"
cp -r node_modules/katex/dist/katex.min.css public/katex.min.css
cp -r node_modules/katex/dist/fonts/* public/fonts/
-9
View File
@@ -1,9 +0,0 @@
#!/usr/bin/env bash
set -Eeou pipefail
version="$(node -e 'console.log(require("twemoji/package.json").version)')"
echo "Installing Twemoji SVG assets v${version}"
curl -sSL "https://github.com/twitter/twemoji/archive/refs/tags/v${version}.tar.gz" \
| tar -x -z -C public/twemoji/ --strip-components=3 "twemoji-${version}/assets/svg"
+78
View File
@@ -0,0 +1,78 @@
import type { Plugin } from "vite";
import fs from "node:fs/promises";
import path from "node:path";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
function packageDir(name: string): string {
return path.dirname(require.resolve(`${name}/package.json`));
}
function packageVersion(name: string): string {
const { version } = require(`${name}/package.json`) as { version: string };
return version;
}
// index.html loads /katex.min.css, so the stylesheet and the fonts it
// references must be in the public directory.
async function copyKatexAssets(publicDir: string) {
const dist = path.join(packageDir("katex"), "dist");
await fs.copyFile(
path.join(dist, "katex.min.css"),
path.join(publicDir, "katex.min.css"),
);
await fs.cp(path.join(dist, "fonts"), path.join(publicDir, "fonts"), {
recursive: true,
});
}
// Emoji URLs are built at runtime, so the SVGs cannot be bundled and must be
// in the public directory. The version stamp skips the copy of ~3700 files
// when the assets are already installed. It lives outside the public directory
// because Vite copies that directory into the build as it is. node_modules is
// the right place for it: an install is what changes the package, and it is
// also what discards the stamp.
//
// The stamp is not enough on its own, because it and the assets sit in
// different places and either one can go without the other. The Docker e2e
// setup keeps node_modules in a volume that outlives the working tree, so a
// `git clean` there removes the assets and leaves the stamp behind. Checking
// the target as well stops a stamp that has outlived its assets from producing
// a build with no emoji in it, which nothing else would report.
async function copyTwemojiAssets(root: string, publicDir: string) {
const version = packageVersion("@twemoji/svg");
const target = path.join(publicDir, "twemoji");
const stamp = path.join(root, "node_modules", ".cache", "twemoji-version");
const installed = await fs.readFile(stamp, "utf8").catch(() => undefined);
const present = await fs.stat(target).catch(() => undefined);
if (installed === version && present) {
return;
}
const source = packageDir("@twemoji/svg");
await fs.cp(source, target, {
recursive: true,
// The package also holds a readme, a license and package.json.
filter: entry => entry === source || entry.endsWith(".svg"),
});
await fs.mkdir(path.dirname(stamp), { recursive: true });
await fs.writeFile(stamp, version);
}
export function assets(): Plugin {
return {
name: "assets",
// Vitest loads this config too, but does not need the assets.
apply: () => !process.env.VITEST,
// The dev server reads the public directory right after the config is
// resolved, so the assets have to be in place before that.
async configResolved(config) {
await copyKatexAssets(config.publicDir);
await copyTwemojiAssets(config.root, config.publicDir);
},
};
}
+3
View File
@@ -3,6 +3,8 @@ import path from "node:path";
import { defineConfig } from "vitest/config"; import { defineConfig } from "vitest/config";
import { svelte } from "@sveltejs/vite-plugin-svelte"; import { svelte } from "@sveltejs/vite-plugin-svelte";
import { assets } from "./vite-plugin-assets";
export default defineConfig({ export default defineConfig({
define: { define: {
buildTimeConfig: JSON.stringify(config.util.toObject()), buildTimeConfig: JSON.stringify(config.util.toObject()),
@@ -13,6 +15,7 @@ export default defineConfig({
reporters: "verbose", reporters: "verbose",
}, },
plugins: [ plugins: [
assets(),
svelte({ svelte({
extensions: [".svelte", ".md"], extensions: [".svelte", ".md"],
// Reference: https://github.com/sveltejs/vite-plugin-svelte/issues/270#issuecomment-1033190138 // Reference: https://github.com/sveltejs/vite-plugin-svelte/issues/270#issuecomment-1033190138