Files
edgeever-updater[bot] 67cf5297de chore: update EdgeEver from v2.18.0
EdgeEver-Upstream-Commit: 4235f3f2f1f68ad94802612362d1daea6dabb3cb
2026-11-03 09:33:32 +00:00

547 lines
21 KiB
TypeScript

import { automaticPosterThumbnails } from "./poster-thumbnails.ts";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { resolveDeploymentBuildMetadata } from "@edgeever/shared/deployment-metadata";
import { execSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { fileURLToPath, URL } from "node:url";
import { defineConfig, type Plugin } from "vite";
import { VitePWA } from "vite-plugin-pwa";
import { localDevelopmentAuth } from "./local-dev-auth";
import { publicShareMarkdownDev } from "./share-markdown-dev";
import { resolveAppVersion, resolveReleaseTimestamp } from "./build-metadata";
const readPackageVersion = () => {
try {
const packagePath = fileURLToPath(new URL("../../package.json", import.meta.url));
const packageJson = JSON.parse(readFileSync(packagePath, "utf8")) as {
version?: string;
};
return packageJson.version ?? "0.0.0";
} catch {
return "0.0.0";
}
};
const readReleaseSummary = (packageVersion: string) => {
const summaryPath = fileURLToPath(new URL("../../release-summary.json", import.meta.url));
const summary = JSON.parse(readFileSync(summaryPath, "utf8")) as {
version?: unknown;
changes?: unknown;
};
const localizedChanges = summary.changes && typeof summary.changes === "object" && !Array.isArray(summary.changes)
? Object.entries(summary.changes)
: [];
if (
summary.version !== packageVersion ||
localizedChanges.length === 0 ||
!localizedChanges.every(([locale, changes]) =>
/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(locale) &&
Array.isArray(changes) &&
changes.length > 0 &&
changes.every((item) => typeof item === "string" && item.trim())
) ||
!localizedChanges.some(([locale]) => locale.toLowerCase() === "en-us")
) {
throw new Error(`release-summary.json must contain valid localized changes and an en-US fallback for package version ${packageVersion}.`);
}
return summary as { version: string; changes: Record<string, string[]> };
};
const readGitCommit = () => {
try {
return execSync("git rev-parse --short=12 HEAD", { encoding: "utf8" }).trim();
} catch {
return null;
}
};
const readGitDescription = () => {
try {
return execSync('git describe --tags --long --match "v[0-9]*.[0-9]*.[0-9]*" HEAD', { encoding: "utf8" }).trim();
} catch {
return null;
}
};
const readLatestReleaseCommitTimestamp = () => {
try {
const releaseTag = execSync('git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" HEAD', { encoding: "utf8" }).trim();
return execSync(`git log -1 --format=%cI ${releaseTag}`, { encoding: "utf8" }).trim();
} catch {
// Workers Builds may check out the source without fetching tags. The
// package version is updated in the release-preparation commit, so its
// timestamp is the best local fallback for self-hosted builds.
try {
return execSync("git log -1 --format=%cI -- package.json", { encoding: "utf8" }).trim();
} catch {
return "";
}
}
};
const buildId = process.env.WORKERS_CI_COMMIT_SHA?.slice(0, 12)
?? process.env.CF_PAGES_COMMIT_SHA?.slice(0, 12)
?? process.env.GITHUB_SHA?.slice(0, 12)
?? readGitCommit()
?? "local";
const gitDescription = readGitDescription();
const packageVersion = readPackageVersion();
const appVersion = resolveAppVersion(packageVersion, gitDescription);
const releaseSummary = readReleaseSummary(packageVersion);
const OPTIONAL_CHUNK_WARNING_LIMIT_KB = 1_700;
const TARGET_VENDOR_CHUNK_BYTES = 450 * 1024;
const releaseTimestamp = resolveReleaseTimestamp(process.env.EDGE_EVER_RELEASED_AT) || readLatestReleaseCommitTimestamp();
const deploymentMetadata = resolveDeploymentBuildMetadata(process.env);
const isDesktopBuild = process.env.EDGE_EVER_DESKTOP_BUILD === "1";
const developmentServiceWorkerReset: Plugin = {
name: "edgeever-development-service-worker-reset",
apply: "serve",
configureServer(server) {
server.middlewares.use((request, response, next) => {
if (request.url?.split("?")[0] !== "/sw.js") {
next();
return;
}
response.statusCode = 200;
response.setHeader("Content-Type", "application/javascript; charset=utf-8");
response.setHeader("Cache-Control", "no-store");
response.end(`
self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("activate", (event) => {
event.waitUntil((async () => {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)));
await self.clients.claim();
await self.registration.unregister();
const clients = await self.clients.matchAll({ type: "window" });
await Promise.all(clients.map((client) => client.navigate(client.url)));
})());
});
`);
});
},
};
export default defineConfig({
root: "apps/web",
// Poster creation loads Fabric through a lazy module. Prepare it at startup
// so the first poster does not trigger dependency optimization mid-session.
optimizeDeps: {
include: ["fabric"],
},
// Packaged Electron apps load index.html via file://, so root-absolute
// asset URLs resolve to the filesystem root and leave a blank window.
base: isDesktopBuild ? "./" : "/",
define: {
__EDGEEVER_APP_VERSION__: JSON.stringify(appVersion),
__EDGEEVER_BUILD_ID__: JSON.stringify(buildId),
__EDGEEVER_BUILD_LABEL__: JSON.stringify(buildId === "local" ? "local" : buildId.slice(0, 7)),
__EDGEEVER_RELEASED_AT__: JSON.stringify(releaseTimestamp),
__EDGEEVER_RELEASE_SUMMARY__: JSON.stringify(releaseSummary),
__EDGEEVER_DEPLOYMENT_TRIGGER__: JSON.stringify(deploymentMetadata.trigger),
__EDGEEVER_DEPLOYMENT_METHOD__: JSON.stringify(deploymentMetadata.method),
__EDGEEVER_DEVELOPMENT_PROFILE__: JSON.stringify(process.env.EDGE_EVER_DEVELOPMENT_PROFILE ?? ""),
__EDGEEVER_DESKTOP_BUILD__: JSON.stringify(isDesktopBuild),
},
plugins: [
automaticPosterThumbnails(),
tailwindcss(),
localDevelopmentAuth(),
publicShareMarkdownDev(),
developmentServiceWorkerReset,
react(),
VitePWA({
registerType: "autoUpdate",
includeAssets: [],
includeManifestIcons: false,
manifest: {
name: "EdgeEver",
short_name: "EdgeEver",
description: "EdgeEver:基于 Cloudflare 与 Docker 自托管的现代化开源笔记与 AI 个人知识库。",
start_url: "/",
scope: "/",
display: "standalone",
background_color: "#0f172a",
theme_color: "#0f172a",
lang: "zh-CN",
categories: ["productivity", "utilities"],
icons: [
{
src: "/pwa-192x192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "/pwa-512x512.png",
sizes: "512x512",
type: "image/png",
},
{
src: "/maskable-icon-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
workbox: {
// Install-time precache excludes the app shell. The plugin still
// injects the web manifest so the app stays installable; other
// caches fill on first use.
globPatterns: [],
globIgnores: ["**/*"],
cleanupOutdatedCaches: true,
navigateFallback: null,
navigationPreload: true,
runtimeCaching: [
{
urlPattern: ({ request, url }) =>
request.mode === "navigate" &&
// Share HTML includes the note. Leave it out of the shared navigation cache.
!/^\/(?:share|s)\//.test(url.pathname) &&
!["/mobile-edit.html", "/note-print.html", "/tiptap-ime-test.html"].includes(url.pathname),
handler: "NetworkFirst",
options: {
cacheName: "edgeever-app-shell",
networkTimeoutSeconds: 5,
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: ({ url }) => /^\/api\/v1\/resources\/[^/]+\/blob$/.test(url.pathname),
handler: "CacheFirst",
options: {
cacheName: "edgeever-resource-blobs",
cacheableResponse: {
statuses: [0, 200],
},
expiration: {
maxEntries: 500,
maxAgeSeconds: 60 * 60 * 24 * 90,
},
},
},
{
urlPattern: ({ url }) => /\/assets\/i18n-(?:en-US|ja|pl|ru)-/.test(url.pathname),
handler: "CacheFirst",
options: {
cacheName: "edgeever-optional-locales",
expiration: {
maxEntries: 8,
maxAgeSeconds: 60 * 60 * 24 * 30,
},
},
},
{
urlPattern: ({ url }) => /\/assets\/(?:.*beautiful-mermaid|vendor-mermaid|.*mermaid\.core|.*Diagram-|vendor-x6|vendor-codemirror)/.test(url.pathname),
handler: "CacheFirst",
options: {
cacheName: "edgeever-optional-diagrams",
expiration: {
maxEntries: 120,
maxAgeSeconds: 60 * 60 * 24 * 30,
},
},
},
{
urlPattern: ({ url }) => /\/assets\/(?:vendor~pdf-|pdf\.worker\.min-)/.test(url.pathname),
handler: "CacheFirst",
options: {
cacheName: "edgeever-optional-pdf",
expiration: {
maxEntries: 4,
maxAgeSeconds: 30 * 24 * 60 * 60,
},
},
},
{
urlPattern: ({ url }) => url.pathname.startsWith("/api/") || url.pathname.startsWith("/mcp/"),
handler: "NetworkOnly",
},
],
},
}),
],
resolve: {
alias: {
// Its browser export requires document; the default table also works in Workers.
"decode-named-character-reference": fileURLToPath(import.meta.resolve("decode-named-character-reference")),
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
server: {
proxy: {
"/api": {
target: "http://127.0.0.1:8787",
changeOrigin: true,
},
"/mcp": {
target: "http://127.0.0.1:8787",
changeOrigin: true,
},
},
},
build: {
outDir: "dist",
emptyOutDir: true,
// ELK is distributed as one ~1.6 MiB module by beautiful-mermaid. It is
// loaded only when a diagram is rendered and is excluded from HTML
// modulepreload; verify-web-performance.mjs enforces those constraints
// for every chunk above Vite's default 500 KiB limit.
chunkSizeWarningLimit: OPTIONAL_CHUNK_WARNING_LIMIT_KB,
modulePreload: isDesktopBuild
? false
: {
resolveDependencies: (_filename, dependencies) => dependencies.filter((dependency) =>
!/(?:vendor-code-highlight|vendor-(?:mermaid|D3|tiptap|prosemirror|floating|codemirror|x6|infographic|zod|streamdown)|vendor~(?:wasm|emacs-lisp)-|vendor-radix(?!-slot)|ui-primitives|ui-button-tooltip|i18n-(?:en-US|ja|pl|ru)-)/.test(dependency),
),
},
rolldownOptions: {
input: {
app: fileURLToPath(new URL("./index.html", import.meta.url)),
"mobile-edit": fileURLToPath(new URL("./mobile-edit.html", import.meta.url)),
"note-print": fileURLToPath(new URL("./note-print.html", import.meta.url)),
"tiptap-ime-test": fileURLToPath(new URL("./tiptap-ime-test.html", import.meta.url)),
...(isDesktopBuild
? {
"desktop-renderer-test": fileURLToPath(
new URL("./desktop-renderer-test.html", import.meta.url)
),
}
: {}),
},
output: {
// Preserve initialization order when manual vendor groups form cycles.
strictExecutionOrder: true,
codeSplitting: {
// Groups own only matching modules. Recursive ownership lets optional
// editors capture shared utilities (clsx, tslib, React hooks), making
// a basic button or locale load the whole editor at startup.
includeDependenciesRecursively: false,
groups: [
{
// Font coverage tables are static data used by image sharing.
// Keep them out of the editor component's JavaScript chunk.
name: "note-image-font-manifest",
test: /[\\/]note-image-font-manifest\.ts$/,
priority: 51,
},
{
name: "vendor-code-highlight",
test: /node_modules[\\/](?:lowlight|highlight\.js)[\\/]/,
priority: 50,
// lowlight registers highlight.js languages through a cyclic
// module graph. Splitting this group by size can evaluate a
// language before its constructor is initialized in packaged
// file:// desktop builds, leaving the entire window blank. Keep
// that graph atomic, but leave TipTap's lightweight adapter in
// the regular extension group so plain mobile code blocks do not
// inherit the highlighter as a startup dependency.
},
{
name: "i18n-en-US",
test: /[\\/]i18n[\\/](?:resources[\\/])?en-US\.ts$/,
priority: 41,
},
{
name: "i18n-ja",
test: /[\\/]i18n[\\/](?:resources[\\/])?ja\.ts$/,
priority: 41,
},
{
name: "i18n-pl",
test: /[\\/]i18n[\\/](?:resources[\\/])?pl\.ts$/,
priority: 41,
},
{
name: "i18n-ru",
test: /[\\/]i18n[\\/](?:resources[\\/])?ru\.ts$/,
priority: 41,
},
{
name: "vendor-react",
test: /node_modules[\\/](react|react-dom|scheduler|react-router)[\\/]/,
priority: 40,
},
{
name: "vendor-x6",
test: /node_modules[\\/]@antv[\\/]x6[\\/]/,
priority: 39,
// X6 initializes registries and plugins through a cyclic module
// graph. Size-based splitting can evaluate clipboard storage
// before Config is initialized, crashing the lazy diagram editor.
// Keep the graph atomic and defer the resulting chunk instead.
},
{
name: "vendor-infographic",
test: /node_modules[\\/]@antv[\\/]infographic[\\/]/,
priority: 39,
// AntV Infographic registers its template and shape catalogs through
// internal registries. Size-based splitting across chunks breaks
// initialization order, causing registry map lookups (e.g. .set)
// to fail on undefined during chunk evaluation. Keep this graph
// atomic and leave the resulting chunk off the initial modulepreload.
},
{
name: "vendor-prosemirror",
test: /node_modules[\\/](prosemirror-|orderedmap|rope-sequence)[\\/]/,
priority: 38,
},
{
name: "vendor-codemirror",
test: /[\\/]node_modules[\\/](?:@codemirror|@lezer|@uiw[\\/](?:react-)?codemirror|@uiw[\\/]codemirror-themes|codemirror)[\\/]/,
priority: 37,
},
{
name: "vendor-streamdown",
test: /node_modules[\\/](?:streamdown|@streamdown|micromark|mdast|unist|remark|rehype|vfile|zwitch|longest-streak|character-entities|property-information|space-separated-tokens|comma-separated-tokens|html-void-elements|ccount|devlop|bail|trough|unified)[\\/]/,
priority: 35,
// Streamdown and the unified/micromark/mdast parser stack rely on
// tight cross-module references and top-level initializer functions
// (e.g. unist-util-is convert() called at import time by mdast-util-phrasing).
// Splitting these modules across chunks via maxSize or entriesAware causes
// circular chunk evaluation order issues where convert() is undefined.
// Keep the entire Streamdown parsing graph atomic.
},
{
name: "vendor-tiptap-pm",
test: /node_modules[\\/]@tiptap[\\/]pm[\\/]/,
priority: 36,
},
{
name: "vendor-tiptap-core",
test: /node_modules[\\/]@tiptap[\\/]core[\\/]/,
priority: 34,
},
{
name: "vendor-tiptap-react",
test: /node_modules[\\/]@tiptap[\\/]react[\\/]/,
priority: 32,
},
{
name: "vendor-tiptap-extensions",
test: /node_modules[\\/](?:@tiptap[\\/](?:extension-|extensions)|tiptap-)[\\/]/,
priority: 30,
},
{
name: "vendor-tiptap-starter",
test: /node_modules[\\/]@tiptap[\\/]starter-kit[\\/]/,
priority: 29,
},
{
name: "vendor-linkify",
test: /node_modules[\\/]linkifyjs[\\/]/,
priority: 29,
},
{
name: "vendor-floating",
test: /node_modules[\\/](@floating-ui|tippy\.js)[\\/]/,
priority: 28,
entriesAware: true,
},
{
name: "vendor-query",
test: /node_modules[\\/]@tanstack[\\/](?:react-query|query-core)[\\/]/,
priority: 25,
},
{
name: "vendor-zod",
test: /node_modules[\\/]zod[\\/]/,
priority: 22,
},
{
name: "vendor-i18n",
test: /node_modules[\\/](i18next|react-i18next)[\\/]/,
priority: 21,
},
{
name: "vendor-storage",
test: /node_modules[\\/](dexie|workbox-window)[\\/]/,
priority: 20,
},
{
name: "vendor-icons",
test: /node_modules[\\/]lucide-react[\\/]/,
priority: 18,
entriesAware: true,
},
{
name: "vendor-radix-slot",
test: /node_modules[\\/]@radix-ui[\\/](?:react-slot|react-compose-refs)[\\/]/,
priority: 17,
// Button needs Slot in the app shell. Keep this tiny primitive
// separate from overlays that are loaded with lazy screens.
},
{
name: "vendor-radix",
test: /node_modules[\\/](@radix-ui|cmdk|vaul)[\\/]/,
priority: 15,
entriesAware: true,
},
{
name: "vendor-ui-utils",
test: /node_modules[\\/](class-variance-authority|clsx|tailwind-merge)[\\/]/,
priority: 12,
},
{
name: "vendor-mermaid-d3",
test: /[\\/](?:d3(?:-[^\\/@]+)?|internmap|delaunator|robust-predicates)(?:@|[\\/])/,
priority: 11,
},
{
name: "vendor-mermaid-layout",
test: /[\\/](?:cytoscape(?:-[^\\/@]+)?|dagre-d3-es|graphlib|roughjs|khroma|@upsetjs[\\/]venn\.js)(?:@|[\\/])/,
priority: 11,
maxSize: TARGET_VENDOR_CHUNK_BYTES,
},
{
name: "vendor-mermaid-render",
test: /[\\/](?:@mermaid-js[\\/](?:parser|tiny)|katex|dompurify|stylis|dayjs|@iconify[\\/]utils)(?:@|[\\/])/,
priority: 11,
maxSize: TARGET_VENDOR_CHUNK_BYTES,
},
{
name: "vendor-beautiful-mermaid",
test: /[\\/](?:beautiful-mermaid|elkjs|entities)(?:@|[\\/])/,
priority: 13,
maxSize: TARGET_VENDOR_CHUNK_BYTES,
},
{
name: "ui-button",
test: /src[\\/]components[\\/]ui[\\/]button\.tsx$/,
priority: 12,
},
{
name: "ui-button-tooltip",
test: /src[\\/]components[\\/]ui[\\/]button-tooltip\.tsx$/,
priority: 12,
},
{
// Shell tooltips must not pull every lazy screen's UI into startup.
name: "ui-primitives",
test: /src[\\/]components[\\/]ui[\\/]/,
priority: 10,
entriesAware: true,
},
{
name: "vendor",
// Keep Mermaid's internally lazy-loaded diagram modules out of the
// catch-all vendor chunk so they remain on-demand. Entry-aware
// splitting also prevents dependencies used only by lazy settings,
// export, and template screens from leaking into the app entry.
test: /^(?!.*(?:[\\/]mermaid@|node_modules[\\/]mermaid[\\/])).*node_modules[\\/]/,
priority: 5,
entriesAware: true,
maxSize: TARGET_VENDOR_CHUNK_BYTES,
},
],
},
},
},
},
});