@kenjura/ursa 0.88.0 → 0.89.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ # 0.89.0
2
+ 2026-08-20
3
+
4
+ Fixed `generate` dropping every static file that was not an image or an HTML page — fonts, video, audio, PDFs. A site that used any of them worked perfectly in `ursa serve` and shipped broken.
5
+
6
+ `serve` reads static files straight off disk through one list of extensions, while `generate` had two narrower ideas of what to copy: images, and `.html`. Nothing else ever reached the output. Because dev and build disagreed rather than both being wrong, the gap was invisible until deploy — and then invisible again, since a static host that rewrites 404s to `index.html` answers a missing `.mp4` with a page of HTML rather than an error.
7
+
8
+ - **`generate` now copies fonts, audio, video, documents and archives**: `woff`, `woff2`, `ttf`, `eot`, `otf`, `pdf`, `mp3`, `m4a`, `wav`, `flac`, `mp4`, `m4v`, `webm`, `ogv`, `ogg`, `zip`. Images keep their own path, because they also get previews.
9
+ - **One shared list**, in `helper/staticAssets.js`, used by both `generate` and `serve`, so they cannot drift apart again. It is the drift, not either list, that caused this.
10
+ - The static-files progress line now counts HTML and media separately.
11
+
1
12
  # 0.88.0
2
13
  2026-08-19
3
14
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@kenjura/ursa",
3
3
  "author": "Andrew London <andrew@kenjura.com>",
4
4
  "type": "module",
5
- "version": "0.88.0",
5
+ "version": "0.89.1",
6
6
  "description": "static site generator from MD/wikitext/YML",
7
7
  "main": "lib/index.js",
8
8
  "bin": {
@@ -0,0 +1,47 @@
1
+ import {
2
+ isImage,
3
+ isMedia,
4
+ isStaticAsset,
5
+ } from "../staticAssets.js";
6
+
7
+ describe("staticAssets", () => {
8
+ it("treats the image formats as images", () => {
9
+ for (const f of ["a.jpg", "a.jpeg", "a.PNG", "a.gif", "a.webp", "a.svg", "a.ico"]) {
10
+ expect(isImage(f)).toBe(true);
11
+ expect(isMedia(f)).toBe(false);
12
+ }
13
+ });
14
+
15
+ // The regression this module exists for: generate() copied images and HTML
16
+ // and nothing else, so every one of these 404'd in a built site.
17
+ it("treats fonts, audio, video and documents as media", () => {
18
+ for (const f of ["a.woff", "a.woff2", "a.ttf", "a.eot", "a.otf", "a.pdf",
19
+ "a.mp3", "a.m4a", "a.wav", "a.flac", "a.mp4", "a.m4v", "a.webm", "a.ogv", "a.ogg", "a.zip"]) {
20
+ expect(isMedia(f)).toBe(true);
21
+ expect(isImage(f)).toBe(false);
22
+ }
23
+ });
24
+
25
+ it("counts both as static assets", () => {
26
+ expect(isStaticAsset("clip.mp4")).toBe(true);
27
+ expect(isStaticAsset("Herculanum Regular.ttf")).toBe(true);
28
+ expect(isStaticAsset("photo.jpg")).toBe(true);
29
+ });
30
+
31
+ it("leaves the processed formats alone", () => {
32
+ for (const f of ["page.md", "page.mdx", "page.html", "style.css", "script.js", "data.json"]) {
33
+ expect(isStaticAsset(f)).toBe(false);
34
+ }
35
+ });
36
+
37
+ it("matches on the extension, not on a name that merely contains one", () => {
38
+ expect(isStaticAsset("mp4")).toBe(false);
39
+ expect(isStaticAsset("notes-about-mp4-encoding.md")).toBe(false);
40
+ expect(isStaticAsset("my.mp4.md")).toBe(false);
41
+ });
42
+
43
+ it("is case insensitive, as filesystems are not", () => {
44
+ expect(isStaticAsset("CLIP.MP4")).toBe(true);
45
+ expect(isStaticAsset("Font.TTF")).toBe(true);
46
+ });
47
+ });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * What counts as a static asset.
3
+ *
4
+ * This list used to be written out separately in serve.js and in
5
+ * dependencyTracker.js and — fatally — not at all in generate.js, which knew
6
+ * only about *image* extensions. `ursa serve` therefore served fonts, audio and
7
+ * video quite happily while `ursa generate` left them out of the build
8
+ * entirely, so they worked all the way through development and 404'd in
9
+ * production. One list, in one place, used by both.
10
+ */
11
+
12
+ /** Extensions that get preview generation and image transformation. */
13
+ export const IMAGE_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|svg|ico)$/i;
14
+
15
+ /**
16
+ * Everything else copied through untouched: fonts, documents, audio, video.
17
+ * Deliberately not images, which take a different path, and deliberately not
18
+ * .css, .js, .html or the document formats, all of which are processed rather
19
+ * than copied.
20
+ */
21
+ export const MEDIA_EXTENSIONS = /\.(woff2?|ttf|eot|otf|pdf|mp3|m4a|wav|flac|mp4|m4v|webm|ogv|ogg|zip)$/i;
22
+
23
+ /** Any file the build should place in the output as-is. */
24
+ export const STATIC_ASSET_EXTENSIONS = new RegExp(
25
+ `(?:${IMAGE_EXTENSIONS.source})|(?:${MEDIA_EXTENSIONS.source})`,
26
+ 'i'
27
+ );
28
+
29
+ export const isImage = (filename) => IMAGE_EXTENSIONS.test(filename);
30
+ export const isMedia = (filename) => MEDIA_EXTENSIONS.test(filename);
31
+ export const isStaticAsset = (filename) => STATIC_ASSET_EXTENSIONS.test(filename);
@@ -5,6 +5,7 @@ import { filterAsync } from "../helper/filterAsync.js";
5
5
  import { isDirectory } from "../helper/isDirectory.js";
6
6
  import { isFolderHidden, clearConfigCache } from "../helper/folderConfig.js";
7
7
  import { isHiddenOrSystemPath } from "../helper/hiddenPaths.js";
8
+ import { IMAGE_EXTENSIONS, isMedia } from "../helper/staticAssets.js";
8
9
  import {
9
10
  extractMetadata,
10
11
  extractRawMetadata,
@@ -449,7 +450,7 @@ export async function generate({
449
450
  const copiedCssFiles = new Set();
450
451
 
451
452
  // Identify all image files from the filtered source list
452
- const imageExtensions = /\.(jpg|jpeg|png|gif|webp|svg|ico)/;
453
+ const imageExtensions = IMAGE_EXTENSIONS;
453
454
  let allSourceFilenamesThatAreImages = allSourceFilenames.filter(
454
455
  (filename) => filename.match(imageExtensions) && !isHiddenOrSystem(filename)
455
456
  );
@@ -1172,11 +1173,23 @@ export async function generate({
1172
1173
  (filename) => filename.match(/\.html$/) && !isHiddenOrSystem(filename)
1173
1174
  );
1174
1175
 
1175
- const allStaticFiles = allSourceFilenamesThatAreHtml;
1176
+ // Fonts, audio, video, PDFs: copied through untouched. Images are handled
1177
+ // separately above because they also get previews; everything else that is
1178
+ // neither an article nor a stylesheet belongs here. Leaving this out is what
1179
+ // let `ursa serve` and `ursa generate` disagree — serve reads these straight
1180
+ // off disk, so the missing copy step only ever showed up in a built site.
1181
+ const allSourceFilenamesThatAreMedia = allSourceFilenames.filter(
1182
+ (filename) => isMedia(filename) && !isHiddenOrSystem(filename)
1183
+ );
1184
+
1185
+ const allStaticFiles = [...allSourceFilenamesThatAreHtml, ...allSourceFilenamesThatAreMedia];
1176
1186
  const totalStatic = allStaticFiles.length;
1177
1187
  let processedStatic = 0;
1178
1188
  let copiedStatic = 0;
1179
- progress.log(`Processing ${totalStatic} static HTML files...`);
1189
+ progress.log(
1190
+ `Processing ${totalStatic} static files ` +
1191
+ `(${allSourceFilenamesThatAreHtml.length} HTML, ${allSourceFilenamesThatAreMedia.length} media)...`
1192
+ );
1180
1193
  await processBatched(allStaticFiles, async (file) => {
1181
1194
  try {
1182
1195
  processedStatic++;
package/src/serve.js CHANGED
@@ -7,6 +7,7 @@ import fs from "fs";
7
7
  import { promises } from "fs";
8
8
  import { copy as copyDir, outputFile } from "fs-extra";
9
9
  import { processImage } from "./helper/imageProcessor.js";
10
+ import { STATIC_ASSET_EXTENSIONS, IMAGE_EXTENSIONS } from "./helper/staticAssets.js";
10
11
  import { watchModeCache } from "./helper/build/watchCache.js";
11
12
  import { dependencyTracker } from "./helper/dependencyTracker.js";
12
13
  import { bundleMetaTemplateAssets, clearMetaBundleCache } from "./helper/assetBundler.js";
@@ -297,10 +298,9 @@ async function copyCssFile(cssPath, sourceDir, outputDir) {
297
298
  }
298
299
  }
299
300
 
300
- // Static file extensions that should be copied (images, fonts, etc.)
301
- const STATIC_FILE_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf|eot|pdf|mp3|mp4|webm|ogg)$/i;
302
- // Image extensions that get preview processing
303
- const IMAGE_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|svg|ico)$/i;
301
+ // Shared with generate so the two cannot drift apart again — that drift is
302
+ // exactly how fonts and video came to work in dev and 404 in production.
303
+ const STATIC_FILE_EXTENSIONS = STATIC_ASSET_EXTENSIONS;
304
304
 
305
305
  /**
306
306
  * Copy a single static file to the output directory