feat(frontend): produire un bundle web distinct en transport http (#74 F1)
Le transport est figé au build par Vite : un seul `dist` ne peut pas servir
à la fois le desktop (IPC Tauri) et le navigateur (HTTP+WS). Le serveur
embarqué servait donc un bundle desktop, d'où `__TAURI_INTERNALS__ is
undefined` côté navigateur.
- `vite.config.ts` devient une factory `({ mode })` : le mode par défaut émet
le bundle desktop (`dist`), `--mode web` lit `.env.web` et émet le bundle
navigateur (`dist-web`).
- `.env.web` porte `VITE_TRANSPORT=http` dans un fichier de mode plutôt qu'en
préfixe de commande, syntaxe qui n'existe pas sous Windows (bundle NSIS).
- `transport.ts` extrait le prédicat `transportFromEnv()`, partagé par l'app et
la config de build : le constant `__IDEA_TRANSPORT__` et le transport résolu
dérivent de la même variable via le même prédicat, ils ne peuvent pas diverger.
- `main.tsx` publie `__IDEA_TRANSPORT__` sur `window` : l'affectation est un
effet de bord, elle survit à la minification et rend un bundle identifiable
sans grep d'un symbole minifié. Les deux jeux d'adapters étant présents dans
les deux bundles, leur présence ne prouve rien.
Le chemin desktop est inchangé : le web reste opt-in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
7
frontend/.env.web
Normal file
7
frontend/.env.web
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Vite mode `web` (`vite build --mode web`, ticket #74 lot F1): the bundle the
|
||||||
|
# embedded server serves to a browser talks HTTP+WS instead of Tauri IPC.
|
||||||
|
#
|
||||||
|
# This lives in a mode env file rather than an inline `VITE_TRANSPORT=http vite
|
||||||
|
# build` prefix because the npm scripts must also run on Windows (NSIS bundle),
|
||||||
|
# where that shell syntax does not exist.
|
||||||
|
VITE_TRANSPORT=http
|
||||||
@ -6,6 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc --noEmit && vite build",
|
"build": "tsc --noEmit && vite build",
|
||||||
|
"build:web": "tsc --noEmit && vite build --mode web --outDir dist-web --emptyOutDir",
|
||||||
|
"build:bundle": "npm run typecheck && vite build --outDir dist --emptyOutDir && vite build --mode web --outDir dist-web --emptyOutDir",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
shouldUseMock,
|
shouldUseMock,
|
||||||
shouldUseHttp,
|
shouldUseHttp,
|
||||||
} from "./di";
|
} from "./di";
|
||||||
|
import { transportFromEnv } from "./transport";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.unstubAllEnvs();
|
vi.unstubAllEnvs();
|
||||||
@ -66,6 +67,28 @@ describe("resolveTransport / web (HTTP+WS) selection (ticket #13, F1)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("transportFromEnv (ticket #74, F1)", () => {
|
||||||
|
// The build config derives `__IDEA_TRANSPORT__` from this same predicate, so
|
||||||
|
// pinning it here pins both the runtime transport and the build constant.
|
||||||
|
it("selects http only for the exact opt-in value", () => {
|
||||||
|
expect(transportFromEnv("http")).toBe("http");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([undefined, "", "tauri", "HTTP", "http ", "web"])(
|
||||||
|
"falls back to tauri for %o",
|
||||||
|
(value) => {
|
||||||
|
expect(transportFromEnv(value)).toBe("tauri");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("agrees with shouldUseHttp for the value the build reads", () => {
|
||||||
|
vi.stubEnv("VITE_TRANSPORT", "http");
|
||||||
|
expect(shouldUseHttp()).toBe(transportFromEnv("http") === "http");
|
||||||
|
vi.stubEnv("VITE_TRANSPORT", "");
|
||||||
|
expect(shouldUseHttp()).toBe(transportFromEnv("") === "http");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("DIProvider / useGateways", () => {
|
describe("DIProvider / useGateways", () => {
|
||||||
it("provides explicit gateways to consumers", () => {
|
it("provides explicit gateways to consumers", () => {
|
||||||
const gateways = resolveGatewaysMock();
|
const gateways = resolveGatewaysMock();
|
||||||
|
|||||||
@ -18,11 +18,11 @@ import type { Gateways } from "@/ports";
|
|||||||
import { createTauriGateways } from "@/adapters";
|
import { createTauriGateways } from "@/adapters";
|
||||||
import { createMockGateways } from "@/adapters/mock";
|
import { createMockGateways } from "@/adapters/mock";
|
||||||
import { createHttpWsGateways } from "@/adapters/http";
|
import { createHttpWsGateways } from "@/adapters/http";
|
||||||
|
import { transportFromEnv, type Transport } from "./transport";
|
||||||
|
|
||||||
const GatewaysContext = createContext<Gateways | null>(null);
|
const GatewaysContext = createContext<Gateways | null>(null);
|
||||||
|
|
||||||
/** The selected transport backing the gateways. */
|
export type { Transport };
|
||||||
export type Transport = "mock" | "http" | "tauri";
|
|
||||||
|
|
||||||
/** Whether the mock adapters should be used (env-driven, overridable in tests). */
|
/** Whether the mock adapters should be used (env-driven, overridable in tests). */
|
||||||
export function shouldUseMock(): boolean {
|
export function shouldUseMock(): boolean {
|
||||||
@ -36,14 +36,13 @@ export function shouldUseMock(): boolean {
|
|||||||
* and is never affected (ticket #13, lot F1).
|
* and is never affected (ticket #13, lot F1).
|
||||||
*/
|
*/
|
||||||
export function shouldUseHttp(): boolean {
|
export function shouldUseHttp(): boolean {
|
||||||
return import.meta.env.VITE_TRANSPORT === "http";
|
return transportFromEnv(import.meta.env.VITE_TRANSPORT) === "http";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolves which transport to use. Mock wins, then explicit web, else Tauri. */
|
/** Resolves which transport to use. Mock wins, then explicit web, else Tauri. */
|
||||||
export function resolveTransport(): Transport {
|
export function resolveTransport(): Transport {
|
||||||
if (shouldUseMock()) return "mock";
|
if (shouldUseMock()) return "mock";
|
||||||
if (shouldUseHttp()) return "http";
|
return transportFromEnv(import.meta.env.VITE_TRANSPORT);
|
||||||
return "tauri";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolves the gateway set for the current environment. */
|
/** Resolves the gateway set for the current environment. */
|
||||||
|
|||||||
@ -19,6 +19,14 @@ if (!root) {
|
|||||||
// follows the main window's focused project; otherwise the full app.
|
// follows the main window's focused project; otherwise the full app.
|
||||||
const viewParams = parseViewWindowParams(window.location.search);
|
const viewParams = parseViewWindowParams(window.location.search);
|
||||||
|
|
||||||
|
// Ticket #74, F1: publish the transport this bundle was built for. `define`
|
||||||
|
// inlines it as a literal, and assigning it is a side effect, so it survives
|
||||||
|
// minification and tree-shaking — a built bundle can be identified (by QA, or
|
||||||
|
// from devtools) without grepping for a minified symbol. Both adapter sets ship
|
||||||
|
// in either bundle, so their mere presence proves nothing.
|
||||||
|
(window as unknown as { __IDEA_TRANSPORT__?: string }).__IDEA_TRANSPORT__ =
|
||||||
|
__IDEA_TRANSPORT__;
|
||||||
|
|
||||||
// Ticket #13, F2: in web (HTTP) transport mode the browser client renders the
|
// Ticket #13, F2: in web (HTTP) transport mode the browser client renders the
|
||||||
// pairing-gated, read-only `WebApp`. Desktop (Tauri) is unchanged — it renders
|
// pairing-gated, read-only `WebApp`. Desktop (Tauri) is unchanged — it renders
|
||||||
// the full `App` (or a detached view window). Detached windows are desktop-only.
|
// the full `App` (or a detached view window). Detached windows are desktop-only.
|
||||||
|
|||||||
19
frontend/src/app/transport.ts
Normal file
19
frontend/src/app/transport.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* The transport signal, shared by the app and the Vite build config.
|
||||||
|
*
|
||||||
|
* `vite.config.ts` imports {@link transportFromEnv} to compute the
|
||||||
|
* `__IDEA_TRANSPORT__` build constant from the very same `VITE_TRANSPORT`
|
||||||
|
* variable that {@link shouldUseHttp} reads (ticket #74, F1). One predicate,
|
||||||
|
* one variable: the greppable constant and the resolved transport cannot drift.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** The selected transport backing the gateways. */
|
||||||
|
export type Transport = "mock" | "http" | "tauri";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The transport a raw `VITE_TRANSPORT` value selects, mock aside. Web is opt-in
|
||||||
|
* (`"http"`) so the desktop path stays the default and is never affected.
|
||||||
|
*/
|
||||||
|
export function transportFromEnv(value: string | undefined): "http" | "tauri" {
|
||||||
|
return value === "http" ? "http" : "tauri";
|
||||||
|
}
|
||||||
12
frontend/src/vite-env.d.ts
vendored
12
frontend/src/vite-env.d.ts
vendored
@ -9,3 +9,15 @@ interface ImportMetaEnv {
|
|||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv;
|
readonly env: ImportMetaEnv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The transport this bundle was built for, inlined as a literal by Vite
|
||||||
|
* `define` (ticket #74, F1). It is derived in `vite.config.ts` from the same
|
||||||
|
* `VITE_TRANSPORT` variable and the same `transportFromEnv()` predicate that
|
||||||
|
* `resolveTransport()` uses, so it always agrees with the adapters actually
|
||||||
|
* wired in — mock aside, which is a separate `VITE_USE_MOCK` switch.
|
||||||
|
*
|
||||||
|
* `main.tsx` publishes it on `window`, which makes it survive minification and
|
||||||
|
* lets a built bundle be identified without grepping for a minified symbol.
|
||||||
|
*/
|
||||||
|
declare const __IDEA_TRANSPORT__: "http" | "tauri";
|
||||||
|
|||||||
@ -1,16 +1,33 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { fileURLToPath, URL } from "node:url";
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
|
||||||
|
import { transportFromEnv } from "./src/app/transport";
|
||||||
|
|
||||||
|
const root = fileURLToPath(new URL(".", import.meta.url));
|
||||||
|
|
||||||
// Vite config tuned for Tauri v2 (fixed dev port, no clearing the terminal).
|
// Vite config tuned for Tauri v2 (fixed dev port, no clearing the terminal).
|
||||||
export default defineConfig({
|
//
|
||||||
|
// Two build artefacts (ticket #74): the default mode emits the desktop bundle
|
||||||
|
// (`dist`, Tauri IPC transport), and `--mode web` reads `.env.web` to emit the
|
||||||
|
// browser bundle served by the embedded server (`dist-web`, HTTP+WS transport).
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
// Same env file resolution Vite applies to `import.meta.env`, so the
|
||||||
|
// `__IDEA_TRANSPORT__` constant below is computed from the exact variable
|
||||||
|
// `resolveTransport()` reads, through the exact same predicate (#74, F1).
|
||||||
|
const env = loadEnv(mode, root);
|
||||||
|
|
||||||
|
return {
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
define: {
|
||||||
|
__IDEA_TRANSPORT__: JSON.stringify(transportFromEnv(env.VITE_TRANSPORT)),
|
||||||
|
},
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
@ -20,4 +37,5 @@ export default defineConfig({
|
|||||||
target: "es2021",
|
target: "es2021",
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
},
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { defineConfig } from "vitest/config";
|
|||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { fileURLToPath, URL } from "node:url";
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
|
||||||
|
import { transportFromEnv } from "./src/app/transport";
|
||||||
|
|
||||||
// Vitest config for the frontend hexagonal layers (domain/ports/adapters/app).
|
// Vitest config for the frontend hexagonal layers (domain/ports/adapters/app).
|
||||||
// jsdom is used so React-Testing-Library can render the DI provider.
|
// jsdom is used so React-Testing-Library can render the DI provider.
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@ -12,6 +14,13 @@ export default defineConfig({
|
|||||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Mirrors the build constant (#74, F1) through the same predicate, so code
|
||||||
|
// reading `__IDEA_TRANSPORT__` stays testable.
|
||||||
|
define: {
|
||||||
|
__IDEA_TRANSPORT__: JSON.stringify(
|
||||||
|
transportFromEnv(process.env.VITE_TRANSPORT),
|
||||||
|
),
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
globals: true,
|
globals: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user