Feat: Web loader
This commit is contained in:
@@ -13,6 +13,10 @@
|
|||||||
- [ ] Proxy
|
- [ ] Proxy
|
||||||
- [ ] Local-Prod-NGINX-Proxy
|
- [ ] Local-Prod-NGINX-Proxy
|
||||||
- [ ] Static-Frontend-Serving
|
- [ ] Static-Frontend-Serving
|
||||||
|
- [ ] First-Request-Web-Loader
|
||||||
|
- [ ] Bootstrap-Document
|
||||||
|
- [ ] Route-Intent-Handoff
|
||||||
|
- [ ] Tiny-First-Paint-Budget
|
||||||
- [ ] Dev-and-Prod-Builds
|
- [ ] Dev-and-Prod-Builds
|
||||||
- [x] Local-Dev-Just-Commands
|
- [x] Local-Dev-Just-Commands
|
||||||
- [x] Local-Dev-Docker-Compose
|
- [x] Local-Dev-Docker-Compose
|
||||||
|
|||||||
35
Loader/README.md
Normal file
35
Loader/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Loader
|
||||||
|
|
||||||
|
Tiny first-request bootstrap document for Moku.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The loader is intended to be the smallest possible first paint shown before the
|
||||||
|
real application is handed off.
|
||||||
|
|
||||||
|
Long term responsibilities:
|
||||||
|
|
||||||
|
- capture original route intent
|
||||||
|
- collect lightweight client capability hints
|
||||||
|
- make a tiny boot decision
|
||||||
|
- hand off to login or app
|
||||||
|
|
||||||
|
## Route intent contract
|
||||||
|
|
||||||
|
The loader writes bootstrap state to `sessionStorage`:
|
||||||
|
|
||||||
|
- `moku.loader.intent` — the intended route path
|
||||||
|
- `moku.loader.meta` — JSON metadata for the current bootstrap event
|
||||||
|
|
||||||
|
The loader also writes a short-lived cookie:
|
||||||
|
|
||||||
|
- `moku_loader_seen=1`
|
||||||
|
|
||||||
|
The proxy uses that cookie to decide whether to keep serving the loader or to
|
||||||
|
serve the real app document on the next request.
|
||||||
|
|
||||||
|
## Current handoff behavior
|
||||||
|
|
||||||
|
For now, the loader simply hands off to the originally requested route. That
|
||||||
|
keeps the first implementation tiny while leaving a clear place to add auth or
|
||||||
|
perf-tier decisions later.
|
||||||
44
Loader/index.html
Normal file
44
Loader/index.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="color-scheme" content="dark" />
|
||||||
|
<title>Moku Loader</title>
|
||||||
|
<link rel="stylesheet" href="/__loader/styles.css" />
|
||||||
|
<script type="module" src="/__loader/scripts/main.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="loader-shell" aria-busy="true" aria-live="polite">
|
||||||
|
<section class="loader-panel" aria-label="Application bootstrap">
|
||||||
|
<p class="loader-eyebrow">Moku bootstrap</p>
|
||||||
|
<h1 class="loader-title">Starting Moku</h1>
|
||||||
|
<p class="loader-copy">Capturing route intent and preparing the lightest possible handoff.</p>
|
||||||
|
|
||||||
|
<div class="loader-indicators" aria-hidden="true">
|
||||||
|
<span class="loader-dot"></span>
|
||||||
|
<span class="loader-dot"></span>
|
||||||
|
<span class="loader-dot"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="loader-meta">
|
||||||
|
<div class="loader-meta-row">
|
||||||
|
<dt>Status</dt>
|
||||||
|
<dd id="loader-status">Initializing loader</dd>
|
||||||
|
</div>
|
||||||
|
<div class="loader-meta-row">
|
||||||
|
<dt>Intent</dt>
|
||||||
|
<dd id="loader-intent">Waiting for capture</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<section class="loader-noscript">
|
||||||
|
<strong>JavaScript is required.</strong>
|
||||||
|
<span>Moku's bootstrap loader needs JavaScript to hand off to the app.</span>
|
||||||
|
</section>
|
||||||
|
</noscript>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
Loader/scripts/config.js
Normal file
9
Loader/scripts/config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Path: Loader/scripts/config.js
|
||||||
|
|
||||||
|
export const LOADER_INTENT_STORAGE_KEY = "moku.loader.intent";
|
||||||
|
export const LOADER_META_STORAGE_KEY = "moku.loader.meta";
|
||||||
|
export const LOADER_SEEN_COOKIE_NAME = "moku_loader_seen";
|
||||||
|
export const LOADER_DEFAULT_INTENT = "/";
|
||||||
|
export const LOADER_ROOT_PREFIX = "/__loader";
|
||||||
|
export const LOADER_HANDOFF_DELAY_MS = 720;
|
||||||
|
export const LOADER_SEEN_COOKIE_MAX_AGE_SECONDS = 1800;
|
||||||
25
Loader/scripts/dom.js
Normal file
25
Loader/scripts/dom.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// Path: Loader/scripts/dom.js
|
||||||
|
|
||||||
|
export const getStatusElement = () => {
|
||||||
|
return document.getElementById("loader-status");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIntentElement = () => {
|
||||||
|
return document.getElementById("loader-intent");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setStatus = (value) => {
|
||||||
|
const statusElement = getStatusElement();
|
||||||
|
|
||||||
|
if (statusElement instanceof HTMLElement) {
|
||||||
|
statusElement.textContent = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setIntentPreview = (value) => {
|
||||||
|
const intentElement = getIntentElement();
|
||||||
|
|
||||||
|
if (intentElement instanceof HTMLElement) {
|
||||||
|
intentElement.textContent = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
13
Loader/scripts/handoff.js
Normal file
13
Loader/scripts/handoff.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Path: Loader/scripts/handoff.js
|
||||||
|
|
||||||
|
import { LOADER_HANDOFF_DELAY_MS } from "./config.js";
|
||||||
|
|
||||||
|
export const scheduleHandoff = (target, onBeforeHandoff) => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (typeof onBeforeHandoff === "function") {
|
||||||
|
onBeforeHandoff();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.assign(target);
|
||||||
|
}, LOADER_HANDOFF_DELAY_MS);
|
||||||
|
};
|
||||||
19
Loader/scripts/hints.js
Normal file
19
Loader/scripts/hints.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Path: Loader/scripts/hints.js
|
||||||
|
|
||||||
|
export const collectClientHints = () => {
|
||||||
|
const navigatorConnection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
||||||
|
|
||||||
|
return {
|
||||||
|
capturedAt: new Date().toISOString(),
|
||||||
|
language: navigator.language,
|
||||||
|
languages: Array.isArray(navigator.languages) ? navigator.languages : [],
|
||||||
|
hardwareConcurrency: navigator.hardwareConcurrency ?? null,
|
||||||
|
deviceMemory: navigator.deviceMemory ?? null,
|
||||||
|
networkType: navigatorConnection?.effectiveType ?? null,
|
||||||
|
saveData: navigatorConnection?.saveData ?? null,
|
||||||
|
viewport: {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
32
Loader/scripts/intent.js
Normal file
32
Loader/scripts/intent.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Path: Loader/scripts/intent.js
|
||||||
|
|
||||||
|
import { LOADER_DEFAULT_INTENT, LOADER_ROOT_PREFIX } from "./config.js";
|
||||||
|
|
||||||
|
export const getExplicitIntent = () => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const next = params.get("next");
|
||||||
|
|
||||||
|
if (typeof next === "string" && next.startsWith("/")) {
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCurrentPathIntent = () => {
|
||||||
|
const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||||
|
|
||||||
|
if (current === "/" || current === LOADER_ROOT_PREFIX || current === `${LOADER_ROOT_PREFIX}/` || current.startsWith(`${LOADER_ROOT_PREFIX}/`)) {
|
||||||
|
return LOADER_DEFAULT_INTENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveIntent = () => {
|
||||||
|
return getExplicitIntent() ?? getCurrentPathIntent();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveHandoffTarget = (intent) => {
|
||||||
|
return intent || LOADER_DEFAULT_INTENT;
|
||||||
|
};
|
||||||
26
Loader/scripts/main.js
Normal file
26
Loader/scripts/main.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Path: Loader/scripts/main.js
|
||||||
|
|
||||||
|
import { setIntentPreview, setStatus } from "./dom.js";
|
||||||
|
import { scheduleHandoff } from "./handoff.js";
|
||||||
|
import { resolveHandoffTarget, resolveIntent } from "./intent.js";
|
||||||
|
import { markLoaderSeen, persistLoaderState } from "./storage.js";
|
||||||
|
|
||||||
|
const bootLoader = () => {
|
||||||
|
const intent = resolveIntent();
|
||||||
|
const handoffTarget = resolveHandoffTarget(intent);
|
||||||
|
|
||||||
|
setStatus("Capturing route intent");
|
||||||
|
setIntentPreview(intent);
|
||||||
|
persistLoaderState(intent);
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
setStatus("Preparing handoff");
|
||||||
|
}, 180);
|
||||||
|
|
||||||
|
scheduleHandoff(handoffTarget, () => {
|
||||||
|
setStatus("Handing off to application");
|
||||||
|
markLoaderSeen();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
bootLoader();
|
||||||
24
Loader/scripts/storage.js
Normal file
24
Loader/scripts/storage.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Path: Loader/scripts/storage.js
|
||||||
|
|
||||||
|
import { LOADER_INTENT_STORAGE_KEY, LOADER_META_STORAGE_KEY, LOADER_SEEN_COOKIE_MAX_AGE_SECONDS, LOADER_SEEN_COOKIE_NAME } from "./config.js";
|
||||||
|
import { collectClientHints } from "./hints.js";
|
||||||
|
|
||||||
|
export const persistLoaderState = (intent) => {
|
||||||
|
const payload = {
|
||||||
|
intent,
|
||||||
|
hints: collectClientHints(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.sessionStorage.setItem(LOADER_INTENT_STORAGE_KEY, intent);
|
||||||
|
window.sessionStorage.setItem(LOADER_META_STORAGE_KEY, JSON.stringify(payload));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Loader] Unable to persist bootstrap state.", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markLoaderSeen = () => {
|
||||||
|
document.cookie = `${LOADER_SEEN_COOKIE_NAME}=1; Path=/; Max-Age=${LOADER_SEEN_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;
|
||||||
|
};
|
||||||
177
Loader/styles.css
Normal file
177
Loader/styles.css
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/* Path: Loader/styles.css */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--loader-bg: #07111f;
|
||||||
|
--loader-panel: rgba(15, 23, 42, 0.92);
|
||||||
|
--loader-panel-border: rgba(148, 163, 184, 0.18);
|
||||||
|
--loader-panel-highlight: rgba(255, 255, 255, 0.04);
|
||||||
|
--loader-text: #f8fafc;
|
||||||
|
--loader-text-muted: #94a3b8;
|
||||||
|
--loader-accent: #60a5fa;
|
||||||
|
--loader-accent-soft: rgba(96, 165, 250, 0.16);
|
||||||
|
--loader-shadow: 0 24px 64px rgba(2, 6, 23, 0.42);
|
||||||
|
--loader-radius: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100dvh;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
ui-sans-serif,
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
sans-serif;
|
||||||
|
background: radial-gradient(circle at top, rgba(37, 99, 235, 0.18), transparent 36%), linear-gradient(180deg, #091528 0%, var(--loader-bg) 100%);
|
||||||
|
color: var(--loader-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-shell {
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-panel {
|
||||||
|
width: min(100%, 30rem);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid var(--loader-panel-border);
|
||||||
|
border-radius: var(--loader-radius);
|
||||||
|
background: linear-gradient(180deg, var(--loader-panel-highlight), transparent 16%), var(--loader-panel);
|
||||||
|
box-shadow: var(--loader-shadow);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-eyebrow {
|
||||||
|
margin: 0 0 0.625rem;
|
||||||
|
color: var(--loader-text-muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.75rem, 5vw, 2.25rem);
|
||||||
|
line-height: 1.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-copy {
|
||||||
|
margin: 0.875rem 0 0;
|
||||||
|
max-width: 30ch;
|
||||||
|
color: var(--loader-text-muted);
|
||||||
|
font-size: 0.975rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-indicators {
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-dot {
|
||||||
|
width: 0.625rem;
|
||||||
|
height: 0.625rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--loader-accent-soft);
|
||||||
|
animation: loader-pulse 1.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-dot:nth-child(2) {
|
||||||
|
animation-delay: 0.18s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-dot:nth-child(3) {
|
||||||
|
animation-delay: 0.36s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-meta {
|
||||||
|
margin: 1.5rem 0 0;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid rgba(148, 163, 184, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-meta-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 4.5rem minmax(0, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-meta-row + .loader-meta-row {
|
||||||
|
margin-top: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-meta dt {
|
||||||
|
color: var(--loader-text-muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-meta dd {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--loader-text);
|
||||||
|
font-size: 0.925rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-noscript {
|
||||||
|
position: fixed;
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
left: 1rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
border: 1px solid rgba(248, 113, 113, 0.28);
|
||||||
|
border-radius: 0.875rem;
|
||||||
|
background: rgba(69, 10, 10, 0.92);
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loader-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
background: var(--loader-accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(-0.125rem);
|
||||||
|
background: var(--loader-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 47.99rem) {
|
||||||
|
.loader-shell {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-panel {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-meta-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
## =========================
|
||||||
|
|
||||||
|
ARG NGINX_VERSION=1.29.8
|
||||||
|
|
||||||
FROM node:22-alpine AS frontend-dependencies
|
FROM node:22-alpine AS frontend-dependencies
|
||||||
|
|
||||||
WORKDIR /workspace/Frontend
|
WORKDIR /workspace/Frontend
|
||||||
@@ -17,10 +22,77 @@ COPY Frontend/ ./
|
|||||||
|
|
||||||
RUN pnpm build:static
|
RUN pnpm build:static
|
||||||
|
|
||||||
FROM nginx:1.29-alpine AS production
|
## Proxy
|
||||||
|
## =========================
|
||||||
|
|
||||||
|
FROM alpine:3.22 AS precompressed-assets
|
||||||
|
|
||||||
|
RUN apk add --no-cache brotli gzip
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
COPY Loader ./html/__loader
|
||||||
|
COPY --from=frontend-build /workspace/Frontend/dist/client ./html
|
||||||
|
|
||||||
|
RUN find ./html -type f \( \
|
||||||
|
-name '*.html' -o \
|
||||||
|
-name '*.css' -o \
|
||||||
|
-name '*.js' -o \
|
||||||
|
-name '*.json' -o \
|
||||||
|
-name '*.svg' -o \
|
||||||
|
-name '*.txt' -o \
|
||||||
|
-name '*.xml' -o \
|
||||||
|
-name '*.webmanifest' \
|
||||||
|
\) -exec gzip -kf -9 {} \; \
|
||||||
|
&& find ./html -type f \( \
|
||||||
|
-name '*.html' -o \
|
||||||
|
-name '*.css' -o \
|
||||||
|
-name '*.js' -o \
|
||||||
|
-name '*.json' -o \
|
||||||
|
-name '*.svg' -o \
|
||||||
|
-name '*.txt' -o \
|
||||||
|
-name '*.xml' -o \
|
||||||
|
-name '*.webmanifest' \
|
||||||
|
\) -exec brotli -kf -Z {} \;
|
||||||
|
|
||||||
|
FROM alpine:3.22 AS brotli-modules
|
||||||
|
|
||||||
|
ARG NGINX_VERSION
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
alpine-sdk \
|
||||||
|
cmake \
|
||||||
|
git \
|
||||||
|
linux-headers \
|
||||||
|
openssl-dev \
|
||||||
|
pcre2-dev \
|
||||||
|
wget \
|
||||||
|
zlib-dev
|
||||||
|
|
||||||
|
WORKDIR /tmp
|
||||||
|
|
||||||
|
RUN wget -O "nginx-${NGINX_VERSION}.tar.gz" "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" \
|
||||||
|
&& tar -xzf "nginx-${NGINX_VERSION}.tar.gz" \
|
||||||
|
&& git clone --recurse-submodules https://github.com/google/ngx_brotli.git
|
||||||
|
|
||||||
|
WORKDIR /tmp/nginx-${NGINX_VERSION}
|
||||||
|
|
||||||
|
RUN cd /tmp/ngx_brotli/deps/brotli \
|
||||||
|
&& mkdir -p out \
|
||||||
|
&& cd out \
|
||||||
|
&& cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_C_FLAGS="-fPIC" -DCMAKE_CXX_FLAGS="-fPIC" .. \
|
||||||
|
&& cmake --build . --config Release -j"$(getconf _NPROCESSORS_ONLN)" \
|
||||||
|
&& cd /tmp/nginx-${NGINX_VERSION} \
|
||||||
|
&& ./configure --with-compat --add-dynamic-module=/tmp/ngx_brotli \
|
||||||
|
&& make modules
|
||||||
|
|
||||||
|
FROM nginx:${NGINX_VERSION}-alpine AS production
|
||||||
|
|
||||||
|
COPY Proxy/Local/nginx.conf /etc/nginx/nginx.conf
|
||||||
COPY Proxy/Local/default.conf /etc/nginx/conf.d/default.conf
|
COPY Proxy/Local/default.conf /etc/nginx/conf.d/default.conf
|
||||||
COPY --from=frontend-build /workspace/Frontend/dist/client /usr/share/nginx/html
|
COPY --from=brotli-modules /tmp/nginx-${NGINX_VERSION}/objs/ngx_http_brotli_filter_module.so /usr/lib/nginx/modules/
|
||||||
|
COPY --from=brotli-modules /tmp/nginx-${NGINX_VERSION}/objs/ngx_http_brotli_static_module.so /usr/lib/nginx/modules/
|
||||||
|
COPY --from=precompressed-assets /workspace/html /usr/share/nginx/html
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
map $http_cookie $moku_bootstrap_document {
|
||||||
|
default /__loader/index.html;
|
||||||
|
"~*(^|; )moku_loader_seen=1($|;)" /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
@@ -5,8 +10,19 @@ server {
|
|||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
|
location = / {
|
||||||
|
add_header Cache-Control "no-store";
|
||||||
|
rewrite ^ $moku_bootstrap_document last;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /__loader/ {
|
||||||
|
access_log off;
|
||||||
|
add_header Cache-Control "no-store";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ $moku_bootstrap_document;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /favicon.ico {
|
location /favicon.ico {
|
||||||
|
|||||||
60
Proxy/Local/nginx.conf
Normal file
60
Proxy/Local/nginx.conf
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
load_module /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so;
|
||||||
|
load_module /usr/lib/nginx/modules/ngx_http_brotli_static_module.so;
|
||||||
|
|
||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/error.log notice;
|
||||||
|
pid /run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_static on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/manifest+json
|
||||||
|
application/xml
|
||||||
|
application/rss+xml
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
brotli on;
|
||||||
|
brotli_static on;
|
||||||
|
brotli_comp_level 5;
|
||||||
|
brotli_min_length 1024;
|
||||||
|
brotli_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/manifest+json
|
||||||
|
application/xml
|
||||||
|
application/rss+xml
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user