Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/admin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "admin",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"description": "Admin UI for Plane",
"license": "AGPL-3.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "plane-api",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"description": "API server powering Plane's backend",
"license": "AGPL-3.0"
Expand Down
2 changes: 1 addition & 1 deletion apps/live/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "live",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"description": "A realtime collaborative server powers Plane's rich text editor",
"license": "AGPL-3.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/space/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "space",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"type": "module",
Expand Down
18 changes: 18 additions & 0 deletions apps/web/app/entry.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,27 @@ import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";

import polyfills from "@/lib/polyfills";
import { isStaleAssetErrorMessage, recoverFromStaleAsset } from "@/lib/stale-asset-error";

void polyfills;

// Production-only: in dev these errors come from the dev server itself (restarts,
// stale optimized deps) and auto-reloading would mask them.
if (import.meta.env.PROD) {
window.addEventListener("vite:preloadError", (event) => {
if (recoverFromStaleAsset()) event.preventDefault();
});

window.addEventListener("error", (event) => {
if (isStaleAssetErrorMessage(event.message || "")) recoverFromStaleAsset();
});

window.addEventListener("unhandledrejection", (event) => {
const reason = event.reason instanceof Error ? event.reason.message : String(event.reason ?? "");
if (isStaleAssetErrorMessage(reason)) recoverFromStaleAsset();
});
}

startTransition(() => {
hydrateRoot(
document,
Expand Down
7 changes: 7 additions & 0 deletions apps/web/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import globalStyles from "@/styles/globals.css?url";
import type { Route } from "./+types/root";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
// lib
import { isStaleAssetError, recoverFromStaleAsset } from "@/lib/stale-asset-error";
// local
import { CustomErrorComponent } from "./error";
import { AppProvider } from "./provider";
Expand Down Expand Up @@ -146,5 +148,10 @@ export function HydrateFallback() {
}

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
// A stale chunk failure surfaces here as React Router's own wrapper error
// (the failed dynamic import itself never reaches a window event) — recover
// the same way entry.client.tsx does instead of just showing the error page.
if (import.meta.env.PROD && isStaleAssetError(error)) recoverFromStaleAsset();

return <CustomErrorComponent error={error} />;
}
Comment on lines 150 to 157
66 changes: 66 additions & 0 deletions apps/web/core/lib/stale-asset-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/

// Error signatures produced when a deploy removes hashed assets that an open tab
// still references (stale-tab version skew).
const STALE_ASSET_ERROR_SIGNATURES = [
"Failed to fetch dynamically imported module", // Chromium
"error loading dynamically imported module", // Firefox
"Importing a module script failed", // Safari
"Unable to preload CSS", // Vite preload helper
// React Router's own wrapper when a route module's lazy import fails during
// navigation — the original browser error above never reaches here as a
// window event, only this synthesized message.
"No result returned from dataStrategy for route",
];

export const isStaleAssetErrorMessage = (message: string): boolean =>
STALE_ASSET_ERROR_SIGNATURES.some((signature) => message.includes(signature));

export const isStaleAssetError = (error: unknown): boolean => {
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
return isStaleAssetErrorMessage(message);
};

// Reload-once-then-fall-through-to-boundary guard, shared by the window-level
// listeners (entry.client.tsx) and the route ErrorBoundary (root.tsx) so both
// paths to the same failure share one reload attempt instead of racing.
const STALE_ASSET_RELOAD_KEY = "__plane_chunk_reload";
const STALE_ASSET_RELOAD_WINDOW_MS = 30_000;

const hasRecentStaleAssetReload = (): boolean => {
try {
const lastReloadAt = Number(sessionStorage.getItem(STALE_ASSET_RELOAD_KEY));
return Number.isFinite(lastReloadAt) && Date.now() - lastReloadAt < STALE_ASSET_RELOAD_WINDOW_MS;
} catch {
// No storage means no loop guard — never auto-reload in that case.
return true;
}
};

let isRecoveringFromStaleAsset = false;

// Returns whether it actually triggered a reload, so callers that can otherwise
// fall back to default browser error handling (e.g. Vite's preload-error
// overlay) only suppress that fallback when a reload is really in flight.
export const recoverFromStaleAsset = (): boolean => {
if (isRecoveringFromStaleAsset) return false;
isRecoveringFromStaleAsset = true;
Comment on lines +46 to +51

if (hasRecentStaleAssetReload()) {
// Second failure in a row — surface to the route error boundary instead of looping.
isRecoveringFromStaleAsset = false;
return false;
}
try {
sessionStorage.setItem(STALE_ASSET_RELOAD_KEY, String(Date.now()));
} catch {
isRecoveringFromStaleAsset = false;
return false;
}
window.location.reload();
return true;
};
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "plane",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"description": "Open-source project management that unlocks customer value",
"license": "AGPL-3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/codemods/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/codemods",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"scripts": {
"test": "vitest run",
Expand Down
2 changes: 1 addition & 1 deletion packages/constants/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/editor",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"description": "Core Editor that powers Plane",
"keywords": [
Expand Down
2 changes: 1 addition & 1 deletion packages/hooks/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/hooks",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"description": "React hooks that are shared across multiple apps internally",
"license": "AGPL-3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/i18n",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"description": "I18n shared across multiple apps internally",
"license": "AGPL-3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/logger/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/logger",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"description": "Logger shared across multiple apps internally",
"license": "AGPL-3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/propel/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/propel",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/services/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/services",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/shared-state/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/shared-state",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"description": "Shared state shared across multiple apps internally",
"license": "AGPL-3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/tailwind-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/tailwind-config",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"description": "common tailwind configuration across monorepo",
"license": "AGPL-3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/types/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/typescript-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/typescript-config",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"files": [
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/ui",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"description": "UI components shared across multiple apps internally",
"license": "AGPL-3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/utils",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"description": "Helper functions shared across multiple apps internally",
"license": "AGPL-3.0",
Expand Down
Loading