mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(docker): fix TDZ crash, icon bundle bloat, rate-limit on static assets
- Fix "Cannot access 'a' before initialization" TDZ error after login caused by manualChunks splitting react-vendor + lucide icons into circular ES-module chunks. Removed manualChunks entirely. - Replace `import * as icons from "lucide-react"` (pulls all ~1000 icons) with a targeted icon-map of ~50 icons actually used by tool definitions. Reduces shared icons chunk from 745KB to 62KB (132KB→16KB gzip). - Exclude static files from @fastify/rate-limit via allowList so rapid page navigations don't 429 on JS/CSS chunk requests. - Move Docker auth defaults (AUTH_ENABLED, DEFAULT_USERNAME, DEFAULT_PASSWORD) from Dockerfile ENV to entrypoint.sh runtime exports to avoid SecretsUsedInArgOrEnv warnings. - Fix Docker CMD to use pnpm --filter for workspace-scoped tsx binary. - Set COREPACK_HOME system-wide so non-root user can access pnpm cache. - Lazy-load all pages in App.tsx and all controls in pipeline-step-settings.tsx to keep main bundle under 300KB.
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { TOOLS } from "@ashim/shared";
|
||||
import * as icons from "lucide-react";
|
||||
import { ArrowRight, ChevronDown, ChevronRight, Download, Undo2 } from "lucide-react";
|
||||
import { ArrowRight, ChevronDown, ChevronRight, Download, FileImage, Undo2 } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { formatFileSize, triggerDownload } from "@/lib/download";
|
||||
import { ICON_MAP } from "@/lib/icon-map";
|
||||
import { getSuggestedTools } from "@/lib/suggested-tools";
|
||||
|
||||
interface ReviewPanelProps {
|
||||
@@ -124,12 +124,8 @@ export function ReviewPanel({
|
||||
<div className="space-y-1">
|
||||
{suggestedTools.map((tool) => {
|
||||
const ToolIcon =
|
||||
(
|
||||
icons as unknown as Record<
|
||||
string,
|
||||
React.ComponentType<{ className?: string }>
|
||||
>
|
||||
)[tool.icon] || icons.FileImage;
|
||||
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ??
|
||||
FileImage;
|
||||
return (
|
||||
<button
|
||||
key={tool.id}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Tool } from "@ashim/shared";
|
||||
import * as icons from "lucide-react";
|
||||
import { FileImage, Star } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ICON_MAP } from "@/lib/icon-map";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ToolCardProps {
|
||||
@@ -9,8 +9,8 @@ interface ToolCardProps {
|
||||
}
|
||||
|
||||
export function ToolCard({ tool }: ToolCardProps) {
|
||||
const iconsMap = icons as unknown as Record<string, React.ComponentType<{ className?: string }>>;
|
||||
const IconComponent = iconsMap[tool.icon] || FileImage;
|
||||
const IconComponent =
|
||||
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ?? FileImage;
|
||||
|
||||
return (
|
||||
<div className="group flex items-center gap-3 relative">
|
||||
|
||||
@@ -15,11 +15,11 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import * as icons from "lucide-react";
|
||||
import { GripVertical, Plus, X } from "lucide-react";
|
||||
import { FileImage, GripVertical, Plus, X } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { SearchBar } from "@/components/common/search-bar";
|
||||
import { apiGet } from "@/lib/api";
|
||||
import { ICON_MAP } from "@/lib/icon-map";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { PipelineStep } from "@/stores/pipeline-store";
|
||||
import { PipelineStepSettings } from "./pipeline-step-settings";
|
||||
@@ -30,8 +30,6 @@ const PIPELINE_TOOLS_BASE = TOOLS.filter(
|
||||
(t) => !["pipeline", "batch", "compare", "find-duplicates", "collage", "compose"].includes(t.id),
|
||||
);
|
||||
|
||||
const iconsMap = icons as unknown as Record<string, React.ComponentType<{ className?: string }>>;
|
||||
|
||||
interface PipelineBuilderProps {
|
||||
steps: PipelineStep[];
|
||||
expandedStepId: string | null;
|
||||
@@ -75,7 +73,7 @@ function SortableStep({
|
||||
const tool = TOOLS.find((t) => t.id === step.toolId);
|
||||
if (!tool) return null;
|
||||
|
||||
const Icon = iconsMap[tool.icon] || icons.FileImage;
|
||||
const Icon = (ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ?? FileImage;
|
||||
const summary = getSettingsSummary(step.toolId, step.settings);
|
||||
|
||||
return (
|
||||
@@ -264,7 +262,8 @@ export function PipelineBuilder({
|
||||
<p className="text-sm text-muted-foreground text-center py-4">No tools found</p>
|
||||
) : (
|
||||
PIPELINE_TOOLS.map((tool) => {
|
||||
const Icon = iconsMap[tool.icon] || icons.FileImage;
|
||||
const Icon =
|
||||
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ?? FileImage;
|
||||
return (
|
||||
<button
|
||||
key={tool.id}
|
||||
|
||||
@@ -1,24 +1,62 @@
|
||||
import { BlurFacesControls } from "./blur-faces-settings";
|
||||
import { BorderControls } from "./border-settings";
|
||||
import { ColorControls } from "./color-settings";
|
||||
import { CompressControls } from "./compress-settings";
|
||||
import { ConvertControls } from "./convert-settings";
|
||||
import { CropControls } from "./crop-settings";
|
||||
import { EnhanceFacesControls } from "./enhance-faces-settings";
|
||||
import { GifToolsControls } from "./gif-tools-settings";
|
||||
import { NoiseRemovalControls } from "./noise-removal-settings";
|
||||
import { RemoveBgControls } from "./remove-bg-settings";
|
||||
import { ReplaceColorControls } from "./replace-color-settings";
|
||||
import { ResizeControls } from "./resize-settings";
|
||||
import { RotateControls } from "./rotate-settings";
|
||||
import { SmartCropControls } from "./smart-crop-settings";
|
||||
import { StripMetadataControls } from "./strip-metadata-settings";
|
||||
import { TextOverlayControls } from "./text-overlay-settings";
|
||||
import { UpscaleControls } from "./upscale-settings";
|
||||
import { WatermarkTextControls } from "./watermark-text-settings";
|
||||
import { lazy, Suspense } from "react";
|
||||
|
||||
type ControlProps = {
|
||||
settings: Record<string, unknown>;
|
||||
onChange: (settings: Record<string, unknown>) => void;
|
||||
};
|
||||
|
||||
// Lazy-load every control so these modules are not pulled into the main bundle.
|
||||
// The pipeline builder wraps this component in <Suspense> so each control
|
||||
// loads on demand when the user selects a step in the pipeline.
|
||||
const CONTROLS: Record<string, React.LazyExoticComponent<React.FC<ControlProps>>> = {
|
||||
resize: lazy(() => import("./resize-settings").then((m) => ({ default: m.ResizeControls }))),
|
||||
crop: lazy(() => import("./crop-settings").then((m) => ({ default: m.CropControls }))),
|
||||
rotate: lazy(() => import("./rotate-settings").then((m) => ({ default: m.RotateControls }))),
|
||||
convert: lazy(() => import("./convert-settings").then((m) => ({ default: m.ConvertControls }))),
|
||||
compress: lazy(() =>
|
||||
import("./compress-settings").then((m) => ({ default: m.CompressControls })),
|
||||
),
|
||||
"strip-metadata": lazy(() =>
|
||||
import("./strip-metadata-settings").then((m) => ({ default: m.StripMetadataControls })),
|
||||
),
|
||||
border: lazy(() => import("./border-settings").then((m) => ({ default: m.BorderControls }))),
|
||||
"watermark-text": lazy(() =>
|
||||
import("./watermark-text-settings").then((m) => ({ default: m.WatermarkTextControls })),
|
||||
),
|
||||
"text-overlay": lazy(() =>
|
||||
import("./text-overlay-settings").then((m) => ({ default: m.TextOverlayControls })),
|
||||
),
|
||||
"replace-color": lazy(() =>
|
||||
import("./replace-color-settings").then((m) => ({ default: m.ReplaceColorControls })),
|
||||
),
|
||||
"smart-crop": lazy(() =>
|
||||
import("./smart-crop-settings").then((m) => ({ default: m.SmartCropControls })),
|
||||
),
|
||||
"gif-tools": lazy(() =>
|
||||
import("./gif-tools-settings").then((m) => ({ default: m.GifToolsControls })),
|
||||
),
|
||||
upscale: lazy(() => import("./upscale-settings").then((m) => ({ default: m.UpscaleControls }))),
|
||||
"blur-faces": lazy(() =>
|
||||
import("./blur-faces-settings").then((m) => ({ default: m.BlurFacesControls })),
|
||||
),
|
||||
"enhance-faces": lazy(() =>
|
||||
import("./enhance-faces-settings").then((m) => ({ default: m.EnhanceFacesControls })),
|
||||
),
|
||||
"remove-background": lazy(() =>
|
||||
import("./remove-bg-settings").then((m) => ({ default: m.RemoveBgControls })),
|
||||
),
|
||||
"noise-removal": lazy(() =>
|
||||
import("./noise-removal-settings").then((m) => ({ default: m.NoiseRemovalControls })),
|
||||
),
|
||||
};
|
||||
|
||||
const COLOR_TOOL_IDS = new Set(["adjust-colors"]);
|
||||
|
||||
// ColorControls needs an extra toolId prop so it lives outside the shared map.
|
||||
const LazyColorControls = lazy(() =>
|
||||
import("./color-settings").then((m) => ({ default: m.ColorControls })),
|
||||
);
|
||||
|
||||
interface PipelineStepSettingsProps {
|
||||
toolId: string;
|
||||
settings: Record<string, unknown>;
|
||||
@@ -26,32 +64,23 @@ interface PipelineStepSettingsProps {
|
||||
}
|
||||
|
||||
export function PipelineStepSettings({ toolId, settings, onChange }: PipelineStepSettingsProps) {
|
||||
if (toolId === "resize") return <ResizeControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "crop") return <CropControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "rotate") return <RotateControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "convert") return <ConvertControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "compress") return <CompressControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "strip-metadata")
|
||||
return <StripMetadataControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "border") return <BorderControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "watermark-text")
|
||||
return <WatermarkTextControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "text-overlay")
|
||||
return <TextOverlayControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "replace-color")
|
||||
return <ReplaceColorControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "smart-crop") return <SmartCropControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "gif-tools") return <GifToolsControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "upscale") return <UpscaleControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "blur-faces") return <BlurFacesControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "enhance-faces")
|
||||
return <EnhanceFacesControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "remove-background")
|
||||
return <RemoveBgControls settings={settings} onChange={onChange} />;
|
||||
if (toolId === "noise-removal")
|
||||
return <NoiseRemovalControls settings={settings} onChange={onChange} />;
|
||||
if (COLOR_TOOL_IDS.has(toolId))
|
||||
return <ColorControls toolId={toolId} settings={settings} onChange={onChange} />;
|
||||
const Control = CONTROLS[toolId];
|
||||
|
||||
if (Control) {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<Control settings={settings} onChange={onChange} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
if (COLOR_TOOL_IDS.has(toolId)) {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyColorControls toolId={toolId} settings={settings} onChange={onChange} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { formatHeaders } from "@/lib/api";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
type SubjectType = "people" | "products" | "general";
|
||||
@@ -565,22 +566,20 @@ export function RemoveBgSettings({ onBgPreview }: RemoveBgSettingsProps = {}) {
|
||||
// Decode HEIC via server preview endpoint
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
import("@/lib/api").then(({ formatHeaders }) => {
|
||||
fetch("/api/v1/preview", {
|
||||
method: "POST",
|
||||
headers: formatHeaders(),
|
||||
body: formData,
|
||||
fetch("/api/v1/preview", {
|
||||
method: "POST",
|
||||
headers: formatHeaders(),
|
||||
body: formData,
|
||||
})
|
||||
.then((res) => (res.ok ? res.blob() : null))
|
||||
.then((blob) => {
|
||||
if (blob && bgImageFileRef.current === file) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
revoke = () => URL.revokeObjectURL(url);
|
||||
setBgImageBlobUrl(url);
|
||||
}
|
||||
})
|
||||
.then((res) => (res.ok ? res.blob() : null))
|
||||
.then((blob) => {
|
||||
if (blob && bgImageFileRef.current === file) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
revoke = () => URL.revokeObjectURL(url);
|
||||
setBgImageBlobUrl(url);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
.catch(() => {});
|
||||
} else {
|
||||
const url = URL.createObjectURL(file);
|
||||
revoke = () => URL.revokeObjectURL(url);
|
||||
@@ -744,7 +743,7 @@ export function RemoveBgSettings({ onBgPreview }: RemoveBgSettingsProps = {}) {
|
||||
formData.append("backgroundImage", bgImageFile);
|
||||
}
|
||||
|
||||
const headers = (await import("@/lib/api")).formatHeaders();
|
||||
const headers = formatHeaders();
|
||||
const response = await fetch("/api/v1/tools/remove-background/effects", {
|
||||
method: "POST",
|
||||
headers,
|
||||
|
||||
Reference in New Issue
Block a user