Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 5 additions & 19 deletions eslint.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,13 @@ export default tseslint.config(
files: ["src/**/*.ts", "src/**/*.tsx"],
...sonarjs.configs.recommended,
},
{
// e2e/ and scripts/ are Node-side tooling, not plugin code that ships
// into Obsidian: they run in CI/local Node, need real fetch/Buffer/process,
// and are the one place `fetch` is correct (the obsidian-request-url shim
// IS the requestUrl implementation the obsidianmd rule wants everyone to use).
files: ["e2e/**/*.ts", "scripts/**/*.mjs"],
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
"import/no-nodejs-modules": "off",
"no-restricted-globals": "off",
},
},
globalIgnores([
"node_modules",
"dist",
"node_modules/**",
"dist/**",
"e2e/**",
"scripts/**",
"esbuild.config.mjs",
"eslint.config.js",
"eslint.config.*",
"version-bump.mjs",
"versions.json",
"main.js",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && npm run typecheck:compat && node esbuild.config.mjs production",
"typecheck:compat": "node scripts/typecheck-compat.mjs",
"check:obsidian-compat": "node scripts/check-obsidian-compat.mjs",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"lint": "eslint .",
"test": "vitest run",
Expand Down
82 changes: 82 additions & 0 deletions scripts/check-obsidian-compat.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';

const NODE_BUILTINS = ['crypto', 'child_process', 'util'];

/** Removes comments while preserving code and string literals. */
function stripComments(source) {

Check failure on line 7 in scripts/check-obsidian-compat.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 23 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AZ_csQjiAtF8P_erihdM&open=AZ_csQjiAtF8P_erihdM&pullRequest=119
let result = '';
let index = 0;
let quote = '';

while (index < source.length) {
const character = source[index];
const next = source[index + 1];

if (quote) {
result += character;
if (character === '\\') {
result += next ?? '';
index += 2;
continue;
}
if (character === quote) quote = '';
index += 1;
continue;
}

if (character === '"' || character === "'" || character === '`') {
quote = character;
result += character;
index += 1;
continue;
}

if (character === '/' && next === '/') {
index = source.indexOf('\n', index);
if (index === -1) break;
result += '\n';
index += 1;
continue;
}

if (character === '/' && next === '*') {
const end = source.indexOf('*/', index + 2);
index = end === -1 ? source.length : end + 2;
continue;
}

result += character;
index += 1;
}

return result;
}

export function findCompatibilityViolations(bundle) {
const code = stripComments(bundle);
const violations = [];

for (const builtin of NODE_BUILTINS) {
const nodeImport = new RegExp(`\\b(?:require|import)\\s*\\(\\s*["']node:${builtin}["']\\s*\\)|\\bfrom\\s*["']node:${builtin}["']`);

Check warning on line 61 in scripts/check-obsidian-compat.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AZ_csQjjAtF8P_erihdN&open=AZ_csQjjAtF8P_erihdN&pullRequest=119
if (nodeImport.test(code)) violations.push(`node:${builtin}`);
}

const executableCode = code.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '');
if (/(?:^|[^.$\w])fetch\s*\(/.test(executableCode)) violations.push('native fetch');

return violations;
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
const bundlePath = new URL('../main.js', import.meta.url);
const bundle = await readFile(bundlePath, 'utf8');
const violations = findCompatibilityViolations(bundle);

if (violations.length > 0) {
console.error(`Obsidian compatibility check failed: ${violations.join(', ')}`);
process.exitCode = 1;
} else {
console.log('Obsidian compatibility check passed: main.js contains no prohibited Node imports or native fetch calls.');
}
}
2 changes: 1 addition & 1 deletion src/services/gitlab-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class GitLabService extends BaseGitService implements GitServiceInterface
await this.safeRequest(url, 'POST', { branch, commit_message: message, actions });

// The Commits API response doesn't include each file's new blob sha, so
// read it back via a single follow-up tree fetch (one extra call for the
// read it back via a single follow-up tree request (one extra call for the
// whole batch, not per file) rather than per-file getFile calls.
const freshTree = await this.listFilesDetailed(branch, false);
const shaByPath = new Map(freshTree.map(e => [e.path, e.sha]));
Expand Down
24 changes: 24 additions & 0 deletions tests/scripts/check-obsidian-compat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';
import { findCompatibilityViolations } from '../../scripts/check-obsidian-compat.mjs';

describe('findCompatibilityViolations', () => {
it('accepts a bundle with no Node built-ins or native fetch calls', () => {
expect(findCompatibilityViolations('const request = require("obsidian").requestUrl; request({ url: "https://example.test" });'))
.toEqual([]);
});

it('finds imported Node built-ins', () => {
expect(findCompatibilityViolations('const crypto = require("node:crypto");'))
.toEqual(['node:crypto']);
});

it('finds a direct native fetch call but not a property named fetch', () => {
expect(findCompatibilityViolations('fetch("https://example.test"); client.fetch("/path");'))
.toEqual(['native fetch']);
});

it('does not inspect comments', () => {
expect(findCompatibilityViolations('// require("node:crypto")\n// fetch("https://example.test")'))
.toEqual([]);
});
});
Loading