mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
A pass over every file the branch touched, after the iOS animation hunt left tries and reverts behind it. Removed what nothing reaches: the view-transition rule for a transition no code starts, an unreachable guard in the transition hook, a no-op KeepAlive key branch, a sign-in guard the menu already enforces, usage-dialog wrappers that only forwarded to the store, a duplicated modifier-click check and card filter (isPlainClick / listedTools), a test that hand-rolled listedTools, a drawer-era fixture parameter. auth-flow.js folded into auth-redirect.js. Comments cut to what the code cannot say itself — file headers of a few lines, the measured iOS reasons, non-obvious orderings — and stale ones fixed (Nav under KeepAlive, Home.vue reusing DEFAULT_META, "from Nav", enabledCards). AGENTS.md sections tightened to the code as it is now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
279 lines
10 KiB
JavaScript
279 lines
10 KiB
JavaScript
import dotenv, { parse } from 'dotenv';
|
|
import { defineConfig } from 'vite'
|
|
import vue from '@vitejs/plugin-vue'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
import { CodeInspectorPlugin } from 'code-inspector-plugin';
|
|
import { sentryVitePlugin } from '@sentry/vite-plugin';
|
|
import { PREFS_STORAGE_KEY } from './frontend/data/default-preferences.js';
|
|
import { LOCALE_CODES } from './common/locale-registry.js';
|
|
import { stripPack } from './common/locale-pack.js';
|
|
import { FIREBASE_AUTH_PROXY_PATH, firebaseAuthProxyTarget } from './common/firebase-auth-proxy.js';
|
|
|
|
dotenv.config();
|
|
|
|
// Sentry source-map upload — build-time only. Needs SENTRY_AUTH_TOKEN and a
|
|
// production SENTRY_ENVIRONMENT (unset means production, matching the
|
|
// backend convention) — dev/test builds neither generate nor upload maps.
|
|
// Maps go to Sentry, then get deleted from dist/ — users never download them.
|
|
const sentryUploadEnabled = !!process.env.SENTRY_AUTH_TOKEN
|
|
&& (process.env.SENTRY_ENVIRONMENT || 'production') === 'production';
|
|
|
|
const backEndPort = parseInt(process.env.BACKEND_PORT || 11966, 10);
|
|
const frontEndPort = parseInt(process.env.FRONTEND_PORT || 18966, 10);
|
|
const firebaseAuthTarget = firebaseAuthProxyTarget(process.env.VITE_FIREBASE_PROJECT_ID);
|
|
const nodeModuleChunkGroups = {
|
|
vendor: ['vue', 'vue-router', 'vue-i18n'],
|
|
chart: ['chart.js'],
|
|
speedtest: ['@cloudflare/speedtest'],
|
|
'browser-detect': ['@thumbmarkjs/thumbmarkjs', 'ua-parser-js'],
|
|
};
|
|
|
|
const sourceChunkGroups = {
|
|
'utils-getips': [
|
|
'/frontend/utils/getips/index',
|
|
'/frontend/utils/ip/valid-ip',
|
|
'/frontend/utils/ip/transform-ip-data'
|
|
],
|
|
'utils-auth': [
|
|
'/frontend/utils/authenticated-fetch'
|
|
]
|
|
};
|
|
|
|
function isNodePackage(normalizedId, packageName) {
|
|
const nodeModulesPath = '/node_modules/';
|
|
const nodeModulesIndex = normalizedId.lastIndexOf(nodeModulesPath);
|
|
|
|
if (nodeModulesIndex === -1) {
|
|
return false;
|
|
}
|
|
|
|
const packagePath = normalizedId.slice(nodeModulesIndex + nodeModulesPath.length);
|
|
return packagePath === packageName || packagePath.startsWith(`${packageName}/`);
|
|
}
|
|
|
|
// `index.html` contains a small conditional block delimited by
|
|
// `<!-- @site-url:open -->` … `<!-- @site-url:close -->` and uses
|
|
// `__SITE_URL__` as the placeholder for an absolute origin.
|
|
function siteUrlHtmlPlugin() {
|
|
const siteUrl = (process.env.VITE_SITE_URL || '').trim().replace(/\/+$/, '');
|
|
const blockRe = /[ \t]*<!--\s*@site-url:open\s*-->[\s\S]*?<!--\s*@site-url:close\s*-->\n?/g;
|
|
const markerOpenRe = /[ \t]*<!--\s*@site-url:open\s*-->\n?/g;
|
|
const markerCloseRe = /[ \t]*<!--\s*@site-url:close\s*-->\n?/g;
|
|
return {
|
|
name: 'site-url-html',
|
|
transformIndexHtml: {
|
|
order: 'pre',
|
|
handler(html) {
|
|
if (!siteUrl) return html.replace(blockRe, '');
|
|
return html
|
|
.replace(markerOpenRe, '')
|
|
.replace(markerCloseRe, '')
|
|
.replaceAll('__SITE_URL__', siteUrl);
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
// Locale packs mark untranslated strings with "" (TRANSLATING.md). Dropping
|
|
// them here — ahead of the built-in JSON handling, hence `enforce: 'pre'` —
|
|
// is what makes "" behave like an absent key at runtime: the app never holds
|
|
// one, so the i18n fallback chain resolves it. The whole folder is in scope;
|
|
// the convention belongs to locale packs, not to whichever file uses it today.
|
|
const localeStripPlugin = () => ({
|
|
name: 'locale-strip-untranslated',
|
|
enforce: 'pre',
|
|
transform(code, id) {
|
|
const file = id.replaceAll('\\', '/').split('?')[0];
|
|
if (!file.includes('/frontend/locales/') || !file.endsWith('.json')) return null;
|
|
return { code: JSON.stringify(stripPack(JSON.parse(code))), map: null };
|
|
},
|
|
});
|
|
|
|
// Build-only inline script that modulepreloads the visitor's locale pack.
|
|
// Mount waits on the active locale's messages, but their dynamic import can
|
|
// only start after index.js has downloaded and executed — a serialized
|
|
// round-trip on the boot critical path. This plugin finds the emitted
|
|
// locale-pack chunks in the bundle and injects a small head script that
|
|
// picks the language exactly like locales/i18n.js (stored prefs →
|
|
// ?hl= → browser language → en) and appends a
|
|
// <link rel="modulepreload"> for every pack on its fallback chain — the
|
|
// same set mount awaits — while the HTML is still streaming; the
|
|
// packs then download in parallel with the main bundle. A wrong pick only
|
|
// wastes one preload; the real import decides. Dev serves no bundle, so
|
|
// nothing is injected there.
|
|
const localePreloadPlugin = () => {
|
|
const preloadScript = (chunks) => `(function () {
|
|
var chunks = ${JSON.stringify(chunks)};
|
|
// Mirrors matchLocale() in common/locale-registry.js.
|
|
var match = function (tag) {
|
|
var wanted = String(tag || '').toLowerCase();
|
|
if (!wanted) return null;
|
|
var codes = Object.keys(chunks);
|
|
for (var i = 0; i < codes.length; i++) if (codes[i].toLowerCase() === wanted) return codes[i];
|
|
var base = wanted.split('-')[0];
|
|
for (var j = 0; j < codes.length; j++) if (codes[j].toLowerCase() === base) return codes[j];
|
|
for (var k = 0; k < codes.length; k++) if (codes[k].toLowerCase().split('-')[0] === base) return codes[k];
|
|
return null;
|
|
};
|
|
var lang = null;
|
|
try {
|
|
var stored = JSON.parse(localStorage.getItem(${JSON.stringify(PREFS_STORAGE_KEY)}) || '{}').lang;
|
|
if (chunks[stored]) lang = stored;
|
|
} catch (e) { /* malformed entry — fall through to the default pick */ }
|
|
if (!lang) {
|
|
var hl = new URLSearchParams(location.search).get('hl');
|
|
lang = match(hl || navigator.language) || 'en';
|
|
}
|
|
// Same chain as fallbackChain(): variant, base, en.
|
|
var chain = [lang];
|
|
var base = lang.split('-')[0];
|
|
if (base !== lang && chunks[base]) chain.push(base);
|
|
if (chain.indexOf('en') === -1) chain.push('en');
|
|
chain.forEach(function (l) {
|
|
if (!chunks[l]) return;
|
|
var link = document.createElement('link');
|
|
link.rel = 'modulepreload';
|
|
link.crossOrigin = '';
|
|
link.href = chunks[l];
|
|
document.head.appendChild(link);
|
|
});
|
|
})();`;
|
|
|
|
return {
|
|
name: 'locale-preload',
|
|
transformIndexHtml: {
|
|
order: 'post',
|
|
handler(html, ctx) {
|
|
const chunks = {};
|
|
for (const [fileName, chunk] of Object.entries(ctx.bundle || {})) {
|
|
if (chunk.type !== 'chunk') continue;
|
|
const facade = (chunk.facadeModuleId || '').replaceAll('\\', '/');
|
|
// Optional region subtag leaves room for `zh-TW`; the privacy /
|
|
// security-checklist packs sit a folder deeper and never match.
|
|
const match = facade.match(/\/frontend\/locales\/([a-z]{2}(?:-[A-Za-z]{2,4})?)\.json$/);
|
|
if (match) chunks[match[1]] = '/' + fileName;
|
|
}
|
|
// Registry order, so the sibling `match()` settles on is the one the
|
|
// app itself would pick.
|
|
const ordered = {};
|
|
for (const code of LOCALE_CODES) if (chunks[code]) ordered[code] = chunks[code];
|
|
if (Object.keys(ordered).length === 0) return html;
|
|
return {
|
|
html,
|
|
tags: [{ tag: 'script', children: preloadScript(ordered), injectTo: 'head' }],
|
|
};
|
|
},
|
|
},
|
|
};
|
|
};
|
|
|
|
function manualChunks(id) {
|
|
const normalizedId = id.replaceAll('\\', '/');
|
|
|
|
for (const [chunkName, packages] of Object.entries(nodeModuleChunkGroups)) {
|
|
if (packages.some((packageName) => isNodePackage(normalizedId, packageName))) {
|
|
return chunkName;
|
|
}
|
|
}
|
|
|
|
for (const [chunkName, modules] of Object.entries(sourceChunkGroups)) {
|
|
if (modules.some((moduleName) => normalizedId.includes(moduleName))) {
|
|
return chunkName;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export default defineConfig({
|
|
plugins: [
|
|
vue({
|
|
template: {
|
|
compilerOptions: {
|
|
isCustomElement: (tag) => tag === 'pwa-install'
|
|
}
|
|
}
|
|
}),
|
|
tailwindcss(),
|
|
siteUrlHtmlPlugin(),
|
|
localeStripPlugin(),
|
|
localePreloadPlugin(),
|
|
CodeInspectorPlugin({
|
|
bundler: 'vite',
|
|
hideDomPathAttr: true,
|
|
behavior: {
|
|
copy: '{file}',
|
|
},
|
|
}),
|
|
// Must stay last so it sees the final emitted chunks. Release name
|
|
// defaults to the current git commit SHA; the plugin injects the same
|
|
// release into the SDK at runtime, tying events to their sourcemaps.
|
|
sentryUploadEnabled && sentryVitePlugin({
|
|
org: process.env.SENTRY_ORG,
|
|
project: process.env.SENTRY_PROJECT_FRONTEND,
|
|
authToken: process.env.SENTRY_AUTH_TOKEN,
|
|
telemetry: false,
|
|
sourcemaps: {
|
|
// Rolldown's module-loading runtime chunk has no source to map —
|
|
// skip it so every build doesn't warn about its missing .map
|
|
ignore: ['**/rolldown-runtime*.js'],
|
|
filesToDeleteAfterUpload: ['dist/**/*.map'],
|
|
},
|
|
}),
|
|
],
|
|
resolve: {
|
|
alias: {
|
|
'@': '/frontend',
|
|
},
|
|
},
|
|
build: {
|
|
// Hidden sourcemaps exist only for the Sentry upload; without a token,
|
|
// don't generate them at all (they'd end up publicly served from dist/)
|
|
sourcemap: sentryUploadEnabled ? 'hidden' : false,
|
|
rollupOptions: {
|
|
// @vueuse/core ships a couple of /* #__PURE__ */ comments in positions
|
|
// Rolldown (Vite 8's new bundler) can't interpret. They're harmless —
|
|
// just slightly less aggressive tree-shaking for those call sites —
|
|
// but the warnings clutter every build. Filter them out and let
|
|
// everything else through. Drop this once @vueuse/core fixes positions.
|
|
onwarn(warning, defaultHandler) {
|
|
if (warning.code === 'INVALID_ANNOTATION' && warning.id?.includes('@vueuse/core')) {
|
|
return;
|
|
}
|
|
defaultHandler(warning);
|
|
},
|
|
output: {
|
|
manualChunks,
|
|
assetFileNames: (assetInfo) => {
|
|
if (assetInfo.fileName && (assetInfo.fileName.endsWith('.woff') || assetInfo.fileName.endsWith('.woff2'))) {
|
|
return 'fonts/[name][extname]';
|
|
}
|
|
return 'assets/[name]-[hash][extname]';
|
|
},
|
|
chunkFileNames: (chunkInfo) => {
|
|
const prefix = chunkInfo.name.startsWith('utils-') ? 'utils/' : '';
|
|
return `assets/${prefix}[name].[hash].js`;
|
|
}
|
|
}
|
|
},
|
|
chunkSizeWarningLimit: 1000,
|
|
splitChunks: {
|
|
chunks: 'all',
|
|
minSize: 20000,
|
|
maxSize: 500000,
|
|
}
|
|
},
|
|
server: {
|
|
host: '0.0.0.0',
|
|
port: frontEndPort,
|
|
proxy: {
|
|
'/api': `http://localhost:${backEndPort}`,
|
|
// Firebase Auth handler, as frontend-server.js serves it in production
|
|
...(firebaseAuthTarget && {
|
|
[FIREBASE_AUTH_PROXY_PATH]: { target: firebaseAuthTarget, changeOrigin: true },
|
|
}),
|
|
},
|
|
allowedHosts: ['dev.ipcheck.ing', 'test.ipcheck.ing'],
|
|
}
|
|
})
|