Files
openidea/scripts/check-webkit.ts
T
l0ng-ai 0938a507da fix(macos): require macOS 13, the first release whose system engine runs the app (#3271)
On macOS 11 and 12 other apps keep using the system's built-in WebKit (Safari 14.1 / 15.x) even after Safari is updated, so the Safari 16 interface could not start there. The bundle now declares macOS 13 as its minimum, the install docs say macOS 13, and the startup failure page no longer suggests updating Safari.

Fixes #2008
2026-11-14 10:23:17 +08:00

90 lines
3.6 KiB
TypeScript

import { readFileSync, readdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import ts from "typescript";
// Safari 16 is macOS 13's system engine (the app's minimum). Keep this floor in sync with vite.config.ts.
// Syntax lowering does not replace newer runtime APIs in dependencies.
const newerStatics = new Set([
"Promise.withResolvers",
"Object.groupBy",
"Map.groupBy",
"Array.fromAsync",
"Atomics.waitAsync",
"CSS.registerProperty",
"Uint8Array.fromBase64",
"Uint8Array.fromHex",
]);
const newerMethods = new Set(["isWellFormed", "toWellFormed"]);
export function checkSource(source: string, startup: boolean): string[] {
const tree = ts.createSourceFile("bundle.js", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
const errors = new Set<string>();
const checkRegex = (pattern: string, flags: string) => {
if (flags.includes("v")) errors.add("RegExp v flag requires Safari 17");
if (/\(\?<([=!])/.test(pattern)) errors.add("RegExp lookbehind requires Safari 16.4");
};
const walk = (node: ts.Node) => {
if (ts.isRegularExpressionLiteral(node)) {
const end = node.text.lastIndexOf("/");
checkRegex(node.text.slice(1, end), node.text.slice(end + 1));
}
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.attributes) {
errors.add("Import attributes require newer WebKit");
}
if (startup && ts.isPropertyAccessExpression(node)) {
const name = node.getText(tree);
if (newerStatics.has(name) || newerMethods.has(node.name.text)) errors.add(`${name} requires newer WebKit`);
}
if (
startup &&
(ts.isNewExpression(node) || ts.isCallExpression(node)) &&
node.expression.getText(tree) === "RegExp" &&
node.arguments?.[0] &&
ts.isStringLiteralLike(node.arguments[0])
) {
const flags = node.arguments[1];
checkRegex(node.arguments[0].text, flags && ts.isStringLiteralLike(flags) ? flags.text : "");
}
ts.forEachChild(node, walk);
};
walk(tree);
return [...errors];
}
export function checkBuild(dir: string): string[] {
const entry = readFileSync(resolve(dir, "index.html"), "utf8").match(/<script type="module"[^>]*src="([^"]+)"/);
if (!entry?.[1]) throw new Error("Production entry script was not found");
const startup = new Set<string>();
const visit = (file: string) => {
if (startup.has(file)) return;
startup.add(file);
const source = readFileSync(file, "utf8");
const tree = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
for (const node of tree.statements) {
if (
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
node.moduleSpecifier &&
ts.isStringLiteral(node.moduleSpecifier)
) {
visit(resolve(dirname(file), node.moduleSpecifier.text));
}
}
};
visit(resolve(dir, entry[1].replace(/^\//, "")));
startup.add(resolve(dir, "boot.js"));
const errors: string[] = [];
for (const file of readdirSync(dir, { recursive: true, encoding: "utf8" })) {
if (!file.endsWith(".js")) continue;
const path = resolve(dir, file);
for (const error of checkSource(readFileSync(path, "utf8"), startup.has(path))) errors.push(`${file}: ${error}`);
}
return errors;
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
const errors = checkBuild(resolve("dist"));
if (errors.length) throw new Error(`Safari 16 compatibility check failed:\n${errors.join("\n")}`);
console.log("Safari 16 startup API and bundle syntax checks passed");
}