diff --git a/eslint.config.mts b/eslint.config.mts index 8c1ee63..78c86b7 100644 --- a/eslint.config.mts +++ b/eslint.config.mts @@ -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", diff --git a/package.json b/package.json index 77e1bb8..b1a2a23 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/check-obsidian-compat.mjs b/scripts/check-obsidian-compat.mjs new file mode 100644 index 0000000..180fd23 --- /dev/null +++ b/scripts/check-obsidian-compat.mjs @@ -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) { + 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}["']`); + 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.'); + } +} diff --git a/src/services/gitlab-service.ts b/src/services/gitlab-service.ts index 514370d..610e8ee 100644 --- a/src/services/gitlab-service.ts +++ b/src/services/gitlab-service.ts @@ -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])); diff --git a/tests/scripts/check-obsidian-compat.test.ts b/tests/scripts/check-obsidian-compat.test.ts new file mode 100644 index 0000000..4ae356f --- /dev/null +++ b/tests/scripts/check-obsidian-compat.test.ts @@ -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([]); + }); +});