u
This commit is contained in:
32
package-lock.json
generated
32
package-lock.json
generated
@@ -10,6 +10,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@jsquash/avif": "^2.1.1",
|
||||||
|
"@jsquash/resize": "^2.1.1",
|
||||||
"@mui/icons-material": "^9.2.0",
|
"@mui/icons-material": "^9.2.0",
|
||||||
"@mui/material": "^9.2.0",
|
"@mui/material": "^9.2.0",
|
||||||
"@reduxjs/toolkit": "^2.12.0",
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
@@ -17,7 +19,8 @@
|
|||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
"react-router-dom": "^7.18.2"
|
"react-router-dom": "^7.18.2",
|
||||||
|
"thumbhash": "^0.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "6.0.2",
|
"@vitejs/plugin-react": "6.0.2",
|
||||||
@@ -1828,6 +1831,21 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jsquash/avif": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jsquash/avif/-/avif-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-LMRxd0fMgfCLtobDh0/sFYJMMiRJTNYSEEWvRDKXlAeZ08t3gI5V+1thIT0XjXJ+SVG7Zug9B0XPyx0Ti5VRNA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"wasm-feature-detect": "^1.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jsquash/resize": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jsquash/resize/-/resize-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-0R5UL1ZLHUT+carjVikcE1QfA+kfNQ2YamYyGVRmhfh4zttU5EY3bQBGxPIPtY2xIAw1P4Kgyxm2xrceRw1r2w==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@mui/core-downloads-tracker": {
|
"node_modules/@mui/core-downloads-tracker": {
|
||||||
"version": "9.2.0",
|
"version": "9.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.2.0.tgz",
|
||||||
@@ -6712,6 +6730,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/thumbhash": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/thumbhash/-/thumbhash-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
@@ -7084,6 +7108,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wasm-feature-detect": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@jsquash/avif": "^2.1.1",
|
||||||
|
"@jsquash/resize": "^2.1.1",
|
||||||
"@mui/icons-material": "^9.2.0",
|
"@mui/icons-material": "^9.2.0",
|
||||||
"@mui/material": "^9.2.0",
|
"@mui/material": "^9.2.0",
|
||||||
"@reduxjs/toolkit": "^2.12.0",
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
@@ -23,7 +25,8 @@
|
|||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
"react-router-dom": "^7.18.2"
|
"react-router-dom": "^7.18.2",
|
||||||
|
"thumbhash": "^0.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "6.0.2",
|
"@vitejs/plugin-react": "6.0.2",
|
||||||
|
|||||||
BIN
public/fixtures/from-payload.png
Normal file
BIN
public/fixtures/from-payload.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 281 B |
BIN
public/fixtures/sample-article.png
Normal file
BIN
public/fixtures/sample-article.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
public/fixtures/thumbhash-decoded-200.png
Normal file
BIN
public/fixtures/thumbhash-decoded-200.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/fixtures/thumbhash-internal-resolution.png
Normal file
BIN
public/fixtures/thumbhash-internal-resolution.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -8,7 +8,7 @@ function logApi(tag, details) {
|
|||||||
} catch {
|
} catch {
|
||||||
text = String(details);
|
text = String(details);
|
||||||
}
|
}
|
||||||
console.log(`[api] ${tag} ${text}`);
|
console[tag === 'error-body' ? 'error' : 'log'](`[api] ${tag} ${text}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readableHeaders(headers) {
|
function readableHeaders(headers) {
|
||||||
@@ -81,7 +81,7 @@ async function http(path, { method = 'GET', body } = {}) {
|
|||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function httpMultipart(path, { method = 'POST', formData } = {}) {
|
async function httpBinary(path, { method = 'POST', body, contentType } = {}) {
|
||||||
const url = new URL(path, window.location.origin).href;
|
const url = new URL(path, window.location.origin).href;
|
||||||
|
|
||||||
logApi('request', {
|
logApi('request', {
|
||||||
@@ -90,13 +90,15 @@ async function httpMultipart(path, { method = 'POST', formData } = {}) {
|
|||||||
url,
|
url,
|
||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
multipart: true,
|
contentType,
|
||||||
|
bodyBytes: body?.byteLength ?? body?.size ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await fetch(path, {
|
const res = await fetch(path, {
|
||||||
method,
|
method,
|
||||||
|
headers: { 'Content-Type': contentType },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: formData,
|
body,
|
||||||
});
|
});
|
||||||
|
|
||||||
const responseHeaders = readableHeaders(res.headers);
|
const responseHeaders = readableHeaders(res.headers);
|
||||||
@@ -176,17 +178,15 @@ export const article = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/article/:sku/photos — multipart/form-data
|
* POST /api/article/:sku/image
|
||||||
* Body: file (image/avif, max 1000×1000, prepared client-side), optional sort (default 0)
|
* Body: raw AVIF bytes (Content-Type: image/avif, max 1000×1000, prepared client-side via WASM)
|
||||||
* → 201 { article }
|
* → 201 { article }
|
||||||
*/
|
*/
|
||||||
uploadPhoto(sku, file, { sort = 0 } = {}) {
|
uploadImage(sku, file) {
|
||||||
const formData = new FormData();
|
return httpBinary(`/api/article/${encodeURIComponent(sku)}/image`, {
|
||||||
formData.append('file', file);
|
|
||||||
formData.append('sort', String(sort));
|
|
||||||
return httpMultipart(`/api/article/${encodeURIComponent(sku)}/photos`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
formData,
|
contentType: 'image/avif',
|
||||||
|
body: file,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { bootstrapSession as bootstrapSessionThunk } from '../store/authSlice';
|
import { notifyError } from '../utils/notify';
|
||||||
|
|
||||||
let sessionBootstrapped = false;
|
let sessionBootstrapped = false;
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ class AuthBootstrap extends Component {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
enqueueSnackbar(`Session check failed: ${err.message}`, { variant: 'error' });
|
notifyError(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { connect } from 'react-redux';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import GoogleIcon from '@mui/icons-material/Google';
|
import GoogleIcon from '@mui/icons-material/Google';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { notifyErrorMessage } from '../utils/notify';
|
||||||
import { loginWithGoogle, selectAuthStatus, selectGoogleClientId } from '../store/authSlice';
|
|
||||||
import {
|
import {
|
||||||
abortGoogleSignIn,
|
abortGoogleSignIn,
|
||||||
FEDCM_BLOCKED_MESSAGE,
|
FEDCM_BLOCKED_MESSAGE,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
|||||||
import { article } from '../../api';
|
import { article } from '../../api';
|
||||||
import { PanelTile } from '../panels';
|
import { PanelTile } from '../panels';
|
||||||
import { requireObject } from '../../utils/apiShape';
|
import { requireObject } from '../../utils/apiShape';
|
||||||
|
import { notifyError } from '../../utils/notify';
|
||||||
import { prepareArticlePhoto } from '../../utils/prepareArticlePhoto';
|
import { prepareArticlePhoto } from '../../utils/prepareArticlePhoto';
|
||||||
import { pasteImageOrFallback } from '../../utils/pasteImage';
|
import { pasteImageOrFallback } from '../../utils/pasteImage';
|
||||||
import { primaryPhoto } from '../../utils/articleDetail';
|
import { primaryPhoto } from '../../utils/articleDetail';
|
||||||
@@ -25,10 +26,12 @@ export default function ArticlePhotoTile({
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const file = await prepareArticlePhoto(rawBlob);
|
const file = await prepareArticlePhoto(rawBlob);
|
||||||
const res = await article.uploadPhoto(articleData.sku, file);
|
const res = await article.uploadImage(articleData.sku, file);
|
||||||
requireObject(res, 'POST /api/article/:sku/photos');
|
requireObject(res, 'POST /api/article/:sku/image');
|
||||||
requireObject(res.article, 'article');
|
requireObject(res.article, 'article');
|
||||||
onArticleUpdated?.(res.article);
|
onArticleUpdated?.(res.article);
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Paper from '@mui/material/Paper';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { injectMuiTheme } from './injectMuiTheme';
|
import { injectMuiTheme } from './injectMuiTheme';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { notifyError } from '../../utils/notify';
|
||||||
import { panelTilePaperSx, TILE_ACTION_PADDING, tileActionIconSx, tileActionSx } from './panelTileLayout';
|
import { panelTilePaperSx, TILE_ACTION_PADDING, tileActionIconSx, tileActionSx } from './panelTileLayout';
|
||||||
|
|
||||||
function valuesEqual(a, b) {
|
function valuesEqual(a, b) {
|
||||||
@@ -113,9 +113,8 @@ class EditablePanelTileBase extends Component {
|
|||||||
this.setState({ editing: false, draft: null, saving: false, error: null });
|
this.setState({ editing: false, draft: null, saving: false, error: null });
|
||||||
enqueueSnackbar('Gespeichert.', { variant: 'success' });
|
enqueueSnackbar('Gespeichert.', { variant: 'success' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const normalized = backendErrorFrom(err);
|
const normalized = notifyError(err);
|
||||||
this.setState({ saving: false, error: normalized.message });
|
this.setState({ saving: false, error: normalized.message });
|
||||||
enqueueSnackbar(normalized.message, { variant: 'error' });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ createRoot(document.getElementById('root')).render(
|
|||||||
<SnackbarProvider
|
<SnackbarProvider
|
||||||
maxSnack={3}
|
maxSnack={3}
|
||||||
autoHideDuration={2000}
|
autoHideDuration={2000}
|
||||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||||
>
|
>
|
||||||
<AuthBootstrap>
|
<AuthBootstrap>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
20
src/utils/notify.js
Normal file
20
src/utils/notify.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
import { backendErrorFrom } from './backendError';
|
||||||
|
|
||||||
|
/** Show error toast and log to console for debugging. */
|
||||||
|
export function notifyError(err) {
|
||||||
|
const normalized = backendErrorFrom(err);
|
||||||
|
console.error('[error]', normalized.message, {
|
||||||
|
status: normalized.status,
|
||||||
|
path: normalized.path,
|
||||||
|
method: normalized.method,
|
||||||
|
err,
|
||||||
|
});
|
||||||
|
enqueueSnackbar(normalized.message, { variant: 'error' });
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notifyErrorMessage(message) {
|
||||||
|
console.error('[error]', message);
|
||||||
|
enqueueSnackbar(message, { variant: 'error' });
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
const MAX_EDGE = 1000;
|
const MAX_EDGE = 1000;
|
||||||
const AVIF_MIME = 'image/avif';
|
const AVIF_MIME = 'image/avif';
|
||||||
const AVIF_QUALITY = 0.8;
|
const AVIF_QUALITY = 80;
|
||||||
|
|
||||||
|
let encodeAvif;
|
||||||
|
let resizeImage;
|
||||||
|
|
||||||
function scaledDimensions(width, height) {
|
function scaledDimensions(width, height) {
|
||||||
const scale = Math.min(1, MAX_EDGE / width, MAX_EDGE / height);
|
const scale = Math.min(1, MAX_EDGE / width, MAX_EDGE / height);
|
||||||
@@ -10,8 +13,43 @@ function scaledDimensions(width, height) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getAvifEncoder() {
|
||||||
|
if (!encodeAvif) {
|
||||||
|
const { default: encode } = await import('@jsquash/avif/encode');
|
||||||
|
encodeAvif = encode;
|
||||||
|
}
|
||||||
|
return encodeAvif;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getResizer() {
|
||||||
|
if (!resizeImage) {
|
||||||
|
const { default: resize } = await import('@jsquash/resize');
|
||||||
|
resizeImage = resize;
|
||||||
|
}
|
||||||
|
return resizeImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decodeToImageData(input) {
|
||||||
|
const bitmap = await createImageBitmap(input);
|
||||||
|
try {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = bitmap.width;
|
||||||
|
canvas.height = bitmap.height;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('Canvas nicht verfügbar.');
|
||||||
|
}
|
||||||
|
ctx.drawImage(bitmap, 0, 0);
|
||||||
|
return ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
||||||
|
} finally {
|
||||||
|
bitmap.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resize to fit within 1000×1000 (aspect ratio kept, no upscale) and encode as AVIF.
|
* Resize to fit within 1000×1000 (aspect ratio kept, no upscale).
|
||||||
|
* Encodes as AVIF via @jsquash/avif (WASM).
|
||||||
* @param {Blob|File} input
|
* @param {Blob|File} input
|
||||||
* @returns {Promise<File>}
|
* @returns {Promise<File>}
|
||||||
*/
|
*/
|
||||||
@@ -23,39 +61,21 @@ export async function prepareArticlePhoto(input) {
|
|||||||
throw new Error('Kein Bild.');
|
throw new Error('Kein Bild.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const bitmap = await createImageBitmap(input);
|
const originalImageData = await decodeToImageData(input);
|
||||||
try {
|
const { width, height } = scaledDimensions(
|
||||||
const { width, height } = scaledDimensions(bitmap.width, bitmap.height);
|
originalImageData.width,
|
||||||
const canvas = document.createElement('canvas');
|
originalImageData.height,
|
||||||
canvas.width = width;
|
);
|
||||||
canvas.height = height;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const resize = await getResizer();
|
||||||
if (!ctx) {
|
const imageData = await resize(originalImageData, { width, height });
|
||||||
throw new Error('Canvas nicht verfügbar.');
|
|
||||||
}
|
|
||||||
ctx.drawImage(bitmap, 0, 0, width, height);
|
|
||||||
|
|
||||||
const avifBlob = await new Promise((resolve, reject) => {
|
const encode = await getAvifEncoder();
|
||||||
canvas.toBlob(
|
const avifBuffer = await encode(imageData, { quality: AVIF_QUALITY });
|
||||||
(blob) => {
|
|
||||||
if (!blob || blob.type !== AVIF_MIME) {
|
|
||||||
reject(new Error('AVIF wird von diesem Browser nicht unterstützt.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(blob);
|
|
||||||
},
|
|
||||||
AVIF_MIME,
|
|
||||||
AVIF_QUALITY,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const baseName = input instanceof File && input.name
|
const baseName = input instanceof File && input.name
|
||||||
? input.name.replace(/\.[^.]+$/, '')
|
? input.name.replace(/\.[^.]+$/, '')
|
||||||
: 'photo';
|
: 'photo';
|
||||||
|
|
||||||
return new File([avifBlob], `${baseName}.avif`, { type: AVIF_MIME });
|
return new File([avifBuffer], `${baseName}.avif`, { type: AVIF_MIME });
|
||||||
} finally {
|
|
||||||
bitmap.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,31 @@ function truncate(text, max = 500) {
|
|||||||
return `${s.slice(0, max)}… (${s.length} chars)`;
|
return `${s.slice(0, max)}… (${s.length} chars)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLoggedBody(statusCode, body) {
|
function isBinaryContentType(contentType = '') {
|
||||||
|
const type = contentType.split(';')[0].trim().toLowerCase();
|
||||||
|
if (!type) return false;
|
||||||
|
if (type.startsWith('image/')) return true;
|
||||||
|
if (type === 'application/octet-stream') return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRequestBody(contentType, buffer) {
|
||||||
|
if (!buffer || buffer.length === 0) return '(empty)';
|
||||||
|
if (isBinaryContentType(contentType)) {
|
||||||
|
const type = contentType.split(';')[0].trim();
|
||||||
|
return `(binary ${type}, ${buffer.length} bytes)`;
|
||||||
|
}
|
||||||
|
return truncate(buffer.toString('utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatResponseBody(statusCode, contentType, buffer) {
|
||||||
if (statusCode === 304) return '(304 no body)';
|
if (statusCode === 304) return '(304 no body)';
|
||||||
if (statusCode === 204 || body === '') return '(empty)';
|
if (statusCode === 204 || !buffer || buffer.length === 0) return '(empty)';
|
||||||
return truncate(body);
|
if (isBinaryContentType(contentType)) {
|
||||||
|
const type = contentType.split(';')[0].trim();
|
||||||
|
return `(binary ${type}, ${buffer.length} bytes)`;
|
||||||
|
}
|
||||||
|
return truncate(buffer.toString('utf8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasRequestBody(method) {
|
function hasRequestBody(method) {
|
||||||
@@ -57,9 +78,11 @@ const sharedServer = {
|
|||||||
const chunks = [];
|
const chunks = [];
|
||||||
req.on('data', (chunk) => chunks.push(chunk));
|
req.on('data', (chunk) => chunks.push(chunk));
|
||||||
req.on('end', () => {
|
req.on('end', () => {
|
||||||
req._proxyRequestBody = chunks.length
|
const buffer = chunks.length ? Buffer.concat(chunks) : null;
|
||||||
? Buffer.concat(chunks).toString('utf8')
|
req._proxyRequestBody = formatRequestBody(
|
||||||
: '';
|
req.headers['content-type'] ?? '',
|
||||||
|
buffer,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,9 +98,9 @@ const sharedServer = {
|
|||||||
const responseChunks = [];
|
const responseChunks = [];
|
||||||
proxyRes.on('data', (chunk) => responseChunks.push(chunk));
|
proxyRes.on('data', (chunk) => responseChunks.push(chunk));
|
||||||
proxyRes.on('end', () => {
|
proxyRes.on('end', () => {
|
||||||
const responseBody = responseChunks.length
|
const responseBuffer = responseChunks.length
|
||||||
? Buffer.concat(responseChunks).toString('utf8')
|
? Buffer.concat(responseChunks)
|
||||||
: '';
|
: null;
|
||||||
const parts = [
|
const parts = [
|
||||||
'[vite-proxy]',
|
'[vite-proxy]',
|
||||||
req.method,
|
req.method,
|
||||||
@@ -85,14 +108,16 @@ const sharedServer = {
|
|||||||
String(proxyRes.statusCode),
|
String(proxyRes.statusCode),
|
||||||
];
|
];
|
||||||
if (hasRequestBody(req.method)) {
|
if (hasRequestBody(req.method)) {
|
||||||
parts.push(
|
parts.push(`body=${req._proxyRequestBody ?? '(empty)'}`);
|
||||||
`body=${req._proxyRequestBody ? truncate(req._proxyRequestBody) : '(empty)'}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (cookies?.length) {
|
if (cookies?.length) {
|
||||||
parts.push(`set-cookie=${cookies.map((c) => c.split(';')[0]).join(',')}`);
|
parts.push(`set-cookie=${cookies.map((c) => c.split(';')[0]).join(',')}`);
|
||||||
}
|
}
|
||||||
parts.push(formatLoggedBody(proxyRes.statusCode, responseBody));
|
parts.push(formatResponseBody(
|
||||||
|
proxyRes.statusCode,
|
||||||
|
proxyRes.headers['content-type'] ?? '',
|
||||||
|
responseBuffer,
|
||||||
|
));
|
||||||
console.log(parts.join(' '));
|
console.log(parts.join(' '));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -115,6 +140,10 @@ export default defineConfig(({ command }) => ({
|
|||||||
name: 'mui-vendor',
|
name: 'mui-vendor',
|
||||||
test: /node_modules\/(@mui|@emotion)\//,
|
test: /node_modules\/(@mui|@emotion)\//,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'jsquash-image',
|
||||||
|
test: /node_modules\/@jsquash\/(avif|resize)\//,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -157,6 +186,7 @@ export default defineConfig(({ command }) => ({
|
|||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,webmanifest,woff2}'],
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,webmanifest,woff2}'],
|
||||||
|
globIgnores: ['**/*.wasm'],
|
||||||
navigateFallback: 'index.html',
|
navigateFallback: 'index.html',
|
||||||
navigateFallbackDenylist: [/^\/api/],
|
navigateFallbackDenylist: [/^\/api/],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user