0.3.0-0-pre.5
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: Web/game-snake-poc/frontend/main.html -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -81,6 +81,16 @@
|
||||
<i class="fa-solid fa-arrow-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<dl class="runtime-details small border-top pt-3 mt-3 mb-0">
|
||||
<div class="d-flex justify-content-between gap-3">
|
||||
<dt class="text-body-secondary fw-normal">Provenance</dt>
|
||||
<dd class="mb-2 text-end"><output id="runtime-provenance">web / browser / wasm / unknown / unknown</output></dd>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between gap-3">
|
||||
<dt class="text-body-secondary fw-normal">Assets</dt>
|
||||
<dd class="mb-0 text-end"><output id="asset-status">chargement…</output></dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="alert alert-info small mt-3 mb-0" role="note">
|
||||
Les entrées ne font pas avancer la simulation : elles sont consommées au prochain <code>tick()</code> WASM.
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: Web/game-snake-poc/frontend/sass/_app.scss
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
$app-header-height: 72px;
|
||||
$app-footer-height: 42px;
|
||||
@@ -101,6 +101,11 @@ body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.runtime-details dd {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.direction-pad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(64px, 88px));
|
||||
|
||||
54
Web/game-snake-poc/frontend/ts/assets.ts
Normal file
54
Web/game-snake-poc/frontend/ts/assets.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// file: Web/game-snake-poc/frontend/ts/assets.ts
|
||||
// version: 1
|
||||
|
||||
export interface SnakeWebAssets {
|
||||
engineGeneration: number;
|
||||
game: string;
|
||||
gameKind: string;
|
||||
}
|
||||
|
||||
interface CommonRuntimeAsset {
|
||||
schema: number;
|
||||
scope: string;
|
||||
engine_generation: number;
|
||||
}
|
||||
|
||||
interface GameRuntimeAsset {
|
||||
schema: number;
|
||||
game: string;
|
||||
kind: string;
|
||||
}
|
||||
|
||||
async function fetchJson<T>(relativePath: string): Promise<T> {
|
||||
const response = await fetch(new URL(relativePath, document.baseURI));
|
||||
if (!response.ok) {
|
||||
throw new Error(`Asset runtime indisponible (${response.status}) : ${relativePath}`);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
function validateCommonRuntime(asset: CommonRuntimeAsset): void {
|
||||
if (asset.schema !== 1 || asset.scope !== "common" || asset.engine_generation !== 1) {
|
||||
throw new Error("Asset common://data/runtime.json invalide pour le POC Web Snake.");
|
||||
}
|
||||
}
|
||||
|
||||
function validateGameRuntime(asset: GameRuntimeAsset): void {
|
||||
if (asset.schema !== 1 || asset.game !== "game-snake-poc" || asset.kind !== "snake") {
|
||||
throw new Error("Asset game://data/game.json invalide pour le POC Web Snake.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadSnakeWebAssets(): Promise<SnakeWebAssets> {
|
||||
const [commonRuntime, gameRuntime] = await Promise.all([
|
||||
fetchJson<CommonRuntimeAsset>("./common/data/runtime.json"),
|
||||
fetchJson<GameRuntimeAsset>("./game/data/game.json"),
|
||||
]);
|
||||
validateCommonRuntime(commonRuntime);
|
||||
validateGameRuntime(gameRuntime);
|
||||
return {
|
||||
engineGeneration: commonRuntime.engine_generation,
|
||||
game: gameRuntime.game,
|
||||
gameKind: gameRuntime.kind,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// file: Web/game-snake-poc/frontend/ts/game.ts
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
import init, { SnakeWasmGame } from "@snake-wasm";
|
||||
import { webDebug, webInfo } from "./logging";
|
||||
import { BrowserRuntimeProvenance, type SnakeInputSource } from "./provenance";
|
||||
|
||||
const FRAME_MILLIS = 16;
|
||||
const MAX_FRAME_ELAPSED_MILLIS = 250;
|
||||
@@ -9,7 +11,12 @@ const MAX_FRAME_ELAPSED_MILLIS = 250;
|
||||
export type SnakeDirection = "left" | "right" | "up" | "down";
|
||||
|
||||
export interface SnakeWebSession {
|
||||
queueDirection(direction: SnakeDirection): void;
|
||||
queueDirection(direction: SnakeDirection, source: SnakeInputSource): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
renderNow(): void;
|
||||
refreshDeviceClass(): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
function color(red: number, green: number, blue: number): string {
|
||||
@@ -71,18 +78,42 @@ export async function startGame(
|
||||
canvas: HTMLCanvasElement,
|
||||
score: HTMLOutputElement,
|
||||
length: HTMLOutputElement,
|
||||
provenanceOutput: HTMLOutputElement,
|
||||
): Promise<SnakeWebSession> {
|
||||
await init();
|
||||
webInfo("snake-web-runtime", "wasm_initialized");
|
||||
const context = canvas.getContext("2d");
|
||||
if (context === null) {
|
||||
throw new Error("Le contexte Canvas 2D est indisponible.");
|
||||
}
|
||||
const renderingContext: CanvasRenderingContext2D = context;
|
||||
const game = new SnakeWasmGame();
|
||||
const provenance = new BrowserRuntimeProvenance(game, provenanceOutput);
|
||||
let previous = performance.now();
|
||||
let accumulator = 0;
|
||||
let animationFrame: number | null = null;
|
||||
let running = false;
|
||||
let disposed = false;
|
||||
|
||||
function renderNow(): void {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
render(game, canvas, renderingContext, score, length);
|
||||
}
|
||||
|
||||
function schedule(): void {
|
||||
if (!running || disposed || animationFrame !== null) {
|
||||
return;
|
||||
}
|
||||
animationFrame = window.requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
function frame(now: number): void {
|
||||
animationFrame = null;
|
||||
if (!running || disposed) {
|
||||
return;
|
||||
}
|
||||
const elapsed = Math.min(Math.max(0, now - previous), MAX_FRAME_ELAPSED_MILLIS);
|
||||
previous = now;
|
||||
accumulator += elapsed;
|
||||
@@ -90,15 +121,59 @@ export async function startGame(
|
||||
game.tick();
|
||||
accumulator -= FRAME_MILLIS;
|
||||
}
|
||||
render(game, canvas, renderingContext, score, length);
|
||||
window.requestAnimationFrame(frame);
|
||||
renderNow();
|
||||
schedule();
|
||||
}
|
||||
|
||||
render(game, canvas, renderingContext, score, length);
|
||||
window.requestAnimationFrame(frame);
|
||||
function pause(): void {
|
||||
if (!running || disposed) {
|
||||
return;
|
||||
}
|
||||
running = false;
|
||||
accumulator = 0;
|
||||
if (animationFrame !== null) {
|
||||
window.cancelAnimationFrame(animationFrame);
|
||||
animationFrame = null;
|
||||
}
|
||||
webDebug("snake-web-runtime", "runtime_paused");
|
||||
}
|
||||
|
||||
function resume(): void {
|
||||
if (running || disposed) {
|
||||
return;
|
||||
}
|
||||
previous = performance.now();
|
||||
accumulator = 0;
|
||||
running = true;
|
||||
schedule();
|
||||
webDebug("snake-web-runtime", "runtime_resumed");
|
||||
}
|
||||
|
||||
renderNow();
|
||||
resume();
|
||||
return {
|
||||
queueDirection(direction: SnakeDirection): void {
|
||||
queueDirection(direction: SnakeDirection, source: SnakeInputSource): void {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
provenance.recordInput(source);
|
||||
queueDirection(game, direction);
|
||||
},
|
||||
pause,
|
||||
resume,
|
||||
renderNow,
|
||||
refreshDeviceClass(): void {
|
||||
if (!disposed) {
|
||||
provenance.refreshDeviceClass();
|
||||
}
|
||||
},
|
||||
dispose(): void {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
pause();
|
||||
disposed = true;
|
||||
game.free();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// file: Web/game-snake-poc/frontend/ts/input.ts
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
import type { SnakeDirection, SnakeWebSession } from "./game";
|
||||
import { webDebug } from "./logging";
|
||||
|
||||
const KEY_DIRECTIONS: Readonly<Record<string, SnakeDirection>> = {
|
||||
arrowleft: "left",
|
||||
@@ -34,7 +35,8 @@ export function bindInputs(session: SnakeWebSession, buttons: readonly HTMLButto
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
session.queueDirection(direction);
|
||||
session.queueDirection(direction, "keyboard-mouse");
|
||||
webDebug("snake-web-input", "direction", { direction, source: "keyboard-mouse" });
|
||||
});
|
||||
|
||||
for (const button of buttons) {
|
||||
@@ -44,13 +46,16 @@ export function bindInputs(session: SnakeWebSession, buttons: readonly HTMLButto
|
||||
}
|
||||
button.addEventListener("pointerdown", event => {
|
||||
event.preventDefault();
|
||||
session.queueDirection(direction);
|
||||
const source = event.pointerType === "touch" ? "touch" : "keyboard-mouse";
|
||||
session.queueDirection(direction, source);
|
||||
webDebug("snake-web-input", "direction", { direction, source });
|
||||
});
|
||||
button.addEventListener("click", event => {
|
||||
if (event.detail !== 0) {
|
||||
return;
|
||||
}
|
||||
session.queueDirection(direction);
|
||||
session.queueDirection(direction, "keyboard-mouse");
|
||||
webDebug("snake-web-input", "direction", { direction, source: "keyboard-mouse" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
58
Web/game-snake-poc/frontend/ts/lifecycle.ts
Normal file
58
Web/game-snake-poc/frontend/ts/lifecycle.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// file: Web/game-snake-poc/frontend/ts/lifecycle.ts
|
||||
// version: 1
|
||||
|
||||
import type { SnakeWebSession } from "./game";
|
||||
import { webDebug, webInfo } from "./logging";
|
||||
|
||||
export interface SnakeLifecycleBinding {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export function bindBrowserLifecycle(session: SnakeWebSession, stage: HTMLElement): SnakeLifecycleBinding {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
session.refreshDeviceClass();
|
||||
session.renderNow();
|
||||
});
|
||||
|
||||
function synchronizeVisibility(): void {
|
||||
if (document.visibilityState === "hidden") {
|
||||
session.pause();
|
||||
webDebug("snake-web-lifecycle", "visibility_hidden");
|
||||
return;
|
||||
}
|
||||
session.resume();
|
||||
session.renderNow();
|
||||
webDebug("snake-web-lifecycle", "visibility_visible");
|
||||
}
|
||||
|
||||
function pageHide(event: PageTransitionEvent): void {
|
||||
if (event.persisted) {
|
||||
session.pause();
|
||||
} else {
|
||||
session.dispose();
|
||||
}
|
||||
webInfo("snake-web-lifecycle", "page_hide", { persisted: event.persisted });
|
||||
}
|
||||
|
||||
function pageShow(event: PageTransitionEvent): void {
|
||||
session.resume();
|
||||
session.renderNow();
|
||||
webInfo("snake-web-lifecycle", "page_show", { persisted: event.persisted });
|
||||
}
|
||||
|
||||
resizeObserver.observe(stage);
|
||||
document.addEventListener("visibilitychange", synchronizeVisibility);
|
||||
window.addEventListener("pagehide", pageHide);
|
||||
window.addEventListener("pageshow", pageShow);
|
||||
synchronizeVisibility();
|
||||
|
||||
return {
|
||||
dispose(): void {
|
||||
resizeObserver.disconnect();
|
||||
document.removeEventListener("visibilitychange", synchronizeVisibility);
|
||||
window.removeEventListener("pagehide", pageHide);
|
||||
window.removeEventListener("pageshow", pageShow);
|
||||
session.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
45
Web/game-snake-poc/frontend/ts/logging.ts
Normal file
45
Web/game-snake-poc/frontend/ts/logging.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// file: Web/game-snake-poc/frontend/ts/logging.ts
|
||||
// version: 1
|
||||
|
||||
export type WebLogLevel = "debug" | "info" | "warn" | "error";
|
||||
export type WebLogFields = Readonly<Record<string, boolean | number | string | null>>;
|
||||
|
||||
type ConsoleMethod = (...items: unknown[]) => void;
|
||||
|
||||
function consoleMethod(level: WebLogLevel): ConsoleMethod {
|
||||
if (level === "debug") {
|
||||
return console.debug.bind(console);
|
||||
}
|
||||
if (level === "info") {
|
||||
return console.info.bind(console);
|
||||
}
|
||||
if (level === "warn") {
|
||||
return console.warn.bind(console);
|
||||
}
|
||||
return console.error.bind(console);
|
||||
}
|
||||
|
||||
function emit(level: WebLogLevel, target: string, action: string, fields: WebLogFields = {}): void {
|
||||
const event = {
|
||||
target,
|
||||
action,
|
||||
...fields,
|
||||
};
|
||||
consoleMethod(level)(`[games.sasedev][${target}] ${action}`, event);
|
||||
}
|
||||
|
||||
export function webDebug(target: string, action: string, fields: WebLogFields = {}): void {
|
||||
emit("debug", target, action, fields);
|
||||
}
|
||||
|
||||
export function webInfo(target: string, action: string, fields: WebLogFields = {}): void {
|
||||
emit("info", target, action, fields);
|
||||
}
|
||||
|
||||
export function webWarn(target: string, action: string, fields: WebLogFields = {}): void {
|
||||
emit("warn", target, action, fields);
|
||||
}
|
||||
|
||||
export function webError(target: string, action: string, fields: WebLogFields = {}): void {
|
||||
emit("error", target, action, fields);
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
// file: Web/game-snake-poc/frontend/ts/main.ts
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import "simplebar";
|
||||
import { loadSnakeWebAssets } from "./assets";
|
||||
import { startGame } from "./game";
|
||||
import { bindInputs } from "./input";
|
||||
import { bindBrowserLifecycle } from "./lifecycle";
|
||||
import { webError, webInfo } from "./logging";
|
||||
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
@@ -18,25 +21,39 @@ function requireElement<T extends Element>(selector: string): T {
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const canvas = requireElement<HTMLCanvasElement>("#game");
|
||||
const stage = requireElement<HTMLElement>(".game-stage");
|
||||
const score = requireElement<HTMLOutputElement>("#score");
|
||||
const length = requireElement<HTMLOutputElement>("#length");
|
||||
const provenance = requireElement<HTMLOutputElement>("#runtime-provenance");
|
||||
const assets = requireElement<HTMLOutputElement>("#asset-status");
|
||||
const status = requireElement<HTMLSpanElement>("#runtime-status");
|
||||
const error = requireElement<HTMLParagraphElement>("#startup-error");
|
||||
const buttons = Array.from(document.querySelectorAll<HTMLButtonElement>("[data-direction]"));
|
||||
|
||||
status.textContent = "Chargement assets…";
|
||||
const assetMetadata = await loadSnakeWebAssets();
|
||||
assets.value = `${assetMetadata.gameKind} / engine-v${assetMetadata.engineGeneration}`;
|
||||
webInfo("snake-web-assets", "runtime_assets_loaded", {
|
||||
engineGeneration: assetMetadata.engineGeneration,
|
||||
game: assetMetadata.game,
|
||||
gameKind: assetMetadata.gameKind,
|
||||
});
|
||||
|
||||
status.textContent = "Chargement WASM…";
|
||||
const session = await startGame(canvas, score, length);
|
||||
const session = await startGame(canvas, score, length, provenance);
|
||||
bindInputs(session, buttons);
|
||||
bindBrowserLifecycle(session, stage);
|
||||
status.className = "badge text-bg-success";
|
||||
status.textContent = "Prêt";
|
||||
error.hidden = true;
|
||||
canvas.focus();
|
||||
webInfo("snake-web-main", "runtime_ready");
|
||||
}
|
||||
|
||||
void main().catch(caughtError => {
|
||||
const status = document.querySelector<HTMLSpanElement>("#runtime-status");
|
||||
const error = document.querySelector<HTMLParagraphElement>("#startup-error");
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
const message = caughtError instanceof Error ? caughtError.stack ?? caughtError.message : String(caughtError);
|
||||
if (status !== null) {
|
||||
status.className = "badge text-bg-danger";
|
||||
status.textContent = "Erreur";
|
||||
@@ -45,4 +62,5 @@ void main().catch(caughtError => {
|
||||
error.textContent = `Impossible de démarrer Snake : ${message}`;
|
||||
error.hidden = false;
|
||||
}
|
||||
webError("snake-web-main", "runtime_start_failed", { message });
|
||||
});
|
||||
|
||||
98
Web/game-snake-poc/frontend/ts/provenance.ts
Normal file
98
Web/game-snake-poc/frontend/ts/provenance.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
// file: Web/game-snake-poc/frontend/ts/provenance.ts
|
||||
// version: 1
|
||||
|
||||
import type { SnakeWasmGame } from "@snake-wasm";
|
||||
import { webInfo } from "./logging";
|
||||
|
||||
export type SnakeInputSource = "keyboard-mouse" | "touch";
|
||||
|
||||
type DeviceClass = "desktop" | "phone" | "tablet" | "unknown";
|
||||
type InputProfile = "keyboard-mouse" | "mixed" | "touch" | "unknown";
|
||||
|
||||
function detectedDeviceClass(): DeviceClass {
|
||||
const coarsePointer = window.matchMedia("(pointer: coarse)").matches;
|
||||
const finePointer = window.matchMedia("(pointer: fine)").matches;
|
||||
if (!coarsePointer || finePointer) {
|
||||
return "desktop";
|
||||
}
|
||||
const shortEdge = Math.min(window.screen.width, window.screen.height);
|
||||
if (!Number.isFinite(shortEdge) || shortEdge <= 0) {
|
||||
return "unknown";
|
||||
}
|
||||
return shortEdge < 768 ? "phone" : "tablet";
|
||||
}
|
||||
|
||||
function inputProfile(keyboardMouseObserved: boolean, touchObserved: boolean): InputProfile {
|
||||
if (keyboardMouseObserved && touchObserved) {
|
||||
return "mixed";
|
||||
}
|
||||
if (keyboardMouseObserved) {
|
||||
return "keyboard-mouse";
|
||||
}
|
||||
if (touchObserved) {
|
||||
return "touch";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function displayLabel(game: SnakeWasmGame): string {
|
||||
return [
|
||||
game.provenance_platform_family(),
|
||||
game.provenance_runtime_host(),
|
||||
game.provenance_execution_model(),
|
||||
game.provenance_device_class(),
|
||||
game.provenance_input_profile(),
|
||||
].join(" / ");
|
||||
}
|
||||
|
||||
export class BrowserRuntimeProvenance {
|
||||
private keyboardMouseObserved = false;
|
||||
private touchObserved = false;
|
||||
private deviceClass: DeviceClass = detectedDeviceClass();
|
||||
|
||||
public constructor(
|
||||
private readonly game: SnakeWasmGame,
|
||||
private readonly output: HTMLOutputElement,
|
||||
) {
|
||||
this.synchronize();
|
||||
}
|
||||
|
||||
public recordInput(source: SnakeInputSource): void {
|
||||
if (source === "touch") {
|
||||
if (this.touchObserved) {
|
||||
return;
|
||||
}
|
||||
this.touchObserved = true;
|
||||
} else {
|
||||
if (this.keyboardMouseObserved) {
|
||||
return;
|
||||
}
|
||||
this.keyboardMouseObserved = true;
|
||||
}
|
||||
this.synchronize();
|
||||
}
|
||||
|
||||
public refreshDeviceClass(): void {
|
||||
const nextDeviceClass = detectedDeviceClass();
|
||||
if (nextDeviceClass === this.deviceClass) {
|
||||
return;
|
||||
}
|
||||
this.deviceClass = nextDeviceClass;
|
||||
this.synchronize();
|
||||
}
|
||||
|
||||
private synchronize(): void {
|
||||
const profile = inputProfile(this.keyboardMouseObserved, this.touchObserved);
|
||||
if (!this.game.configure_runtime_provenance(this.deviceClass, profile)) {
|
||||
throw new Error("La provenance navigateur n'a pas pu être configurée dans le bridge WASM.");
|
||||
}
|
||||
this.output.value = displayLabel(this.game);
|
||||
webInfo("snake-web-provenance", "runtime_provenance", {
|
||||
deviceClass: this.game.provenance_device_class(),
|
||||
executionModel: this.game.provenance_execution_model(),
|
||||
inputProfile: this.game.provenance_input_profile(),
|
||||
platformFamily: this.game.provenance_platform_family(),
|
||||
runtimeHost: this.game.provenance_runtime_host(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
"@types/node": "^26.1",
|
||||
"sass-embedded": "^1.102",
|
||||
"typescript": "^7.0",
|
||||
"vite": "^8.2"
|
||||
"vite": "^8.2",
|
||||
"vite-plugin-static-copy": "^4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// file: Web/game-snake-poc/vite.config.ts
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
import { NodePackageImporter } from "sass-embedded";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import { defineConfig, normalizePath } from "vite";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
|
||||
const appRoot = fileURLToPath(new URL(".", import.meta.url));
|
||||
const repositoryRoot = normalizePath(resolve(appRoot, "../.."));
|
||||
@@ -14,8 +15,18 @@ const wasmRoot = normalizePath(resolve(externalBuildRoot, "wasm"));
|
||||
const wasmModule = normalizePath(resolve(wasmRoot, "game_snake_poc_wasm.js"));
|
||||
const frontendDist = normalizePath(resolve(externalBuildRoot, "dist"));
|
||||
const viteCacheDir = normalizePath(resolve(externalBuildRoot, "vite-cache"));
|
||||
const commonRuntimeAsset = normalizePath(resolve(repositoryRoot, "assets/common/data/runtime.json"));
|
||||
const gameRuntimeAsset = normalizePath(resolve(repositoryRoot, "assets/game-snake-poc/data/game.json"));
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: commonRuntimeAsset, dest: "common/data" },
|
||||
{ src: gameRuntimeAsset, dest: "game/data" },
|
||||
],
|
||||
}),
|
||||
],
|
||||
base: "./",
|
||||
cacheDir: viteCacheDir,
|
||||
clearScreen: false,
|
||||
@@ -74,7 +85,7 @@ export default defineConfig({
|
||||
port: 1434,
|
||||
strictPort: true,
|
||||
fs: {
|
||||
allow: [appRoot, externalBuildRoot],
|
||||
allow: [appRoot, repositoryRoot, externalBuildRoot],
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
|
||||
Reference in New Issue
Block a user