release: v1.4.2 #9632

This commit is contained in:
sriram veeraghanta
2026-08-23 20:03:06 +05:30
committed by GitHub
23 changed files with 111 additions and 20 deletions

View File

@@ -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",

View File

@@ -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"

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "space",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"type": "module",

View File

@@ -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,

View File

@@ -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";
@@ -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} />;
}

View File

@@ -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;
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;
};

View File

@@ -1,6 +1,6 @@
{
"name": "web",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"type": "module",

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "@plane/codemods",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"scripts": {
"test": "vitest run",

View File

@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@plane/editor",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"description": "Core Editor that powers Plane",
"keywords": [

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "@plane/propel",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@plane/services",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"type": "module",

View File

@@ -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",

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@plane/typescript-config",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"license": "AGPL-3.0",
"files": [

View File

@@ -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",

View File

@@ -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",