You've already forked Nitro_Render_V3
mirror of
https://github.com/duckietm/Nitro_Render_V3.git
synced 2026-06-19 23:16:20 +00:00
🆕 Added support for JSON5
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import JSON5 from 'json5';
|
||||
|
||||
const JSON5_EXTENSION = /\.json5(?:[?#]|$)/i;
|
||||
const JSON5_MIME = /(?:application|text)\/(?:json5|x-json5)/i;
|
||||
|
||||
const looksLikeJson5Url = (url: string): boolean => !!url && JSON5_EXTENSION.test(url);
|
||||
|
||||
const looksLikeJson5ContentType = (contentType: string): boolean => !!contentType && JSON5_MIME.test(contentType);
|
||||
|
||||
const formatParseError = (sourceUrl: string, strictError: unknown, json5Error: unknown): string =>
|
||||
{
|
||||
const strictMessage = (strictError as Error)?.message || String(strictError);
|
||||
const json5Message = (json5Error as Error)?.message || String(json5Error);
|
||||
const source = sourceUrl ? ` in "${ sourceUrl }"` : '';
|
||||
|
||||
if(strictMessage === json5Message) return `Failed to parse JSON/JSON5${ source } — ${ json5Message }`;
|
||||
|
||||
return `Failed to parse JSON/JSON5${ source } — JSON5: ${ json5Message } (strict JSON: ${ strictMessage })`;
|
||||
};
|
||||
|
||||
export const parseConfigJson = <T = any>(text: string, sourceUrl: string = ''): T =>
|
||||
{
|
||||
if(text === null || text === undefined) throw new Error(`Empty response${ sourceUrl ? ` for "${ sourceUrl }"` : '' }`);
|
||||
|
||||
const trimmed = text.length > 0 ? text : '';
|
||||
|
||||
if(looksLikeJson5Url(sourceUrl))
|
||||
{
|
||||
try
|
||||
{
|
||||
return JSON5.parse<T>(trimmed);
|
||||
}
|
||||
catch(err)
|
||||
{
|
||||
throw new Error(formatParseError(sourceUrl, err, err));
|
||||
}
|
||||
}
|
||||
|
||||
let strictError: unknown;
|
||||
|
||||
try
|
||||
{
|
||||
return JSON.parse(trimmed) as T;
|
||||
}
|
||||
catch(err)
|
||||
{
|
||||
strictError = err;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JSON5.parse<T>(trimmed);
|
||||
}
|
||||
catch(json5Error)
|
||||
{
|
||||
throw new Error(formatParseError(sourceUrl, strictError, json5Error));
|
||||
}
|
||||
};
|
||||
|
||||
export const parseConfigJsonFromResponse = async <T = any>(response: Response, sourceUrl: string = ''): Promise<T> =>
|
||||
{
|
||||
const contentType = response.headers?.get?.('content-type') || '';
|
||||
const text = await response.text();
|
||||
const url = sourceUrl || (response as any).url || '';
|
||||
|
||||
if(looksLikeJson5ContentType(contentType) && !looksLikeJson5Url(url))
|
||||
{
|
||||
try
|
||||
{
|
||||
return JSON5.parse<T>(text);
|
||||
}
|
||||
catch(err)
|
||||
{
|
||||
throw new Error(formatParseError(url, err, err));
|
||||
}
|
||||
}
|
||||
|
||||
return parseConfigJson<T>(text, url);
|
||||
};
|
||||
|
||||
export const fetchConfigJson = async <T = any>(url: string, init?: RequestInit): Promise<T> =>
|
||||
{
|
||||
const response = await fetch(url, init);
|
||||
|
||||
if(!response || response.status !== 200) throw new Error(`Failed to fetch "${ url }" — server returned HTTP ${ response?.status ?? 'no response' }`);
|
||||
|
||||
return parseConfigJsonFromResponse<T>(response, url);
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { fetchConfigJson, parseConfigJson, parseConfigJsonFromResponse } from '../JsonParser';
|
||||
|
||||
describe('parseConfigJson', () =>
|
||||
{
|
||||
it('parses strict JSON', () =>
|
||||
{
|
||||
const result = parseConfigJson('{"a": 1, "b": [2, 3]}');
|
||||
expect(result).toEqual({ a: 1, b: [ 2, 3 ] });
|
||||
});
|
||||
|
||||
it('falls back to JSON5 for trailing commas', () =>
|
||||
{
|
||||
const result = parseConfigJson('{"a": 1, "b": [2, 3,],}');
|
||||
expect(result).toEqual({ a: 1, b: [ 2, 3 ] });
|
||||
});
|
||||
|
||||
it('falls back to JSON5 for comments', () =>
|
||||
{
|
||||
const result = parseConfigJson(`{
|
||||
// a number
|
||||
"a": 1,
|
||||
/* a list */
|
||||
"b": [2, 3]
|
||||
}`);
|
||||
expect(result).toEqual({ a: 1, b: [ 2, 3 ] });
|
||||
});
|
||||
|
||||
it('falls back to JSON5 for unquoted keys and single quotes', () =>
|
||||
{
|
||||
const result = parseConfigJson("{ a: 1, b: 'hello' }");
|
||||
expect(result).toEqual({ a: 1, b: 'hello' });
|
||||
});
|
||||
|
||||
it('uses JSON5 directly for .json5 URLs', () =>
|
||||
{
|
||||
const result = parseConfigJson('{ a: 1, /* hi */ b: 2 }', 'https://example.com/cfg.json5');
|
||||
expect(result).toEqual({ a: 1, b: 2 });
|
||||
});
|
||||
|
||||
it('throws a helpful error when both strict and JSON5 fail', () =>
|
||||
{
|
||||
expect(() => parseConfigJson('{ this is :: not json ::', 'cfg.json'))
|
||||
.toThrowError(/Failed to parse JSON\/JSON5 in "cfg\.json"/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseConfigJsonFromResponse', () =>
|
||||
{
|
||||
const buildResponse = (body: string, contentType = 'application/json', url = 'https://example.com/x.json'): Response =>
|
||||
{
|
||||
const headers = new Headers({ 'content-type': contentType });
|
||||
return new Response(body, { status: 200, headers });
|
||||
};
|
||||
|
||||
it('parses JSON response bodies', async () =>
|
||||
{
|
||||
const res = buildResponse('{"a": 1}');
|
||||
await expect(parseConfigJsonFromResponse(res, 'https://example.com/x.json')).resolves.toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
it('parses JSON5 response bodies with comments', async () =>
|
||||
{
|
||||
const res = buildResponse('{ /* yo */ a: 1, b: 2, }');
|
||||
await expect(parseConfigJsonFromResponse(res, 'https://example.com/x.json')).resolves.toEqual({ a: 1, b: 2 });
|
||||
});
|
||||
|
||||
it('respects application/json5 content-type', async () =>
|
||||
{
|
||||
const res = buildResponse('{ a: 1 }', 'application/json5');
|
||||
await expect(parseConfigJsonFromResponse(res, 'https://example.com/x.txt')).resolves.toEqual({ a: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchConfigJson', () =>
|
||||
{
|
||||
it('fetches and parses JSON or JSON5', async () =>
|
||||
{
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () => new Response('{ a: 1, b: 2, }', {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' }
|
||||
})) as any;
|
||||
|
||||
try
|
||||
{
|
||||
await expect(fetchConfigJson('https://example.com/cfg.json')).resolves.toEqual({ a: 1, b: 2 });
|
||||
}
|
||||
finally
|
||||
{
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
it('throws for non-200 responses', async () =>
|
||||
{
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () => new Response('', { status: 404 })) as any;
|
||||
|
||||
try
|
||||
{
|
||||
await expect(fetchConfigJson('https://example.com/missing.json')).rejects.toThrowError(/HTTP 404/);
|
||||
}
|
||||
finally
|
||||
{
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ export * from './GetTickerFPS';
|
||||
export * from './GetTickerTime';
|
||||
export * from './HabboWebTools';
|
||||
export * from './Int32';
|
||||
export * from './JsonParser';
|
||||
export * from './LegacyExternalInterface';
|
||||
export * from './LinkTracker';
|
||||
export * from './Matrix4x4';
|
||||
|
||||
Reference in New Issue
Block a user