Compare commits

...

2 Commits

Author SHA1 Message Date
vorotamoroz 82172e68e3 Update notes 2026-06-15 10:52:29 +01:00
vorotamoroz 24bc8404fe ### Improved
- Fetch chunks on demand now respects network conditions.

### Misc
- Prettified
2026-06-15 10:50:25 +01:00
10 changed files with 130 additions and 136 deletions
+5 -1
View File
@@ -109,7 +109,11 @@ Deno.test("daemon: ignore rules behaviour", async (t) => {
await runCliOrFail(vaultDir, "--settings", settingsFile, "mirror"); await runCliOrFail(vaultDir, "--settings", settingsFile, "mirror");
const dbList = await runCliOrFail(vaultDir, "--settings", settingsFile, "ls"); const dbList = await runCliOrFail(vaultDir, "--settings", settingsFile, "ls");
assertNotContains(dbList, "debug.log", "debug.log (ignored via .gitignore import) was unexpectedly synced to database"); assertNotContains(
dbList,
"debug.log",
"debug.log (ignored via .gitignore import) was unexpectedly synced to database"
);
assertContains(dbList, "regular.md", "regular.md was not synced normally alongside .gitignore import rules"); assertContains(dbList, "regular.md", "regular.md was not synced normally alongside .gitignore import rules");
console.log("[PASS] Case 3 verified successfully"); console.log("[PASS] Case 3 verified successfully");
}); });
+4 -31
View File
@@ -46,9 +46,7 @@ Deno.test("decoupled database and vault", async () => {
console.log("[INFO] applying CouchDB environment variables to settings"); console.log("[INFO] applying CouchDB environment variables to settings");
await applyCouchdbSettings(settingsFile, uri, user, password, dbname); await applyCouchdbSettings(settingsFile, uri, user, password, dbname);
} else { } else {
console.warn( console.warn("[WARN] CouchDB environment variables are not fully set. Push and pull operations may fail.");
"[WARN] CouchDB environment variables are not fully set. Push and pull operations may fail."
);
await markSettingsConfigured(settingsFile); await markSettingsConfigured(settingsFile);
} }
@@ -59,29 +57,11 @@ Deno.test("decoupled database and vault", async () => {
// 1. Test push command with decoupled vault directory // 1. Test push command with decoupled vault directory
console.log(`[INFO] push with decoupled vault -> ${REMOTE_PATH}`); console.log(`[INFO] push with decoupled vault -> ${REMOTE_PATH}`);
await runCliOrFail( await runCliOrFail(dbDir, "--vault", vaultDir, "--settings", settingsFile, "push", srcFile, REMOTE_PATH);
dbDir,
"--vault",
vaultDir,
"--settings",
settingsFile,
"push",
srcFile,
REMOTE_PATH
);
// 2. Test pull command with decoupled vault directory // 2. Test pull command with decoupled vault directory
console.log(`[INFO] pull with decoupled vault <- ${REMOTE_PATH}`); console.log(`[INFO] pull with decoupled vault <- ${REMOTE_PATH}`);
await runCliOrFail( await runCliOrFail(dbDir, "--vault", vaultDir, "--settings", settingsFile, "pull", REMOTE_PATH, pulledFile);
dbDir,
"--vault",
vaultDir,
"--settings",
settingsFile,
"pull",
REMOTE_PATH,
pulledFile
);
const pulled = await Deno.readTextFile(pulledFile); const pulled = await Deno.readTextFile(pulledFile);
assertEquals(pulled, content, "push/pull roundtrip with decoupled vault content mismatch"); assertEquals(pulled, content, "push/pull roundtrip with decoupled vault content mismatch");
@@ -93,14 +73,7 @@ Deno.test("decoupled database and vault", async () => {
// 4. Test mirror command with decoupled vault directory // 4. Test mirror command with decoupled vault directory
console.log("[INFO] mirror with decoupled vault"); console.log("[INFO] mirror with decoupled vault");
await runCliOrFail( await runCliOrFail(dbDir, "--vault", vaultDir, "--settings", settingsFile, "mirror");
dbDir,
"--vault",
vaultDir,
"--settings",
settingsFile,
"mirror"
);
const restoredFile = join(vaultDir, REMOTE_PATH); const restoredFile = join(vaultDir, REMOTE_PATH);
const restored = await Deno.readTextFile(restoredFile); const restored = await Deno.readTextFile(restoredFile);
@@ -14,7 +14,7 @@ import { getOptimalLoopbackIp } from "./helpers/net.ts";
Deno.test("p2p-peers: discovers host through local relay", async () => { Deno.test("p2p-peers: discovers host through local relay", async () => {
const loopbackIp = await getOptimalLoopbackIp(); const loopbackIp = await getOptimalLoopbackIp();
const loopbackHost = loopbackIp === "::1" ? "[::1]" : loopbackIp; const loopbackHost = loopbackIp === "::1" ? "[::1]" : loopbackIp;
const relay = Deno.env.get("RELAY") ?? `ws://${loopbackHost}:4000/`; const relay = Deno.env.get("RELAY") ?? `ws://${loopbackHost}:4000/`;
const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`; const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`;
const passphrase = Deno.env.get("PASSPHRASE") ?? "test"; const passphrase = Deno.env.get("PASSPHRASE") ?? "test";
+1 -1
View File
@@ -15,7 +15,7 @@ import { getOptimalLoopbackIp } from "./helpers/net.ts";
Deno.test("p2p-sync: discovers peer and completes sync", async () => { Deno.test("p2p-sync: discovers peer and completes sync", async () => {
const loopbackIp = await getOptimalLoopbackIp(); const loopbackIp = await getOptimalLoopbackIp();
const loopbackHost = loopbackIp === "::1" ? "[::1]" : loopbackIp; const loopbackHost = loopbackIp === "::1" ? "[::1]" : loopbackIp;
const relay = Deno.env.get("RELAY") ?? `ws://${loopbackHost}:4000/`; const relay = Deno.env.get("RELAY") ?? `ws://${loopbackHost}:4000/`;
const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`; const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`;
const passphrase = Deno.env.get("PASSPHRASE") ?? "test"; const passphrase = Deno.env.get("PASSPHRASE") ?? "test";
@@ -15,7 +15,7 @@ import { getOptimalLoopbackIp } from "./helpers/net.ts";
Deno.test("p2p: three nodes detect and resolve conflicts", async () => { Deno.test("p2p: three nodes detect and resolve conflicts", async () => {
const loopbackIp = await getOptimalLoopbackIp(); const loopbackIp = await getOptimalLoopbackIp();
const loopbackHost = loopbackIp === "::1" ? "[::1]" : loopbackIp; const loopbackHost = loopbackIp === "::1" ? "[::1]" : loopbackIp;
const relay = Deno.env.get("RELAY") ?? `ws://${loopbackHost}:4000/`; const relay = Deno.env.get("RELAY") ?? `ws://${loopbackHost}:4000/`;
const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`; const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`;
const passphrase = Deno.env.get("PASSPHRASE") ?? "test"; const passphrase = Deno.env.get("PASSPHRASE") ?? "test";
@@ -61,11 +61,7 @@ Deno.test("remote management commands", async () => {
// 1. remote-status outputs valid JSON with CouchDB details // 1. remote-status outputs valid JSON with CouchDB details
console.log("[CASE] remote-status outputs valid JSON with CouchDB details"); console.log("[CASE] remote-status outputs valid JSON with CouchDB details");
const statusOutput = await runCliCombinedOrFail(vaultDir, "--settings", settingsFile, "remote-status"); const statusOutput = await runCliCombinedOrFail(vaultDir, "--settings", settingsFile, "remote-status");
assertContains( assertContains(statusOutput, `"db_name": "${dbname}"`, "remote-status should return JSON containing db_name");
statusOutput,
`"db_name": "${dbname}"`,
"remote-status should return JSON containing db_name"
);
console.log("[PASS] remote-status verified"); console.log("[PASS] remote-status verified");
// 2. lock-remote locks and verifies state // 2. lock-remote locks and verifies state
+1 -1
Submodule src/lib updated: 5a552b3ec4...29b552f34b
+1
View File
@@ -124,6 +124,7 @@ export class ModuleLiveSyncMain extends AbstractModule {
await this.services.database.openDatabase({ await this.services.database.openDatabase({
databaseEvents: this.services.databaseEvents, databaseEvents: this.services.databaseEvents,
replicator: this.services.replicator, replicator: this.services.replicator,
replication: this.services.replication,
}); });
// this.core.$$realizeSettingSyncMode = this.core.$$realizeSettingSyncMode.bind(this); // this.core.$$realizeSettingSyncMode = this.core.$$realizeSettingSyncMode.bind(this);
// this.$$parseReplicationResult = this.$$parseReplicationResult.bind(this); // this.$$parseReplicationResult = this.$$parseReplicationResult.bind(this);
+6
View File
@@ -3,6 +3,12 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
## Unreleased
### Improved
- Now chunk waiting timeout is adaptive
- Fetch chunks on demand now respects network conditions. Wait until the replication and network requests have been finished.
## 0.25.75 ## 0.25.75
13th June, 2026 13th June, 2026
+109 -95
View File
@@ -1,14 +1,14 @@
import * as acorn from 'acorn'; import * as acorn from "acorn";
import fs from 'fs'; import fs from "fs";
// Parse command line arguments // Parse command line arguments
const args = process.argv.slice(2); const args = process.argv.slice(2);
let file = 'main.js'; let file = "main.js";
let target = 2018; let target = 2018;
let ios = null; let ios = null;
// Help menu // Help menu
if (args.includes('--help') || args.includes('-h')) { if (args.includes("--help") || args.includes("-h")) {
console.log(`Usage: node utils/check-compatibility.js [options] console.log(`Usage: node utils/check-compatibility.js [options]
Options: Options:
--file <path> Path to the bundle file to check (default: main.js) --file <path> Path to the bundle file to check (default: main.js)
@@ -27,13 +27,13 @@ Options:
} }
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
if (args[i] === '--file' && args[i + 1]) { if (args[i] === "--file" && args[i + 1]) {
file = args[i + 1]; file = args[i + 1];
i++; i++;
} else if (args[i] === '--target' && args[i + 1]) { } else if (args[i] === "--target" && args[i + 1]) {
target = parseInt(args[i + 1], 10); target = parseInt(args[i + 1], 10);
i++; i++;
} else if (args[i] === '--ios' && args[i + 1]) { } else if (args[i] === "--ios" && args[i + 1]) {
ios = parseFloat(args[i + 1]); ios = parseFloat(args[i + 1]);
i++; i++;
} }
@@ -69,22 +69,22 @@ if (ios !== null) {
// Override defaults with explicit command line options if specified // Override defaults with explicit command line options if specified
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
if (args[i] === '--allow-dynamic-import') allowDynamicImport = true; if (args[i] === "--allow-dynamic-import") allowDynamicImport = true;
else if (args[i] === '--no-allow-dynamic-import') allowDynamicImport = false; else if (args[i] === "--no-allow-dynamic-import") allowDynamicImport = false;
else if (args[i] === '--allow-bigint') allowBigInt = true; else if (args[i] === "--allow-bigint") allowBigInt = true;
else if (args[i] === '--no-allow-bigint') allowBigInt = false; else if (args[i] === "--no-allow-bigint") allowBigInt = false;
else if (args[i] === '--allow-numeric-separator') allowNumericSeparator = true; else if (args[i] === "--allow-numeric-separator") allowNumericSeparator = true;
else if (args[i] === '--no-allow-numeric-separator') allowNumericSeparator = false; else if (args[i] === "--no-allow-numeric-separator") allowNumericSeparator = false;
else if (args[i] === '--allow-class-fields') allowClassFields = true; else if (args[i] === "--allow-class-fields") allowClassFields = true;
else if (args[i] === '--no-allow-class-fields') allowClassFields = false; else if (args[i] === "--no-allow-class-fields") allowClassFields = false;
else if (args[i] === '--allow-class-static-blocks') allowClassStaticBlocks = true; 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] === "--no-allow-class-static-blocks") allowClassStaticBlocks = false;
else if (args[i] === '--allow-regexp-lookbehind') allowRegexpLookbehind = true; else if (args[i] === "--allow-regexp-lookbehind") allowRegexpLookbehind = true;
else if (args[i] === '--no-allow-regexp-lookbehind') allowRegexpLookbehind = false; else if (args[i] === "--no-allow-regexp-lookbehind") allowRegexpLookbehind = false;
else if (args[i] === '--allow-regexp-indices') allowRegexpIndices = true; else if (args[i] === "--allow-regexp-indices") allowRegexpIndices = true;
else if (args[i] === '--no-allow-regexp-indices') allowRegexpIndices = false; else if (args[i] === "--no-allow-regexp-indices") allowRegexpIndices = false;
else if (args[i] === '--allow-regexp-v-flag') allowRegexpVFlag = true; else if (args[i] === "--allow-regexp-v-flag") allowRegexpVFlag = true;
else if (args[i] === '--no-allow-regexp-v-flag') allowRegexpVFlag = false; else if (args[i] === "--no-allow-regexp-v-flag") allowRegexpVFlag = false;
} }
if (!fs.existsSync(file)) { if (!fs.existsSync(file)) {
@@ -92,35 +92,35 @@ if (!fs.existsSync(file)) {
process.exit(1); process.exit(1);
} }
const code = fs.readFileSync(file, 'utf8'); const code = fs.readFileSync(file, "utf8");
let ast; let ast;
const targetInfo = ios !== null ? `iOS ${ios}` : `ES${target}`; const targetInfo = ios !== null ? `iOS ${ios}` : `ES${target}`;
console.log(`Parsing '${file}' to inspect compatibility (target ${targetInfo})...`); console.log(`Parsing '${file}' to inspect compatibility (target ${targetInfo})...`);
console.log(`Rules: console.log(`Rules:
Dynamic Import: ${allowDynamicImport ? 'Allowed' : 'Prohibited'} Dynamic Import: ${allowDynamicImport ? "Allowed" : "Prohibited"}
BigInt: ${allowBigInt ? 'Allowed' : 'Prohibited'} BigInt: ${allowBigInt ? "Allowed" : "Prohibited"}
Numeric Separators: ${allowNumericSeparator ? 'Allowed' : 'Prohibited'} Numeric Separators: ${allowNumericSeparator ? "Allowed" : "Prohibited"}
Class Fields: ${allowClassFields ? 'Allowed' : 'Prohibited'} Class Fields: ${allowClassFields ? "Allowed" : "Prohibited"}
Class Static Block: ${allowClassStaticBlocks ? 'Allowed' : 'Prohibited'} Class Static Block: ${allowClassStaticBlocks ? "Allowed" : "Prohibited"}
RegExp Lookbehind: ${allowRegexpLookbehind ? 'Allowed' : 'Prohibited'} RegExp Lookbehind: ${allowRegexpLookbehind ? "Allowed" : "Prohibited"}
RegExp Indices (d): ${allowRegexpIndices ? 'Allowed' : 'Prohibited'} RegExp Indices (d): ${allowRegexpIndices ? "Allowed" : "Prohibited"}
RegExp Unicode (v): ${allowRegexpVFlag ? 'Allowed' : 'Prohibited'} RegExp Unicode (v): ${allowRegexpVFlag ? "Allowed" : "Prohibited"}
`); `);
try { try {
ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'script' }); ast = acorn.parse(code, { ecmaVersion: "latest", sourceType: "script" });
} catch (err) { } catch (err) {
console.error(`Syntax Error: Failed to parse '${file}' due to a syntax issue:`); console.error(`Syntax Error: Failed to parse '${file}' due to a syntax issue:`);
console.error(err.message); console.error(err.message);
if (err.pos !== undefined) { if (err.pos !== undefined) {
const line = code.substring(0, err.pos).split('\n').length; const line = code.substring(0, err.pos).split("\n").length;
console.error(`Location: line ${line}, character ${err.pos}`); console.error(`Location: line ${line}, character ${err.pos}`);
const start = Math.max(0, err.pos - 50); const start = Math.max(0, err.pos - 50);
const end = Math.min(code.length, err.pos + 50); const end = Math.min(code.length, err.pos + 50);
console.error('Context around error:'); console.error("Context around error:");
console.error(code.substring(start, end)); console.error(code.substring(start, end));
console.error(' '.repeat(err.pos - start) + '^'); console.error(" ".repeat(err.pos - start) + "^");
} }
process.exit(1); process.exit(1);
} }
@@ -131,14 +131,14 @@ const violations = [];
function hasLookbehind(pattern) { function hasLookbehind(pattern) {
let index = 0; let index = 0;
while (true) { while (true) {
const match = pattern.indexOf('(?<=', index); const match = pattern.indexOf("(?<=", index);
const match2 = pattern.indexOf('(?<!', index); const match2 = pattern.indexOf("(?<!", index);
const pos = (match !== -1 && match2 !== -1) ? Math.min(match, match2) : (match !== -1 ? match : match2); const pos = match !== -1 && match2 !== -1 ? Math.min(match, match2) : match !== -1 ? match : match2;
if (pos === -1) break; if (pos === -1) break;
let backslashes = 0; let backslashes = 0;
for (let i = pos - 1; i >= 0; i--) { for (let i = pos - 1; i >= 0; i--) {
if (pattern[i] === '\\') backslashes++; if (pattern[i] === "\\") backslashes++;
else break; else break;
} }
if (backslashes % 2 === 0) { if (backslashes % 2 === 0) {
@@ -150,178 +150,192 @@ function hasLookbehind(pattern) {
} }
function checkNode(node) { function checkNode(node) {
if (!node || typeof node !== 'object') return; if (!node || typeof node !== "object") return;
if (node.type) { if (node.type) {
// 1. Optional catch binding (ES2019 / iOS 11.3+) // 1. Optional catch binding (ES2019 / iOS 11.3+)
if (node.type === 'CatchClause' && !node.param) { if (node.type === "CatchClause" && !node.param) {
if (target < 2019 && ios === null) { if (target < 2019 && ios === null) {
violations.push({ violations.push({
feature: 'Optional catch binding (ES2019 / iOS 11.3+)', feature: "Optional catch binding (ES2019 / iOS 11.3+)",
pos: node.start, pos: node.start,
node node,
}); });
} else if (ios !== null && ios < 11.3) { } else if (ios !== null && ios < 11.3) {
violations.push({ violations.push({
feature: 'Optional catch binding (iOS 11.3+)', feature: "Optional catch binding (iOS 11.3+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
} }
// 2. Dynamic import (ES2020 / iOS 11.3+) // 2. Dynamic import (ES2020 / iOS 11.3+)
if (node.type === 'ImportExpression') { if (node.type === "ImportExpression") {
if (!allowDynamicImport) { if (!allowDynamicImport) {
violations.push({ violations.push({
feature: 'Dynamic import (ES2020 / iOS 11.3+)', feature: "Dynamic import (ES2020 / iOS 11.3+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
} }
// 3. import.meta (ES2020 / iOS 11.3+) // 3. import.meta (ES2020 / iOS 11.3+)
if (node.type === 'MetaProperty' && node.meta && node.meta.name === 'import') { if (node.type === "MetaProperty" && node.meta && node.meta.name === "import") {
if (!allowDynamicImport) { if (!allowDynamicImport) {
violations.push({ violations.push({
feature: 'import.meta (ES2020 / iOS 11.3+)', feature: "import.meta (ES2020 / iOS 11.3+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
} }
// 4. Optional chaining (ES2020 / iOS 13.4+) // 4. Optional chaining (ES2020 / iOS 13.4+)
if (node.type === 'ChainExpression') { if (node.type === "ChainExpression") {
const isProhibited = ios !== null ? ios < 13.4 : target < 2020; const isProhibited = ios !== null ? ios < 13.4 : target < 2020;
if (isProhibited) { if (isProhibited) {
violations.push({ violations.push({
feature: 'Optional chaining (ES2020 / iOS 13.4+)', feature: "Optional chaining (ES2020 / iOS 13.4+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
} }
// 5. Nullish coalescing (ES2020 / iOS 13.4+) // 5. Nullish coalescing (ES2020 / iOS 13.4+)
if (node.type === 'LogicalExpression' && node.operator === '??') { if (node.type === "LogicalExpression" && node.operator === "??") {
const isProhibited = ios !== null ? ios < 13.4 : target < 2020; const isProhibited = ios !== null ? ios < 13.4 : target < 2020;
if (isProhibited) { if (isProhibited) {
violations.push({ violations.push({
feature: 'Nullish coalescing (ES2020 / iOS 13.4+)', feature: "Nullish coalescing (ES2020 / iOS 13.4+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
} }
// 6. BigInt literal (ES2020 / iOS 14.0+) // 6. BigInt literal (ES2020 / iOS 14.0+)
if (node.type === 'Literal' && node.bigint !== undefined) { if (node.type === "Literal" && node.bigint !== undefined) {
if (!allowBigInt) { if (!allowBigInt) {
violations.push({ violations.push({
feature: 'BigInt literal (ES2020 / iOS 14.0+)', feature: "BigInt literal (ES2020 / iOS 14.0+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
} }
// 7. Logical assignment (ES2021 / iOS 14.0+) // 7. Logical assignment (ES2021 / iOS 14.0+)
if (node.type === 'AssignmentExpression' && ['||=', '&&=', '??='].includes(node.operator)) { if (node.type === "AssignmentExpression" && ["||=", "&&=", "??="].includes(node.operator)) {
const isProhibited = ios !== null ? ios < 14.0 : target < 2021; const isProhibited = ios !== null ? ios < 14.0 : target < 2021;
if (isProhibited) { if (isProhibited) {
violations.push({ violations.push({
feature: `Logical assignment operator '${node.operator}' (ES2021 / iOS 14.0+)`, feature: `Logical assignment operator '${node.operator}' (ES2021 / iOS 14.0+)`,
pos: node.start, pos: node.start,
node node,
}); });
} }
} }
// 8. Numeric separators (ES2021 / iOS 14.0+) // 8. Numeric separators (ES2021 / iOS 14.0+)
if (node.type === 'Literal' && typeof node.value === 'number' && node.raw && node.raw.includes('_')) { if (node.type === "Literal" && typeof node.value === "number" && node.raw && node.raw.includes("_")) {
if (!allowNumericSeparator) { if (!allowNumericSeparator) {
violations.push({ violations.push({
feature: 'Numeric separator (ES2021 / iOS 14.0+)', feature: "Numeric separator (ES2021 / iOS 14.0+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
} }
// 9. Class Fields (ES2022 / iOS 14.0+ public, iOS 14.5+ private/static) // 9. Class Fields (ES2022 / iOS 14.0+ public, iOS 14.5+ private/static)
if (node.type === 'PropertyDefinition') { if (node.type === "PropertyDefinition") {
if (!allowClassFields) { if (!allowClassFields) {
const requiredVersion = node.key.type === 'PrivateIdentifier' || node.static ? 'iOS 14.5+' : 'iOS 14.0+'; const requiredVersion =
node.key.type === "PrivateIdentifier" || node.static ? "iOS 14.5+" : "iOS 14.0+";
violations.push({ violations.push({
feature: `Class field definition '${node.key.name || node.key.value || '#private'}' (ES2022 / ${requiredVersion})`, feature: `Class field definition '${node.key.name || node.key.value || "#private"}' (ES2022 / ${requiredVersion})`,
pos: node.start, pos: node.start,
node node,
}); });
} }
} }
// 10. Class Static Initialization Blocks (ES2022 / iOS 16.4+) // 10. Class Static Initialization Blocks (ES2022 / iOS 16.4+)
if (node.type === 'StaticBlock') { if (node.type === "StaticBlock") {
if (!allowClassStaticBlocks) { if (!allowClassStaticBlocks) {
violations.push({ violations.push({
feature: 'Class static initialization block (ES2022 / iOS 16.4+)', feature: "Class static initialization block (ES2022 / iOS 16.4+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
} }
// 11. RegExp lookbehind assertions (ES2018 / iOS 16.4+) // 11. RegExp lookbehind assertions (ES2018 / iOS 16.4+)
if (node.type === 'Literal' && node.regex) { if (node.type === "Literal" && node.regex) {
if (!allowRegexpLookbehind && hasLookbehind(node.regex.pattern)) { if (!allowRegexpLookbehind && hasLookbehind(node.regex.pattern)) {
violations.push({ violations.push({
feature: 'RegExp Lookbehind assertion (iOS 16.4+)', feature: "RegExp Lookbehind assertion (iOS 16.4+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
if (!allowRegexpIndices && node.regex.flags.includes('d')) { if (!allowRegexpIndices && node.regex.flags.includes("d")) {
violations.push({ violations.push({
feature: "RegExp 'd' (indices) flag (ES2022 / iOS 15.0+)", feature: "RegExp 'd' (indices) flag (ES2022 / iOS 15.0+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
if (!allowRegexpVFlag && node.regex.flags.includes('v')) { if (!allowRegexpVFlag && node.regex.flags.includes("v")) {
violations.push({ violations.push({
feature: "RegExp 'v' (Unicode properties) flag (ES2024 / iOS 17.0+)", feature: "RegExp 'v' (Unicode properties) flag (ES2024 / iOS 17.0+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
} }
if ((node.type === 'NewExpression' || node.type === 'CallExpression') && node.callee && node.callee.name === 'RegExp') { if (
if (!allowRegexpLookbehind && node.arguments[0] && node.arguments[0].type === 'Literal' && typeof node.arguments[0].value === 'string') { (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)) { if (hasLookbehind(node.arguments[0].value)) {
violations.push({ violations.push({
feature: 'RegExp Lookbehind assertion (iOS 16.4+)', feature: "RegExp Lookbehind assertion (iOS 16.4+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
} }
if (node.arguments[1] && node.arguments[1].type === 'Literal' && typeof node.arguments[1].value === 'string') { if (
node.arguments[1] &&
node.arguments[1].type === "Literal" &&
typeof node.arguments[1].value === "string"
) {
const flags = node.arguments[1].value; const flags = node.arguments[1].value;
if (!allowRegexpIndices && flags.includes('d')) { if (!allowRegexpIndices && flags.includes("d")) {
violations.push({ violations.push({
feature: "RegExp 'd' (indices) flag (ES2022 / iOS 15.0+)", feature: "RegExp 'd' (indices) flag (ES2022 / iOS 15.0+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
if (!allowRegexpVFlag && flags.includes('v')) { if (!allowRegexpVFlag && flags.includes("v")) {
violations.push({ violations.push({
feature: "RegExp 'v' (Unicode properties) flag (ES2024 / iOS 17.0+)", feature: "RegExp 'v' (Unicode properties) flag (ES2024 / iOS 17.0+)",
pos: node.start, pos: node.start,
node node,
}); });
} }
} }
@@ -329,13 +343,13 @@ function checkNode(node) {
} }
for (const key in node) { for (const key in node) {
if (key === 'loc' || key === 'start' || key === 'end') continue; if (key === "loc" || key === "start" || key === "end") continue;
const val = node[key]; const val = node[key];
if (Array.isArray(val)) { if (Array.isArray(val)) {
for (const child of val) { for (const child of val) {
checkNode(child); checkNode(child);
} }
} else if (val && typeof val === 'object') { } else if (val && typeof val === "object") {
checkNode(val); checkNode(val);
} }
} }
@@ -347,14 +361,14 @@ checkNode(ast);
if (violations.length > 0) { if (violations.length > 0) {
console.error(`\nCompatibility Check Failed: Found ${violations.length} prohibited features.`); console.error(`\nCompatibility Check Failed: Found ${violations.length} prohibited features.`);
violations.forEach((v, index) => { violations.forEach((v, index) => {
const line = code.substring(0, v.pos).split('\n').length; const line = code.substring(0, v.pos).split("\n").length;
console.error(`\n[${index + 1}] Prohibited feature: ${v.feature}`); console.error(`\n[${index + 1}] Prohibited feature: ${v.feature}`);
console.error(`Location: line ${line}, character ${v.pos}`); console.error(`Location: line ${line}, character ${v.pos}`);
const start = Math.max(0, v.pos - 50); const start = Math.max(0, v.pos - 50);
const end = Math.min(code.length, v.pos + 50); const end = Math.min(code.length, v.pos + 50);
console.error('Context around feature:'); console.error("Context around feature:");
console.error(code.substring(start, end)); console.error(code.substring(start, end));
console.error(' '.repeat(v.pos - start) + '^'); console.error(" ".repeat(v.pos - start) + "^");
}); });
process.exit(1); process.exit(1);
} }