chore: import upstream snapshot with attribution
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
function listCodeFiles(dir) {
|
||||
const out = [];
|
||||
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (ent.name === "node_modules" || ent.name === ".next") continue;
|
||||
const full = path.join(dir, ent.name);
|
||||
if (ent.isDirectory()) out.push(...listCodeFiles(full));
|
||||
else if (ent.isFile() && ent.name.endsWith(".tsx")) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function toRel(p, root) {
|
||||
return path.relative(root, p).replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
function hasUiText(s) {
|
||||
// Very rough heuristic: letters / CJK / common punctuation sequences
|
||||
return /[A-Za-z\u4e00-\u9fff]/.test(s);
|
||||
}
|
||||
|
||||
function auditFile(content) {
|
||||
const findings = [];
|
||||
|
||||
// JSXText: > ... <
|
||||
// Avoid matching tags like ></ by requiring at least one non-whitespace char.
|
||||
const jsxTextRe = />\s*([^<{][^<]*?)\s*</g;
|
||||
for (const m of content.matchAll(jsxTextRe)) {
|
||||
const text = String(m[1] || "").trim();
|
||||
if (!text) continue;
|
||||
// Heuristics to avoid false positives (code / comments / long blocks)
|
||||
if (text.includes("\n") || text.includes("\r")) continue;
|
||||
if (text.length > 120) continue;
|
||||
if (text.includes("{") || text.includes("}") || text.includes("/*") || text.includes("*/"))
|
||||
continue;
|
||||
if (text.includes("=>") || text.includes("export ") || text.includes("import "))
|
||||
continue;
|
||||
// Ternary/JS fragments that often get captured by regex formatting
|
||||
if ((text.includes("?") || text.includes(":")) && (text.includes("(") || text.includes(")")))
|
||||
continue;
|
||||
if (text.startsWith(")")) continue;
|
||||
if (text.includes("&&") || text.includes("= ") || text.startsWith("=")) continue;
|
||||
if (text.includes("mark.") || text.includes("diff")) continue;
|
||||
if (text.includes(">/i") || text.includes("katex")) continue;
|
||||
// Common non-translatable tokens / file extensions / escapes
|
||||
if (text === ".md" || text === ".pdf" || text === "\\n") continue;
|
||||
if (text === "DeepTutor") continue;
|
||||
// Ignore obvious already-i18n'd inline markers
|
||||
if (text.includes('t("') || text.includes("t('")) continue;
|
||||
if (!hasUiText(text)) continue;
|
||||
// Skip single-char separators
|
||||
if (text.length <= 1) continue;
|
||||
findings.push({ kind: "jsxText", text });
|
||||
}
|
||||
|
||||
// Attributes with literal string values
|
||||
const attrRe =
|
||||
/\b(title|placeholder|alt|aria-label)\s*=\s*"([^"]+)"/g;
|
||||
for (const m of content.matchAll(attrRe)) {
|
||||
const attr = m[1];
|
||||
const text = m[2];
|
||||
if (!text) continue;
|
||||
if (text.length > 160) continue;
|
||||
if (!hasUiText(text)) continue;
|
||||
findings.push({ kind: `attr:${attr}`, text });
|
||||
}
|
||||
|
||||
// alert/confirm with literal strings
|
||||
const alertRe = /\b(alert|confirm)\(\s*"([^"]+)"\s*\)/g;
|
||||
for (const m of content.matchAll(alertRe)) {
|
||||
findings.push({ kind: `${m[1]}()`, text: m[2] });
|
||||
}
|
||||
|
||||
return findings;
|
||||
}
|
||||
|
||||
const webRoot = path.resolve(process.cwd());
|
||||
const targets = [path.join(webRoot, "app"), path.join(webRoot, "components")].filter((p) =>
|
||||
fs.existsSync(p),
|
||||
);
|
||||
|
||||
const strict = process.argv.includes("--strict");
|
||||
const fileFilterIdx = process.argv.indexOf("--file");
|
||||
const fileFilter =
|
||||
fileFilterIdx >= 0 ? String(process.argv[fileFilterIdx + 1] || "").trim() : "";
|
||||
const showAll = process.argv.includes("--show-all");
|
||||
|
||||
const allFindings = [];
|
||||
for (const dir of targets) {
|
||||
const files = listCodeFiles(dir);
|
||||
for (const f of files) {
|
||||
if (fileFilter && !toRel(f, webRoot).includes(fileFilter)) continue;
|
||||
const content = fs.readFileSync(f, "utf8");
|
||||
const findings = auditFile(content);
|
||||
if (findings.length) {
|
||||
allFindings.push({
|
||||
file: toRel(f, webRoot),
|
||||
findings,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allFindings.length) {
|
||||
console.log("[i18n:audit] OK (no obvious UI literals found)");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`[i18n:audit] Found ${allFindings.length} files with potential UI literals`);
|
||||
const fileLimit = showAll ? allFindings.length : 80;
|
||||
for (const item of allFindings.slice(0, fileLimit)) {
|
||||
console.log(`\n- ${item.file}`);
|
||||
const perFileLimit = showAll ? item.findings.length : 10;
|
||||
for (const f of item.findings.slice(0, perFileLimit)) {
|
||||
console.log(` - ${f.kind}: ${JSON.stringify(f.text)}`);
|
||||
}
|
||||
if (!showAll && item.findings.length > 10)
|
||||
console.log(` - ... +${item.findings.length - 10} more`);
|
||||
}
|
||||
if (!showAll && allFindings.length > 80)
|
||||
console.log(`\n... and ${allFindings.length - 80} more files`);
|
||||
|
||||
if (strict) process.exit(1);
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,88 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
function listJsonFiles(dir) {
|
||||
const out = [];
|
||||
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, ent.name);
|
||||
if (ent.isDirectory()) out.push(...listJsonFiles(full));
|
||||
else if (ent.isFile() && ent.name.endsWith(".json")) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function loadJson(p) {
|
||||
return JSON.parse(fs.readFileSync(p, "utf8"));
|
||||
}
|
||||
|
||||
function flattenKeys(obj, prefix = "") {
|
||||
const keys = [];
|
||||
if (!obj || typeof obj !== "object") return keys;
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
const next = prefix ? `${prefix}.${k}` : k;
|
||||
if (v && typeof v === "object" && !Array.isArray(v)) keys.push(...flattenKeys(v, next));
|
||||
else keys.push(next);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function toRel(p, root) {
|
||||
return path.relative(root, p).replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
const webRoot = path.resolve(process.cwd());
|
||||
const localesRoot = path.join(webRoot, "locales");
|
||||
const enRoot = path.join(localesRoot, "en");
|
||||
const zhRoot = path.join(localesRoot, "zh");
|
||||
|
||||
if (!fs.existsSync(enRoot) || !fs.existsSync(zhRoot)) {
|
||||
console.error(`[i18n:parity] Missing locales roots: ${enRoot} or ${zhRoot}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const enFiles = listJsonFiles(enRoot).map((p) => toRel(p, enRoot)).sort();
|
||||
const zhFiles = listJsonFiles(zhRoot).map((p) => toRel(p, zhRoot)).sort();
|
||||
|
||||
const missingInZh = enFiles.filter((f) => !zhFiles.includes(f));
|
||||
const extraInZh = zhFiles.filter((f) => !enFiles.includes(f));
|
||||
|
||||
let ok = true;
|
||||
if (missingInZh.length) {
|
||||
ok = false;
|
||||
console.error("[i18n:parity] Missing zh files:");
|
||||
for (const f of missingInZh) console.error(`- ${f}`);
|
||||
}
|
||||
if (extraInZh.length) {
|
||||
ok = false;
|
||||
console.error("[i18n:parity] Extra zh files:");
|
||||
for (const f of extraInZh) console.error(`- ${f}`);
|
||||
}
|
||||
|
||||
for (const rel of enFiles) {
|
||||
if (!zhFiles.includes(rel)) continue;
|
||||
const enPath = path.join(enRoot, rel);
|
||||
const zhPath = path.join(zhRoot, rel);
|
||||
const enJson = loadJson(enPath);
|
||||
const zhJson = loadJson(zhPath);
|
||||
const enKeys = new Set(flattenKeys(enJson));
|
||||
const zhKeys = new Set(flattenKeys(zhJson));
|
||||
|
||||
const missingKeys = [...enKeys].filter((k) => !zhKeys.has(k)).sort();
|
||||
const extraKeys = [...zhKeys].filter((k) => !enKeys.has(k)).sort();
|
||||
|
||||
if (missingKeys.length || extraKeys.length) {
|
||||
ok = false;
|
||||
console.error(`[i18n:parity] Key mismatch in ${rel}`);
|
||||
if (missingKeys.length) {
|
||||
console.error(" Missing zh keys:");
|
||||
for (const k of missingKeys) console.error(` - ${k}`);
|
||||
}
|
||||
if (extraKeys.length) {
|
||||
console.error(" Extra zh keys:");
|
||||
for (const k of extraKeys) console.error(` - ${k}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ok) process.exit(1);
|
||||
console.log("[i18n:parity] OK");
|
||||
@@ -0,0 +1,22 @@
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const URL = "http://localhost:3784/home";
|
||||
const VW = 1440;
|
||||
const VH = 900;
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: VW, height: VH },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
await page.goto(URL, { waitUntil: "networkidle" });
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Rightmost 70 CSS px, full height — any right-edge shadow shows as a
|
||||
// darkening gradient in this narrow strip.
|
||||
await page.screenshot({
|
||||
path: "/tmp/right-strip.png",
|
||||
clip: { x: VW - 70, y: 0, width: 70, height: VH },
|
||||
});
|
||||
console.log("saved /tmp/right-strip.png");
|
||||
await browser.close();
|
||||
@@ -0,0 +1,131 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import vm from "vm";
|
||||
|
||||
const APP_SERVER_DIR = path.resolve(".next/server/app");
|
||||
const APP_OUTPUT_DIR = path.resolve(".next");
|
||||
|
||||
const ROUTE_BUDGETS_KB = {
|
||||
"/": 700,
|
||||
"/playground": 700,
|
||||
"/co-writer": 200,
|
||||
"/co-writer/[docId]": 700,
|
||||
"/knowledge": 450,
|
||||
"/memory": 450,
|
||||
"/settings": 180,
|
||||
};
|
||||
|
||||
const ROOT_SHELL_BUDGET_KB = 220;
|
||||
|
||||
function walkManifestFiles(rootDir) {
|
||||
const entries = [];
|
||||
for (const item of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(rootDir, item.name);
|
||||
if (item.isDirectory()) {
|
||||
entries.push(...walkManifestFiles(fullPath));
|
||||
continue;
|
||||
}
|
||||
if (item.name.endsWith("_client-reference-manifest.js")) {
|
||||
entries.push(fullPath);
|
||||
}
|
||||
}
|
||||
return entries.sort();
|
||||
}
|
||||
|
||||
function evaluateManifest(filePath) {
|
||||
const context = { globalThis: { __RSC_MANIFEST: {} } };
|
||||
vm.createContext(context);
|
||||
vm.runInContext(fs.readFileSync(filePath, "utf8"), context);
|
||||
const manifestEntries = Object.entries(context.globalThis.__RSC_MANIFEST);
|
||||
if (manifestEntries.length !== 1) {
|
||||
throw new Error(`Expected exactly one manifest in ${filePath}`);
|
||||
}
|
||||
const [manifestKey, manifest] = manifestEntries[0];
|
||||
return { manifestKey, manifest };
|
||||
}
|
||||
|
||||
function normalizePublicRoute(manifestKey) {
|
||||
const withoutGroups = manifestKey.replace(/\/\([^/]+\)/g, "");
|
||||
const withoutPageSuffix = withoutGroups.replace(/\/page$/, "");
|
||||
return withoutPageSuffix || "/";
|
||||
}
|
||||
|
||||
function resolveChunkSize(chunkPath) {
|
||||
const filePath = path.join(APP_OUTPUT_DIR, chunkPath.replace(/^\/+/, ""));
|
||||
return fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
|
||||
}
|
||||
|
||||
function sumChunkSizes(chunkPaths) {
|
||||
return chunkPaths.reduce((total, chunkPath) => total + resolveChunkSize(chunkPath), 0);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(APP_SERVER_DIR)) {
|
||||
console.error("Missing .next/server/app. Run `npm run build` before `npm run perf:check`.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const manifestFiles = walkManifestFiles(APP_SERVER_DIR).filter(
|
||||
(filePath) => !filePath.includes("_global-error") && !filePath.includes("_not-found"),
|
||||
);
|
||||
|
||||
const routeRows = [];
|
||||
let rootShellSize = 0;
|
||||
|
||||
for (const manifestFile of manifestFiles) {
|
||||
const { manifestKey, manifest } = evaluateManifest(manifestFile);
|
||||
const route = normalizePublicRoute(manifestKey);
|
||||
const entryFiles = manifest.entryJSFiles;
|
||||
|
||||
const rootLayoutFiles = entryFiles["[project]/app/layout"] || [];
|
||||
if (!rootShellSize && rootLayoutFiles.length > 0) {
|
||||
rootShellSize = sumChunkSizes(rootLayoutFiles);
|
||||
}
|
||||
const rootLayoutChunks = new Set(rootLayoutFiles);
|
||||
|
||||
const routeEntryKey = Object.keys(entryFiles).find(
|
||||
(key) => key.startsWith("[project]/app/") && key.endsWith("/page") && !key.includes("/layout"),
|
||||
);
|
||||
if (!routeEntryKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const chunkPaths = (entryFiles[routeEntryKey] || []).filter(
|
||||
(chunkPath) => !rootLayoutChunks.has(chunkPath),
|
||||
);
|
||||
routeRows.push({
|
||||
route,
|
||||
sizeBytes: sumChunkSizes(chunkPaths),
|
||||
chunks: chunkPaths.map((chunkPath) => path.basename(chunkPath)),
|
||||
});
|
||||
}
|
||||
|
||||
let hasFailure = false;
|
||||
|
||||
console.log("Route budgets (excluding root shell):");
|
||||
for (const row of routeRows) {
|
||||
const sizeKb = Math.round(row.sizeBytes / 1024);
|
||||
const budget = ROUTE_BUDGETS_KB[row.route];
|
||||
const status = budget && sizeKb > budget ? "FAIL" : "OK";
|
||||
if (status === "FAIL") {
|
||||
hasFailure = true;
|
||||
}
|
||||
console.log(
|
||||
`${status.padEnd(4)} ${row.route.padEnd(12)} ${String(sizeKb).padStart(4)}KB` +
|
||||
(budget ? ` / budget ${budget}KB` : ""),
|
||||
);
|
||||
}
|
||||
|
||||
if (rootShellSize) {
|
||||
const rootShellKb = Math.round(rootShellSize / 1024);
|
||||
const rootStatus = rootShellKb > ROOT_SHELL_BUDGET_KB ? "FAIL" : "OK";
|
||||
if (rootStatus === "FAIL") {
|
||||
hasFailure = true;
|
||||
}
|
||||
console.log(
|
||||
`${rootStatus.padEnd(4)} ${"root-shell".padEnd(12)} ${String(rootShellKb).padStart(4)}KB / budget ${ROOT_SHELL_BUDGET_KB}KB`,
|
||||
);
|
||||
}
|
||||
|
||||
if (hasFailure) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { readdirSync, rmSync, statSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const webRoot = path.resolve(__dirname, "..");
|
||||
const distRoot = path.join(webRoot, "dist", "node-tests");
|
||||
const testRoot = path.join(distRoot, "tests");
|
||||
|
||||
function run(cmd, args) {
|
||||
const result = spawnSync(cmd, args, {
|
||||
cwd: webRoot,
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
function collectTests(dir) {
|
||||
const entries = readdirSync(dir)
|
||||
.map((name) => path.join(dir, name))
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
const files = [];
|
||||
for (const entry of entries) {
|
||||
const stats = statSync(entry);
|
||||
if (stats.isDirectory()) {
|
||||
files.push(...collectTests(entry));
|
||||
continue;
|
||||
}
|
||||
if (entry.endsWith(".test.js")) {
|
||||
files.push(entry);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
rmSync(distRoot, { recursive: true, force: true });
|
||||
|
||||
run(path.join(webRoot, "node_modules", ".bin", "tsc"), [
|
||||
"-p",
|
||||
"tsconfig.node-tests.json",
|
||||
]);
|
||||
|
||||
const testFiles = collectTests(testRoot);
|
||||
if (testFiles.length === 0) {
|
||||
console.error("No compiled node tests found.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
run(process.execPath, ["--test", ...testFiles]);
|
||||
Reference in New Issue
Block a user