From 460eb424475fe9555a8a2f2a234fde54e3c41b32 Mon Sep 17 00:00:00 2001 From: adrians5j Date: Tue, 4 Aug 2026 12:56:29 +0200 Subject: [PATCH 1/5] fix(create-webiny-project): clarify self-hosted watch instructions Watching all apps at once isn't supported yet for the self-hosted hosting type, but the post-create message told users to run a single `webiny watch` command. Update the message to run the API and Admin apps separately. Co-Authored-By: Claude Opus 4.8 --- .../src/features/CreateWebinyProject.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/create-webiny-project/src/features/CreateWebinyProject.ts b/packages/create-webiny-project/src/features/CreateWebinyProject.ts index 940f1772428..cf72ea7a313 100644 --- a/packages/create-webiny-project/src/features/CreateWebinyProject.ts +++ b/packages/create-webiny-project/src/features/CreateWebinyProject.ts @@ -233,8 +233,8 @@ export class CreateWebinyProject { console.log( yellow( "⚠ The self-hosted (server) hosting type is in ALPHA. It's for local\n" + - " development and testing with `webiny watch`. It's still maturing, so\n" + - " expect some rough edges." + " development and testing with `webiny watch api` / `webiny watch admin`.\n" + + " It's still maturing, so expect some rough edges." ) ); console.log(); @@ -246,9 +246,12 @@ export class CreateWebinyProject { console.log( [ - `Start your project in development mode by running: ${green( - `cd ${projectName} && yarn webiny watch` - )}`, + `First, enter the project directory: ${green(`cd ${projectName}`)}`, + "", + "Then start development mode. Watching all apps at once isn't supported yet,", + `so run the API and Admin apps separately, each in its own terminal: ${green( + "yarn webiny watch api" + )} and ${green("yarn webiny watch admin")}.`, "", `To see all of the available CLI commands, run ${green( "yarn webiny --help" From de726b7f1638f0937beac39445b6be8e73d01227 Mon Sep 17 00:00:00 2001 From: adrians5j Date: Tue, 4 Aug 2026 14:30:14 +0200 Subject: [PATCH 2/5] fix(project-server-template): prevent oxfmt from mangling GLOBAL_CSS placeholder oxfmt reformatted the admin index.tsx `{GLOBAL_CSS}` placeholder into a block statement, so the literal find-replace in ServerBuildAppWorkspaceService no longer matched and the bare `GLOBAL_CSS;` shipped to the workspace (TS2304). Exclude the server template's appTemplates folder from oxfmt (mirroring the AWS template) and restore the placeholder. Co-Authored-By: Claude Opus 4.8 --- .oxfmtrc.json | 1 + .../template/appTemplates/admin/src/index.tsx | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.oxfmtrc.json b/.oxfmtrc.json index f5def32cd84..69b5b7cfc22 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -34,6 +34,7 @@ ".webiny/**", "packages/admin-ui/scripts/importFromFigma/exports/Alias tokens.json", "packages/project-aws/_templates/appTemplates/", + "packages/project-server-template/template/appTemplates/", "coverage/**", "packages/cli-core/files/**/*.json", "packages/create-webiny-project/utils/binaries/**", diff --git a/packages/project-server-template/template/appTemplates/admin/src/index.tsx b/packages/project-server-template/template/appTemplates/admin/src/index.tsx index e1a4af5d806..8ecc5d8f0ce 100644 --- a/packages/project-server-template/template/appTemplates/admin/src/index.tsx +++ b/packages/project-server-template/template/appTemplates/admin/src/index.tsx @@ -1,7 +1,5 @@ import React from "react"; -{ - GLOBAL_CSS; -} +{GLOBAL_CSS} import { App } from "./App"; From 24c6b674eb8617395a5ae3096ac2b1bd1b0794a4 Mon Sep 17 00:00:00 2001 From: adrians5j Date: Tue, 4 Aug 2026 15:46:49 +0200 Subject: [PATCH 3/5] fix(build-tools): restore server worker-chunk assetPrefix in rsbuild config The 6.5.0 stale-branch merge (#5396) clobbered createRsbuildConfig.js back to a pre-#5453 state, dropping the `assetPrefix: "auto"` config and the isServer externals gating. Without assetPrefix, the self-hosted bg-tasks worker chunk (spawned via `new Worker(new URL(..., import.meta.url))`) resolved to an absolute filesystem-root URL and failed with "Cannot find module". The referencing comments in WorkerTaskService/BreeSchedulerService survived the revert, masking the loss. Restore the #5453 version. Co-Authored-By: Claude Opus 4.8 --- .../bundling/function/createRsbuildConfig.js | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/build-tools/bundling/function/createRsbuildConfig.js b/packages/build-tools/bundling/function/createRsbuildConfig.js index d596125c085..0e42d82f3ea 100644 --- a/packages/build-tools/bundling/function/createRsbuildConfig.js +++ b/packages/build-tools/bundling/function/createRsbuildConfig.js @@ -10,6 +10,11 @@ export const createRsbuildConfig = async ({ cwd, enforceMaxBundleSize }) => { const paths = getPaths(cwd); const mode = getMode(); const isDebugEnabled = process.env.DEBUG === "true"; + // NOTE: dirty for now — sniffing the hosting type off an env var here. Ideally the build config + // wouldn't know about hosting types at all: the caller (createBuildFunction, or the flavour's own + // build layer) would pass in the externals policy + assetPrefix as options, keeping this file + // hosting-agnostic. Good enough while the server hosting type is ALPHA; revisit when it settles. + const isServer = process.env.WEBINY_HOSTING_TYPE === "server"; // Configurable via WEBINY_INFRA_API_MAX_BUNDLE_SIZE (bytes). // Only enforced during build — watch mode skips size checks. @@ -19,9 +24,20 @@ export const createRsbuildConfig = async ({ cwd, enforceMaxBundleSize }) => { return /** @type {import("@rsbuild/core").RsbuildConfig} */ ({ source: { entry: { index: paths.fn.entryFile } }, + // Resolve chunk/asset URLs relative to the running module, not from an absolute root. The + // server's bg-tasks worker chunk is spawned via `new Worker(new URL("...", import.meta.url))` + // and rsbuild's default publicPath "/" turns that into an ABSOLUTE url ("/xyz.mjs"), which + // resolves to the filesystem root and can't be found. "auto" resolves it relative to the module + // (handler.mjs) instead, so the chunk loads from build/. publicPath comes from + // `output.assetPrefix` in production and `dev.assetPrefix` in development (watch runs + // `rsbuild.build({ watch: true })` under NODE_ENV=development), so set BOTH. Not gated to the + // server hosting type: AWS has no worker chunks so it's a no-op there, and for any other + // (async import) chunks "auto" is at least as correct as "/" (Lambda runs from its own dir too). + dev: { assetPrefix: "auto" }, output: { module: true, target: "node", + assetPrefix: "auto", minify: true, sourceMap: { js: isDebugEnabled || mode === "development" ? "source-map" : false @@ -48,20 +64,15 @@ export const createRsbuildConfig = async ({ cwd, enforceMaxBundleSize }) => { maxAssetSize: maxBundleSize } }), - externals: [ - /^@aws-sdk/, - /^aws-sdk$/, - /^sharp$/, - // knex is a runtime-polymorphic package: its source statically require()s a - // driver for EVERY SQL dialect (pg, mysql, mariadb, mssql, sqlite, ...) and - // picks one at runtime from the configured `client`. Bundling it forces the - // bundler to resolve drivers that aren't installed (the mariadb/tedious/pg - // "Module not found" errors). The server flavour runs as a Node process with - // node_modules present, so we externalize knex and let Node load it — knex then - // lazily require()s ONLY the configured dialect's driver (e.g. better-sqlite3), - // never the others. - /^knex(\/|$)/ - ], + // Both hosting types bundle; externalize only what genuinely can't be bundled. + // sharp is a native .node binary; knex statically require()s a driver for every SQL + // dialect (bundling pulls in uninstalled ones) and lazily loads only the configured + // one at runtime (e.g. better-sqlite3). AWS additionally externalizes aws-sdk (the + // Lambda runtime provides it). Server ships these in build/node_modules — the build's + // packaging step (CI/Linux) copies them, natives included. + externals: isServer + ? [/^sharp$/, /^knex(\/|$)/] + : [/^@aws-sdk/, /^aws-sdk$/, /^sharp$/, /^knex(\/|$)/], plugins: [ // Ignore optional `canvas` native module required by jsdom. // https://rspack.dev/plugins/webpack/ignore-plugin From 099bee681563cfdc0d1859a28013cf4959687b7c Mon Sep 17 00:00:00 2001 From: adrians5j Date: Tue, 4 Aug 2026 17:25:49 +0200 Subject: [PATCH 4/5] fix(api-websockets): compare connection recency by epoch, not raw string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SendToIdentity silently matched zero connections on SQL-backed (self-hosted) deployments, so server->client pushes (e.g. the file-manager AI enrichment notification) never arrived even though the connection was registered under the correct identity. ListConnectionsUseCase filtered stale connections with a lexicographic string comparison (`connectedOn >= `). But `connectedOn` is a SQL `datetime` column, and the driver returns it in a shape that doesn't compare against an ISO string: mysql2 hands back a `Date` (coerces to "Wed Aug 04 2026 ...", sorts below "2026-...") and other drivers return a `T`/`Z`-less "2026-08-04 17:02:06" (space sorts before `T`). Either way every live connection read as expired and was dropped. Compare by parsed UTC epoch instead, normalizing the space form back to UTC (safe — the stored wall-clock is UTC), so it works regardless of driver format. Co-Authored-By: Claude Opus 4.8 --- .../ListConnections/ListConnectionsUseCase.ts | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/api-websockets/src/features/ListConnections/ListConnectionsUseCase.ts b/packages/api-websockets/src/features/ListConnections/ListConnectionsUseCase.ts index 3f4a740168b..307bc4c6381 100644 --- a/packages/api-websockets/src/features/ListConnections/ListConnectionsUseCase.ts +++ b/packages/api-websockets/src/features/ListConnections/ListConnectionsUseCase.ts @@ -27,8 +27,30 @@ class ListConnectionsUseCaseImpl implements WebsocketsListConnectionsUseCase.Int return Result.fail(new WebsocketServiceError(error as Error)); } - const threeHoursAgo = new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(); - connections = connections.filter(c => c.connectedOn >= threeHoursAgo); + // Keep only connections seen within the last 3 hours. `connectedOn` is written as a UTC ISO + // string, but SQL `datetime` columns hand it back in a driver-specific shape — a `Date` object + // (mysql2) or a `T`/`Z`-less string like "2026-08-04 17:02:06". A raw comparison against an ISO + // cutoff string breaks for both (a `Date` coerces to "Wed Aug 04 2026 …" and a space sorts + // before `T`), so every live connection reads as expired. Compare by parsed UTC epoch instead, + // normalizing the space form back to UTC (safe — the stored wall-clock IS UTC). + const cutoff = Date.now() - 3 * 60 * 60 * 1000; + const toEpoch = (value: unknown): number => { + if (value instanceof Date) { + return value.getTime(); + } + if (typeof value === "string") { + const normalized = + value.includes(" ") && !value.includes("T") + ? `${value.replace(" ", "T")}Z` + : value; + return new Date(normalized).getTime(); + } + return NaN; + }; + connections = connections.filter(c => { + const epoch = toEpoch(c.connectedOn); + return Number.isFinite(epoch) && epoch >= cutoff; + }); return Result.ok(connections); } From d7d512ad7c27652edaccb1042d15da6a940dc3bc Mon Sep 17 00:00:00 2001 From: adrians5j Date: Tue, 4 Aug 2026 17:43:23 +0200 Subject: [PATCH 5/5] refactor(api-websockets): extract connection-recency helpers Move the connectedOn epoch parsing and recency predicate out of execute() into module-level helpers (connectedOnToEpoch, isRecentConnection) plus a named RECENT_CONNECTION_WINDOW_MS constant. No behavior change. Co-Authored-By: Claude Opus 4.8 --- .../ListConnections/ListConnectionsUseCase.ts | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/packages/api-websockets/src/features/ListConnections/ListConnectionsUseCase.ts b/packages/api-websockets/src/features/ListConnections/ListConnectionsUseCase.ts index 307bc4c6381..0f3d93d08c5 100644 --- a/packages/api-websockets/src/features/ListConnections/ListConnectionsUseCase.ts +++ b/packages/api-websockets/src/features/ListConnections/ListConnectionsUseCase.ts @@ -4,6 +4,36 @@ import type { WebsocketsError } from "~/features/shared/errors.js"; import { WebsocketServiceError } from "~/features/shared/errors.js"; import { ConnectionRegistry } from "~/features/ConnectionRegistry/abstractions.js"; +const RECENT_CONNECTION_WINDOW_MS = 3 * 60 * 60 * 1000; + +/** + * Parse a stored `connectedOn` value to a UTC epoch (ms). + * + * `connectedOn` is written as a UTC ISO string, but SQL `datetime` columns hand it back in a + * driver-specific shape: a `Date` object (mysql2) or a `T`/`Z`-less string like "2026-08-04 17:02:06". + * We normalize the string form back to UTC (safe — the stored wall-clock is UTC) so recency checks + * don't depend on the driver's formatting. Returns NaN for anything unparseable. + */ +const connectedOnToEpoch = (value: unknown): number => { + if (value instanceof Date) { + return value.getTime(); + } + if (typeof value === "string") { + const normalized = + value.includes(" ") && !value.includes("T") ? `${value.replace(" ", "T")}Z` : value; + return new Date(normalized).getTime(); + } + return NaN; +}; + +const isRecentConnection = ( + connection: WebsocketsListConnectionsUseCase.RegistryData, + cutoff: number +): boolean => { + const epoch = connectedOnToEpoch(connection.connectedOn); + return Number.isFinite(epoch) && epoch >= cutoff; +}; + class ListConnectionsUseCaseImpl implements WebsocketsListConnectionsUseCase.Interface { public constructor(private readonly registry: ConnectionRegistry.Interface) {} @@ -27,30 +57,9 @@ class ListConnectionsUseCaseImpl implements WebsocketsListConnectionsUseCase.Int return Result.fail(new WebsocketServiceError(error as Error)); } - // Keep only connections seen within the last 3 hours. `connectedOn` is written as a UTC ISO - // string, but SQL `datetime` columns hand it back in a driver-specific shape — a `Date` object - // (mysql2) or a `T`/`Z`-less string like "2026-08-04 17:02:06". A raw comparison against an ISO - // cutoff string breaks for both (a `Date` coerces to "Wed Aug 04 2026 …" and a space sorts - // before `T`), so every live connection reads as expired. Compare by parsed UTC epoch instead, - // normalizing the space form back to UTC (safe — the stored wall-clock IS UTC). - const cutoff = Date.now() - 3 * 60 * 60 * 1000; - const toEpoch = (value: unknown): number => { - if (value instanceof Date) { - return value.getTime(); - } - if (typeof value === "string") { - const normalized = - value.includes(" ") && !value.includes("T") - ? `${value.replace(" ", "T")}Z` - : value; - return new Date(normalized).getTime(); - } - return NaN; - }; - connections = connections.filter(c => { - const epoch = toEpoch(c.connectedOn); - return Number.isFinite(epoch) && epoch >= cutoff; - }); + // Keep only connections seen within the recency window. + const cutoff = Date.now() - RECENT_CONNECTION_WINDOW_MS; + connections = connections.filter(c => isRecentConnection(c, cutoff)); return Result.ok(connections); }