fix(plugins): tolerate a UTF-8 BOM in trek-plugin.json

Windows editors love to prepend a BOM, and a bare JSON.parse then dies
with an "Unexpected token" pointing at an invisible character - in the
SDK CLIs (dev/validate/entry/submit) and, worse, server-side: a BOM in
an author repo travels through pack into the artifact and fails
discovery and registry install. Strip it at every manifest/JSON read
(readJsonFile in the SDK, parseJsonText in the installer).
This commit is contained in:
Maurice
2026-07-04 20:09:59 +02:00
committed by Maurice
parent d110f8f0ce
commit 91217e83fa
11 changed files with 47 additions and 12 deletions
+3 -2
View File
@@ -14,6 +14,7 @@ import path from 'node:path';
import http from 'node:http';
import { createRequire } from 'node:module';
import * as sdk from '../index.js';
import { readJsonFile } from './json.js';
interface Fixtures {
config?: Record<string, unknown>;
@@ -123,7 +124,7 @@ export function installSdkInjection(): void {
function loadFixtures(dir: string): Fixtures {
const p = path.join(dir, 'dev-fixtures.json');
if (fs.existsSync(p)) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { console.warn('warning: dev-fixtures.json is not valid JSON — ignoring'); } }
if (fs.existsSync(p)) { try { return readJsonFile<Fixtures>(p); } catch { console.warn('warning: dev-fixtures.json is not valid JSON — ignoring'); } }
return {};
}
@@ -143,7 +144,7 @@ export async function runDev(dir: string, opts: { port?: number } = {}): Promise
const abs = path.resolve(dir);
const manifestPath = path.join(abs, 'trek-plugin.json');
if (!fs.existsSync(manifestPath)) throw new Error(`no trek-plugin.json in ${abs}`);
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;
const manifest = readJsonFile<Record<string, unknown>>(manifestPath);
const id = String(manifest.id);
const grants = new Set(Array.isArray(manifest.permissions) ? (manifest.permissions as string[]) : []);
const fx = loadFixtures(abs);
+3 -2
View File
@@ -14,6 +14,7 @@ import path from 'node:path';
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { loadPrivateKey, signArtifact, publicKeyBase64 } from './sign.js';
import { readJsonFile } from './json.js';
interface Version {
version: string; gitTag: string; commitSha: string; downloadUrl: string;
@@ -47,7 +48,7 @@ export function buildEntry(opts: {
/** Optional Ed25519 private-key file — signs the artifact and pins the author key. */
signKeyPath?: string;
}): Entry {
const manifest = JSON.parse(fs.readFileSync(path.join(opts.dir, 'trek-plugin.json'), 'utf8')) as Record<string, unknown>;
const manifest = readJsonFile<Record<string, unknown>>(path.join(opts.dir, 'trek-plugin.json'));
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(opts.repo)) throw new Error(`--repo must be "owner/name", got "${opts.repo}"`);
const minTrek = minTrekFrom(manifest.trek);
if (!minTrek) throw new Error('manifest has no "trek" version range to derive minTrekVersion from (e.g. "trek": ">=3.2.0 <4.0.0")');
@@ -77,7 +78,7 @@ export function buildEntry(opts: {
}
if (opts.mergePath) {
const existing = JSON.parse(fs.readFileSync(opts.mergePath, 'utf8')) as Entry;
const existing = readJsonFile<Entry>(opts.mergePath);
if (authorPublicKey && existing.authorPublicKey && existing.authorPublicKey !== authorPublicKey) {
throw new Error('this signing key differs from the one already published for this plugin — TREK would reject the update. Use the original key.');
}
+11
View File
@@ -0,0 +1,11 @@
import fs from 'node:fs';
/**
* Read + parse a JSON file, tolerating a UTF-8 BOM (0xFEFF) — Windows editors
* love to add one, and a bare JSON.parse then fails with a cryptic
* "Unexpected token" that points at an invisible character.
*/
export function readJsonFile<T = unknown>(p: string): T {
const text = fs.readFileSync(p, 'utf8');
return JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text) as T;
}
+2 -1
View File
@@ -10,6 +10,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { readJsonFile } from './json.js';
const DEFAULT_REGISTRY = 'mauriceboe/TREK-Plugins';
@@ -69,7 +70,7 @@ export function submitEntry(entry: EntryLike, opts: { registry?: string; branch?
let toWrite = entry;
let action = 'add';
if (fs.existsSync(abs)) {
const existing = JSON.parse(fs.readFileSync(abs, 'utf8')) as EntryLike;
const existing = readJsonFile<EntryLike>(abs);
toWrite = mergeOnto(existing, entry);
action = 'update';
}
+2 -1
View File
@@ -33,6 +33,7 @@ import { preflight } from './preflight.js';
import { submitEntry } from './submit.js';
import { publishPlugin } from './publish.js';
import { generateKeypair, loadPrivateKey, signArtifact, publicKeyBase64, defaultKeyPath } from './sign.js';
import { readJsonFile } from './json.js';
const [cmd, ...args] = process.argv.slice(2);
@@ -124,7 +125,7 @@ async function main(): Promise<void> {
}
} else if (cmd === 'preflight') {
const entry = flags.entry
? JSON.parse(fs.readFileSync(flags.entry, 'utf8'))
? readJsonFile<ReturnType<typeof buildEntry>>(flags.entry)
: (flags.repo && flags.tag
? buildEntry({ dir: pos[0] || '.', repo: flags.repo, tag: flags.tag, zipPath: flags.zip || 'plugin.zip', commit: flags.commit, signKeyPath: signKey(), now: new Date().toISOString() })
: fail('preflight needs --repo <owner/name> --tag <vX>, or --entry <file.json>'));
+2 -1
View File
@@ -8,6 +8,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { validateManifest } from '../manifest.js';
import { readJsonFile } from './json.js';
export interface ValidateReport {
ok: boolean;
@@ -24,7 +25,7 @@ export function validatePluginDir(dir: string): ValidateReport {
let manifestId = '';
try {
const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const raw = readJsonFile<Record<string, unknown>>(manifestPath);
const res = validateManifest(raw);
errors.push(...res.errors);
manifestId = res.manifest?.id ?? String(raw.id ?? '');