Add Russian beside Polish for the web app, the mobile editor, memo templates, and AI prompt seeds. Untranslated strings stay in English. iOS, the browser extension, and the marketing site are unchanged.
105 lines
7.5 KiB
JavaScript
105 lines
7.5 KiB
JavaScript
import { readWebStartupGraph } from "./web-startup-graph.mjs";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const distDirectory = join(process.cwd(), "apps", "web", "dist");
|
|
const indexHtml = readFileSync(join(distDirectory, "index.html"), "utf8");
|
|
const serviceWorker = readFileSync(join(distDirectory, "sw.js"), "utf8");
|
|
const staticHeaders = readFileSync(join(distDirectory, "_headers"), "utf8");
|
|
assert.match(serviceWorker, /edgeever-resource-blobs/, "PWA must provide a runtime cache for resource bytes");
|
|
assert.match(serviceWorker, /CacheFirst/, "PWA resource bytes must use a cache-first runtime strategy");
|
|
assert.match(serviceWorker, /edgeever-app-shell/, "PWA navigation must use a dedicated app-shell runtime cache");
|
|
assert.match(serviceWorker, /edgeever-optional-pdf/, "PWA must cache the optional PDF runtime after first use");
|
|
assert.match(serviceWorker, /edgeever-optional-locales/, "PWA must cache the Japanese catalog after first use");
|
|
assert.match(serviceWorker, /NetworkFirst/, "PWA navigation must prefer the current deployment over cached HTML");
|
|
assert.doesNotMatch(serviceWorker, /PrecacheFallbackPlugin/, "PWA navigation must not fall back to an install-time HTML shell");
|
|
assert.doesNotMatch(serviceWorker, /NavigationRoute/, "PWA navigation must not always serve a cached HTML shell");
|
|
assert.match(staticHeaders, /\/sw\.js\s+Cache-Control: no-cache, no-store, must-revalidate/, "Service worker updates must not use stale browser cache");
|
|
assert.match(staticHeaders, /\/assets\/\*\s+Cache-Control: public, max-age=31536000, immutable/, "Fingerprinted assets must use immutable browser caching");
|
|
const precacheStart = serviceWorker.indexOf("precacheAndRoute(");
|
|
assert.ok(precacheStart >= 0, "Web service worker must still register a precache route so outdated caches can be cleaned up");
|
|
const precacheCall = serviceWorker.slice(precacheStart, serviceWorker.indexOf(");", precacheStart) + 2);
|
|
const precacheURLs = [...precacheCall.matchAll(/\{url:"([^"]+)"/g)].map((match) => match[1]);
|
|
assert.deepEqual(
|
|
precacheURLs,
|
|
["manifest.webmanifest"],
|
|
`Install-time PWA precache may only keep the web manifest, found: ${precacheURLs.join(", ")}`,
|
|
);
|
|
assert.doesNotMatch(serviceWorker, /edgeever-offline-shell/, "PWA must not retain a versioned offline HTML shell");
|
|
const modulePreloads = indexHtml.match(/<link rel="modulepreload"[^>]+>/g)?.join("\n") ?? "";
|
|
const initialOptionalPattern = /vendor-code-highlight|vendor-D3|beautiful-mermaid|vendor-(?:mermaid|tiptap|prosemirror|floating|codemirror|x6|infographic|streamdown)|vendor~(?:wasm|emacs-lisp)-|ui-primitives|mermaid\.core|[^"']*Diagram(?:EditorPane)?-/;
|
|
assert.doesNotMatch(modulePreloads, initialOptionalPattern, "Optional editor and diagram chunks must remain out of the initial HTML modulepreload list");
|
|
assert.doesNotMatch(modulePreloads, /mathlive-loader|i18n-ja-/, "MathLive and the Japanese catalog must remain deferred");
|
|
assert.doesNotMatch(modulePreloads, /ui-button-tooltip/, "Button tooltips must load only when a titled button is rendered");
|
|
assert.doesNotMatch(modulePreloads, /vendor-radix(?!-slot)/, "Radix overlays must remain out of the initial HTML modulepreload list");
|
|
const initialModulePreloadBytes = [...indexHtml.matchAll(/<link rel="modulepreload"[^>]+href="([^"]+)"[^>]*>/g)]
|
|
.map((match) => statSync(join(distDirectory, match[1].replace(/^\//, ""))).size)
|
|
.reduce((total, size) => total + size, 0);
|
|
// The current Web entry preloads ~765 KiB; retain a bounded budget with a
|
|
// small growth allowance. Desktop builds do not emit modulepreload links.
|
|
const INITIAL_MODULE_PRELOAD_BUDGET = 775 * 1024;
|
|
assert.ok(initialModulePreloadBytes <= INITIAL_MODULE_PRELOAD_BUDGET, `Initial modulepreload budget exceeded: ${initialModulePreloadBytes} > ${INITIAL_MODULE_PRELOAD_BUDGET}`);
|
|
|
|
const startupGraph = readWebStartupGraph(distDirectory);
|
|
assert.ok(startupGraph.initialJavaScriptBytes <= 1024 * 1024, `Eager JavaScript exceeds 1 MiB: ${startupGraph.initialJavaScriptBytes}`);
|
|
const eagerOptionalPattern = /(?:^|\/)(?:vendor-(?:code-highlight|beautiful-mermaid|mermaid|tiptap|prosemirror|codemirror|x6|infographic|streamdown)|i18n-(?:ja|pl|ru)-|(?:WorkspaceApp|EditorPane|DiagramEditorPane|PosterEditorPane|TableEditorPane|InfographicEditorPane)-)/;
|
|
assert.ok(!startupGraph.initialJavaScriptFiles.some(file => eagerOptionalPattern.test(file)), "Optional editors and locales must remain outside the actual startup module graph");
|
|
|
|
// Optional fonts must remain downloads, even when the desktop installer packages all Web files.
|
|
const bundledFontBytes = readdirSync(join(distDirectory, "assets"))
|
|
.filter(name => /\.(?:woff2?|ttf|otf)$/.test(name))
|
|
.reduce((total, name) => total + statSync(join(distDirectory, "assets", name)).size, 0);
|
|
// Includes shared defaults plus math/rendering dependencies (currently about 6.7 MiB).
|
|
assert.ok(bundledFontBytes <= 8 * 1024 * 1024, `Bundled fonts exceed 8 MiB: ${bundledFontBytes}`);
|
|
assert.ok(!readdirSync(distDirectory).includes("poster-fonts"), "Optional poster fonts must not be copied into clients");
|
|
|
|
const DEFAULT_CHUNK_WARNING_BYTES = 500 * 1024;
|
|
// Shiki, loaded with the infographic conversation, keeps the Oniguruma wasm
|
|
// engine and the Emacs Lisp grammar above 500 KiB. AntV Infographic stays one
|
|
// atomic deferred chunk so its registries initialize in order. Streamdown
|
|
// keeps the unified/micromark parser graph atomic for the same reason; it
|
|
// loads with the note editor. All of these stay off the initial modulepreload list.
|
|
const allowedLargeChunkPattern = /^(?:vendor-(?:code-highlight|beautiful-mermaid|mermaid-(?:layout|render)|codemirror|x6|infographic|streamdown)|vendor~(?:wasm|emacs-lisp)-|.*Diagram-).*\.js$/;
|
|
// MathLive is loaded only when opening the formula dialog; Japanese is an
|
|
// optional locale catalog. Give these atomic chunks explicit size ceilings
|
|
// while retaining the initial preload budget and the checks below.
|
|
const boundedDeferredChunks = [
|
|
{ pattern: /^vendor~mathlive-loader-.*\.js$/, maxBytes: 850 * 1024 },
|
|
// Notebook publishing, sharing and font feedback bring the deferred catalog
|
|
// to ~563 KiB. Keep a bounded allowance without changing the preload budget.
|
|
{ pattern: /^i18n-ja-.*\.js$/, maxBytes: 570 * 1024 },
|
|
];
|
|
const allowedLargeChunk = ({ name, size }) =>
|
|
allowedLargeChunkPattern.test(name) ||
|
|
boundedDeferredChunks.some(({ pattern, maxBytes }) => pattern.test(name) && size <= maxBytes);
|
|
const largeChunks = readdirSync(join(distDirectory, "assets"))
|
|
.filter((name) => name.endsWith(".js"))
|
|
.map((name) => ({ name, size: statSync(join(distDirectory, "assets", name)).size }))
|
|
.filter(({ size }) => size > DEFAULT_CHUNK_WARNING_BYTES);
|
|
assert.ok(
|
|
largeChunks.every(allowedLargeChunk),
|
|
`Unexpected JavaScript chunks exceed 500 KiB: ${largeChunks
|
|
.filter((chunk) => !allowedLargeChunk(chunk))
|
|
.map(({ name, size }) => `${name} (${size})`)
|
|
.join(", ")}`,
|
|
);
|
|
assert.ok(
|
|
largeChunks.every(({ name }) => !modulePreloads.includes(name)),
|
|
"Large optional chunks must not be module-preloaded by the app entry",
|
|
);
|
|
|
|
console.log(JSON.stringify({
|
|
ok: true,
|
|
bundledFontBytes,
|
|
initialJavaScriptBytes: startupGraph.initialJavaScriptBytes,
|
|
initialJavaScriptFiles: startupGraph.initialJavaScriptFiles,
|
|
precacheEntries: precacheURLs.length,
|
|
initialModulePreloadBytes,
|
|
initialModulePreloadBudget: INITIAL_MODULE_PRELOAD_BUDGET,
|
|
largeDeferredChunks: largeChunks,
|
|
resourceBytesCache: "cache-first",
|
|
optionalDiagramChunksDeferred: true,
|
|
optionalInitialChunksDeferred: true,
|
|
}));
|