547 lines
21 KiB
TypeScript
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,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|