import * as acorn from "acorn"; import fs from "fs"; // Parse command line arguments const args = process.argv.slice(2); let file = "main.js"; let target = 2018; let ios = null; // Help menu if (args.includes("--help") || args.includes("-h")) { console.log(`Usage: node utils/check-compatibility.js [options] Options: --file Path to the bundle file to check (default: main.js) --target Target ECMAScript version (default: 2018) --ios Target iOS version (e.g. 14, 15, 16.4). Sets defaults automatically. --[no-]allow-dynamic-import Allow dynamic import() expressions --[no-]allow-bigint Allow BigInt literals --[no-]allow-numeric-separator Allow numeric separators (e.g. 1_000) --[no-]allow-class-fields Allow public/private/static class fields --[no-]allow-class-static-blocks Allow class static initialization blocks --[no-]allow-regexp-lookbehind Allow RegExp lookbehind assertions ((?<=...) / (?= 2020; let allowBigInt = target >= 2020; let allowNumericSeparator = target >= 2021; let allowClassFields = target >= 2022; let allowClassStaticBlocks = target >= 2022; let allowRegexpLookbehind = target >= 2023; let allowRegexpIndices = target >= 2022; let allowRegexpVFlag = target >= 2024; // Override feature flags if target iOS version is specified if (ios !== null) { // Determine a general baseline ECMA version for parser reports if (ios >= 16.4) target = 2022; else if (ios >= 15.0) target = 2021; else if (ios >= 14.0) target = 2020; else target = 2018; allowDynamicImport = ios >= 11.3; allowBigInt = ios >= 14.0; allowNumericSeparator = ios >= 14.0; allowClassFields = ios >= 14.5; allowRegexpIndices = ios >= 15.0; allowClassStaticBlocks = ios >= 16.4; allowRegexpLookbehind = ios >= 16.4; allowRegexpVFlag = ios >= 17.0; } // Override defaults with explicit command line options if specified for (let i = 0; i < args.length; i++) { if (args[i] === "--allow-dynamic-import") allowDynamicImport = true; else if (args[i] === "--no-allow-dynamic-import") allowDynamicImport = false; else if (args[i] === "--allow-bigint") allowBigInt = true; else if (args[i] === "--no-allow-bigint") allowBigInt = false; else if (args[i] === "--allow-numeric-separator") allowNumericSeparator = true; else if (args[i] === "--no-allow-numeric-separator") allowNumericSeparator = false; else if (args[i] === "--allow-class-fields") allowClassFields = true; else if (args[i] === "--no-allow-class-fields") allowClassFields = false; else if (args[i] === "--allow-class-static-blocks") allowClassStaticBlocks = true; else if (args[i] === "--no-allow-class-static-blocks") allowClassStaticBlocks = false; else if (args[i] === "--allow-regexp-lookbehind") allowRegexpLookbehind = true; else if (args[i] === "--no-allow-regexp-lookbehind") allowRegexpLookbehind = false; else if (args[i] === "--allow-regexp-indices") allowRegexpIndices = true; else if (args[i] === "--no-allow-regexp-indices") allowRegexpIndices = false; else if (args[i] === "--allow-regexp-v-flag") allowRegexpVFlag = true; else if (args[i] === "--no-allow-regexp-v-flag") allowRegexpVFlag = false; } if (!fs.existsSync(file)) { console.error(`Error: File '${file}' does not exist.`); process.exit(1); } const code = fs.readFileSync(file, "utf8"); let ast; const targetInfo = ios !== null ? `iOS ${ios}` : `ES${target}`; console.log(`Parsing '${file}' to inspect compatibility (target ${targetInfo})...`); console.log(`Rules: Dynamic Import: ${allowDynamicImport ? "Allowed" : "Prohibited"} BigInt: ${allowBigInt ? "Allowed" : "Prohibited"} Numeric Separators: ${allowNumericSeparator ? "Allowed" : "Prohibited"} Class Fields: ${allowClassFields ? "Allowed" : "Prohibited"} Class Static Block: ${allowClassStaticBlocks ? "Allowed" : "Prohibited"} RegExp Lookbehind: ${allowRegexpLookbehind ? "Allowed" : "Prohibited"} RegExp Indices (d): ${allowRegexpIndices ? "Allowed" : "Prohibited"} RegExp Unicode (v): ${allowRegexpVFlag ? "Allowed" : "Prohibited"} `); try { ast = acorn.parse(code, { ecmaVersion: "latest", sourceType: "script" }); } catch (err) { console.error(`Syntax Error: Failed to parse '${file}' due to a syntax issue:`); console.error(err.message); if (err.pos !== undefined) { const line = code.substring(0, err.pos).split("\n").length; console.error(`Location: line ${line}, character ${err.pos}`); const start = Math.max(0, err.pos - 50); const end = Math.min(code.length, err.pos + 50); console.error("Context around error:"); console.error(code.substring(start, end)); console.error(" ".repeat(err.pos - start) + "^"); } process.exit(1); } // Violations list const violations = []; function hasLookbehind(pattern) { let index = 0; while (true) { const match = pattern.indexOf("(?<=", index); const match2 = pattern.indexOf("(?= 0; i--) { if (pattern[i] === "\\") backslashes++; else break; } if (backslashes % 2 === 0) { return true; } index = pos + 4; } return false; } function checkNode(node) { if (!node || typeof node !== "object") return; if (node.type) { // 1. Optional catch binding (ES2019 / iOS 11.3+) if (node.type === "CatchClause" && !node.param) { if (target < 2019 && ios === null) { violations.push({ feature: "Optional catch binding (ES2019 / iOS 11.3+)", pos: node.start, node, }); } else if (ios !== null && ios < 11.3) { violations.push({ feature: "Optional catch binding (iOS 11.3+)", pos: node.start, node, }); } } // 2. Dynamic import (ES2020 / iOS 11.3+) if (node.type === "ImportExpression") { if (!allowDynamicImport) { violations.push({ feature: "Dynamic import (ES2020 / iOS 11.3+)", pos: node.start, node, }); } } // 3. import.meta (ES2020 / iOS 11.3+) if (node.type === "MetaProperty" && node.meta && node.meta.name === "import") { if (!allowDynamicImport) { violations.push({ feature: "import.meta (ES2020 / iOS 11.3+)", pos: node.start, node, }); } } // 4. Optional chaining (ES2020 / iOS 13.4+) if (node.type === "ChainExpression") { const isProhibited = ios !== null ? ios < 13.4 : target < 2020; if (isProhibited) { violations.push({ feature: "Optional chaining (ES2020 / iOS 13.4+)", pos: node.start, node, }); } } // 5. Nullish coalescing (ES2020 / iOS 13.4+) if (node.type === "LogicalExpression" && node.operator === "??") { const isProhibited = ios !== null ? ios < 13.4 : target < 2020; if (isProhibited) { violations.push({ feature: "Nullish coalescing (ES2020 / iOS 13.4+)", pos: node.start, node, }); } } // 6. BigInt literal (ES2020 / iOS 14.0+) if (node.type === "Literal" && node.bigint !== undefined) { if (!allowBigInt) { violations.push({ feature: "BigInt literal (ES2020 / iOS 14.0+)", pos: node.start, node, }); } } // 7. Logical assignment (ES2021 / iOS 14.0+) if (node.type === "AssignmentExpression" && ["||=", "&&=", "??="].includes(node.operator)) { const isProhibited = ios !== null ? ios < 14.0 : target < 2021; if (isProhibited) { violations.push({ feature: `Logical assignment operator '${node.operator}' (ES2021 / iOS 14.0+)`, pos: node.start, node, }); } } // 8. Numeric separators (ES2021 / iOS 14.0+) if (node.type === "Literal" && typeof node.value === "number" && node.raw && node.raw.includes("_")) { if (!allowNumericSeparator) { violations.push({ feature: "Numeric separator (ES2021 / iOS 14.0+)", pos: node.start, node, }); } } // 9. Class Fields (ES2022 / iOS 14.0+ public, iOS 14.5+ private/static) if (node.type === "PropertyDefinition") { if (!allowClassFields) { const requiredVersion = node.key.type === "PrivateIdentifier" || node.static ? "iOS 14.5+" : "iOS 14.0+"; violations.push({ feature: `Class field definition '${node.key.name || node.key.value || "#private"}' (ES2022 / ${requiredVersion})`, pos: node.start, node, }); } } // 10. Class Static Initialization Blocks (ES2022 / iOS 16.4+) if (node.type === "StaticBlock") { if (!allowClassStaticBlocks) { violations.push({ feature: "Class static initialization block (ES2022 / iOS 16.4+)", pos: node.start, node, }); } } // 11. RegExp lookbehind assertions (ES2018 / iOS 16.4+) if (node.type === "Literal" && node.regex) { if (!allowRegexpLookbehind && hasLookbehind(node.regex.pattern)) { violations.push({ feature: "RegExp Lookbehind assertion (iOS 16.4+)", pos: node.start, node, }); } if (!allowRegexpIndices && node.regex.flags.includes("d")) { violations.push({ feature: "RegExp 'd' (indices) flag (ES2022 / iOS 15.0+)", pos: node.start, node, }); } if (!allowRegexpVFlag && node.regex.flags.includes("v")) { violations.push({ feature: "RegExp 'v' (Unicode properties) flag (ES2024 / iOS 17.0+)", pos: node.start, node, }); } } if ( (node.type === "NewExpression" || node.type === "CallExpression") && node.callee && node.callee.name === "RegExp" ) { if ( !allowRegexpLookbehind && node.arguments[0] && node.arguments[0].type === "Literal" && typeof node.arguments[0].value === "string" ) { if (hasLookbehind(node.arguments[0].value)) { violations.push({ feature: "RegExp Lookbehind assertion (iOS 16.4+)", pos: node.start, node, }); } } if ( node.arguments[1] && node.arguments[1].type === "Literal" && typeof node.arguments[1].value === "string" ) { const flags = node.arguments[1].value; if (!allowRegexpIndices && flags.includes("d")) { violations.push({ feature: "RegExp 'd' (indices) flag (ES2022 / iOS 15.0+)", pos: node.start, node, }); } if (!allowRegexpVFlag && flags.includes("v")) { violations.push({ feature: "RegExp 'v' (Unicode properties) flag (ES2024 / iOS 17.0+)", pos: node.start, node, }); } } } } for (const key in node) { if (key === "loc" || key === "start" || key === "end") continue; const val = node[key]; if (Array.isArray(val)) { for (const child of val) { checkNode(child); } } else if (val && typeof val === "object") { checkNode(val); } } } // Run compatibility checks on the AST checkNode(ast); if (violations.length > 0) { console.error(`\nCompatibility Check Failed: Found ${violations.length} prohibited features.`); violations.forEach((v, index) => { const line = code.substring(0, v.pos).split("\n").length; console.error(`\n[${index + 1}] Prohibited feature: ${v.feature}`); console.error(`Location: line ${line}, character ${v.pos}`); const start = Math.max(0, v.pos - 50); const end = Math.min(code.length, v.pos + 50); console.error("Context around feature:"); console.error(code.substring(start, end)); console.error(" ".repeat(v.pos - start) + "^"); }); process.exit(1); } console.log(`\nCompatibility Check Passed: '${file}' matches the compatibility rules.`); process.exit(0);