Skip to content

Commit 840a72d

Browse files
committed
refactor: migrate RiftJS codebase from JavaScript to TypeScript
Convert the library source from ad-hoc CommonJS JavaScript files to a typed TypeScript source layout under src/, while preserving runtime package compatibility for Node consumers and npm publishing. Source architecture changes: - Replace legacy runtime entrypoint and endpoint modules (index.js, endpoints/riot.js, endpoints/datadragon.js) with TypeScript equivalents in src/. - Introduce src/types.ts as a shared contract layer for region enums, endpoint method interfaces, match/rank option models, and factory wiring types. - Keep Riot and Data Dragon logic modular, but make signatures explicit and strongly typed to improve editor inference and API discoverability. - Improve internal error handling typing paths (Axios-aware narrowing and unknown fallback handling). Build + packaging pipeline: - Add tsconfig.json with strict type checking, CommonJS output target, declaration emit, and dist/ output directory. - Add npm build script (tsc -p tsconfig.json) and prepublishOnly hook to enforce compilation before publish. - Repoint package entry metadata from source JS to compiled artifacts: - main -> dist/index.js - types -> dist/index.d.ts - exports map for typed require() consumers - Add files whitelist for publish payload (dist/, README.md, LICENSE). - Add TypeScript toolchain dev dependencies (@types/node, typescript). Distribution artifact updates: - Add compiled dist JavaScript output and generated declaration files for all exported/public modules. - Ensure package consumers can continue requiring the library from CommonJS while TS projects receive first-class typings. Test harness migration: - Port test-endpoints.js to src/test-endpoints.ts. - Update test script flow to build first, then execute dist/test-endpoints.js. - Preserve existing endpoint coverage semantics and skip behavior while adapting access patterns to typed payloads. Documentation updates: - Update README to explicitly state TypeScript support. - Update project structure references from /endpoints to src/endpoints. - Document new local dev flow including explicit build step before endpoint tests. Net effect: This commit completes a full TypeScript refactor for the codebase, establishes a compile/publish pipeline suitable for npm distribution, and preserves runtime API shape for existing Node.js users while adding typed DX for TS consumers.
1 parent 14f94da commit 840a72d

22 files changed

Lines changed: 1079 additions & 391 deletions

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ A lightweight Node.js wrapper for the Riot Games API, providing easy access to L
77

88
## Overview
99

10-
RiftJS simplifies interaction with the Riot Games API and DataDragon static data. It supports fetching account details, summoner info, match history, and game data using a modular endpoint structure. Built with `axios` and `dotenv`, it’s designed for developers building League of Legends tools or applications.
10+
RiftJS simplifies interaction with the Riot Games API and DataDragon static data. It supports fetching account details, summoner info, match history, and game data using a modular endpoint structure. Built with `axios`, `dotenv`, and TypeScript, it’s designed for developers building League of Legends tools or applications.
1111

1212
## Features
1313

1414
- **RiotAPI**: Fetch account data by Riot ID, summoner data by PUUID, match history, and match details.
1515
- **DataDragon**: Access static game data like champions and items.
1616
- **Region Support**: Handles platform (e.g., `EUW1`) and shard (e.g., `europe`) routing.
17-
- **Modular Design**: Endpoints are organized in an `/endpoints/` directory for easy extension.
17+
- **Modular Design**: Endpoints are organized in `src/endpoints/` for easy extension.
18+
- **TypeScript Types**: The package ships with declaration files for typed usage in TS projects.
1819

1920
## Installation
2021

@@ -178,7 +179,13 @@ To contribute or run locally:
178179
```
179180

180181
3. Create a `.env` file (see [Setup](#setup)).
181-
4. Run endpoint checks:
182+
4. Build the package:
183+
184+
```bash
185+
npm run build
186+
```
187+
188+
5. Run endpoint checks:
182189

183190
```bash
184191
npm test

dist/endpoints/datadragon.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import type { DataDragonEndpointMethods } from '../types';
2+
type BaseURLResolver = string | (() => string | Promise<string>);
3+
export default function dataDragonEndpoints(baseURLOrResolver: BaseURLResolver): DataDragonEndpointMethods;
4+
export {};

dist/endpoints/datadragon.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use strict";
2+
var __importDefault = (this && this.__importDefault) || function (mod) {
3+
return (mod && mod.__esModule) ? mod : { "default": mod };
4+
};
5+
Object.defineProperty(exports, "__esModule", { value: true });
6+
exports.default = dataDragonEndpoints;
7+
const axios_1 = __importDefault(require("axios"));
8+
function dataDragonEndpoints(baseURLOrResolver) {
9+
const resolveBaseURL = async () => {
10+
if (typeof baseURLOrResolver === 'function') {
11+
return baseURLOrResolver();
12+
}
13+
return baseURLOrResolver;
14+
};
15+
return {
16+
async getChampions() {
17+
try {
18+
const baseURL = await resolveBaseURL();
19+
const response = await axios_1.default.get(`${baseURL}/champion.json`);
20+
return response.data;
21+
}
22+
catch (error) {
23+
const message = error instanceof Error ? error.message : 'Unknown DataDragon error';
24+
throw new Error(`DataDragon error: ${message}`);
25+
}
26+
},
27+
async getItems() {
28+
try {
29+
const baseURL = await resolveBaseURL();
30+
const response = await axios_1.default.get(`${baseURL}/item.json`);
31+
return response.data;
32+
}
33+
catch (error) {
34+
const message = error instanceof Error ? error.message : 'Unknown DataDragon error';
35+
throw new Error(`DataDragon error: ${message}`);
36+
}
37+
},
38+
};
39+
}

dist/endpoints/riot.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import type { RiotEndpointMethods, RiotEndpointsFactoryArgs } from '../types';
2+
export default function riotEndpoints({ client, defaultRegion, regionMap, handleError, }: RiotEndpointsFactoryArgs): RiotEndpointMethods;

dist/endpoints/riot.js

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
exports.default = riotEndpoints;
4+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5+
const SOLO_QUEUE = 'RANKED_SOLO_5x5';
6+
const FLEX_QUEUE = 'RANKED_FLEX_SR';
7+
const withRankMetrics = (entry) => {
8+
if (!entry)
9+
return null;
10+
const wins = Number(entry.wins) || 0;
11+
const losses = Number(entry.losses) || 0;
12+
const gamesPlayed = wins + losses;
13+
const winRate = gamesPlayed > 0 ? Number(((wins / gamesPlayed) * 100).toFixed(2)) : 0;
14+
return { ...entry, winRate };
15+
};
16+
const coerceRegion = (region, regionMap) => {
17+
const upper = region.toUpperCase();
18+
if (!regionMap[upper])
19+
throw new Error(`Invalid region: ${region}`);
20+
return upper;
21+
};
22+
function riotEndpoints({ client, defaultRegion, regionMap, handleError, }) {
23+
const getAccountByRiotId = async (riotId, tagLine = null, region = defaultRegion) => {
24+
const resolvedRegion = coerceRegion(region, regionMap);
25+
let gameName;
26+
let tag;
27+
if (riotId.includes('#')) {
28+
[gameName, tag] = riotId.split('#');
29+
}
30+
else {
31+
gameName = riotId;
32+
tag = tagLine || '';
33+
}
34+
if (!tag)
35+
throw new Error('TagLine is required for getAccountByRiotId');
36+
const shard = regionMap[resolvedRegion].shard;
37+
try {
38+
const response = await client.get(`/riot/account/v1/accounts/by-riot-id/${encodeURIComponent(gameName)}/${encodeURIComponent(tag)}`, { baseURL: `https://${shard}` });
39+
return response.data;
40+
}
41+
catch (error) {
42+
throw handleError(error);
43+
}
44+
};
45+
const getSummonerByPuuid = async (puuid, region = defaultRegion) => {
46+
const resolvedRegion = coerceRegion(region, regionMap);
47+
const platform = regionMap[resolvedRegion].platform;
48+
try {
49+
const response = await client.get(`/lol/summoner/v4/summoners/by-puuid/${encodeURIComponent(puuid)}`, { baseURL: `https://${platform}` });
50+
return response.data;
51+
}
52+
catch (error) {
53+
throw handleError(error);
54+
}
55+
};
56+
const getRankEntriesByPuuid = async (puuid, region = defaultRegion) => {
57+
const resolvedRegion = coerceRegion(region, regionMap);
58+
const platform = regionMap[resolvedRegion].platform;
59+
try {
60+
const response = await client.get(`/lol/league/v4/entries/by-puuid/${encodeURIComponent(puuid)}`, {
61+
baseURL: `https://${platform}`,
62+
});
63+
return response.data;
64+
}
65+
catch (error) {
66+
throw handleError(error);
67+
}
68+
};
69+
const getRankByPuuid = async (puuid, region = defaultRegion) => {
70+
const entries = await getRankEntriesByPuuid(puuid, region);
71+
const solo = withRankMetrics(entries.find((entry) => entry.queueType === SOLO_QUEUE) || null);
72+
const flex = withRankMetrics(entries.find((entry) => entry.queueType === FLEX_QUEUE) || null);
73+
return { solo, flex, entries };
74+
};
75+
const getMatchlistByPuuid = async (puuid, options = {}, region = defaultRegion) => {
76+
const resolvedRegion = coerceRegion(region, regionMap);
77+
const shard = regionMap[resolvedRegion].shard;
78+
try {
79+
const response = await client.get(`/lol/match/v5/matches/by-puuid/${encodeURIComponent(puuid)}/ids`, {
80+
baseURL: `https://${shard}`,
81+
params: options,
82+
});
83+
return response.data;
84+
}
85+
catch (error) {
86+
throw handleError(error);
87+
}
88+
};
89+
const getMatchById = async (matchId, region = defaultRegion) => {
90+
const resolvedRegion = coerceRegion(region, regionMap);
91+
const shard = regionMap[resolvedRegion].shard;
92+
try {
93+
const response = await client.get(`/lol/match/v5/matches/${matchId}`, {
94+
baseURL: `https://${shard}`,
95+
});
96+
return response.data;
97+
}
98+
catch (error) {
99+
throw handleError(error);
100+
}
101+
};
102+
const getMatchTimelineById = async (matchId, region = defaultRegion) => {
103+
const resolvedRegion = coerceRegion(region, regionMap);
104+
const shard = regionMap[resolvedRegion].shard;
105+
try {
106+
const response = await client.get(`/lol/match/v5/matches/${matchId}/timeline`, {
107+
baseURL: `https://${shard}`,
108+
});
109+
return response.data;
110+
}
111+
catch (error) {
112+
throw handleError(error);
113+
}
114+
};
115+
const getMatchlistByPuuidAll = async (puuid, options = {}, region = defaultRegion, pacing = {}) => {
116+
const baseStart = Number.isInteger(options.start) ? options.start : 0;
117+
const pageDelayMs = Number.isInteger(pacing.delayMs) ? pacing.delayMs : 1250;
118+
const maxMatches = Number.isInteger(pacing.maxMatches) && Number(pacing.maxMatches) >= 0 ? Number(pacing.maxMatches) : null;
119+
const filters = {
120+
startTime: options.startTime,
121+
endTime: options.endTime,
122+
queue: options.queue,
123+
type: options.type,
124+
};
125+
let start = baseStart;
126+
const allMatchIds = [];
127+
while (true) {
128+
const remaining = maxMatches === null ? 100 : Math.min(100, maxMatches - allMatchIds.length);
129+
if (remaining <= 0)
130+
break;
131+
const page = await getMatchlistByPuuid(puuid, { ...filters, start, count: remaining }, region);
132+
allMatchIds.push(...page);
133+
if (page.length < remaining)
134+
break;
135+
start += page.length;
136+
if (pageDelayMs > 0)
137+
await sleep(pageDelayMs);
138+
}
139+
return allMatchIds;
140+
};
141+
const getMatchesWithDetailsByPuuid = async (puuid, options = {}, region = defaultRegion, pacing = {}) => {
142+
const pageDelayMs = Number.isInteger(pacing.pageDelayMs) ? pacing.pageDelayMs : 1250;
143+
const detailDelayMs = Number.isInteger(pacing.detailDelayMs) ? pacing.detailDelayMs : 1250;
144+
const maxMatches = Number.isInteger(pacing.maxMatches) && Number(pacing.maxMatches) >= 0 ? Number(pacing.maxMatches) : null;
145+
const matchIds = await getMatchlistByPuuidAll(puuid, options, region, { delayMs: pageDelayMs, maxMatches });
146+
const matches = [];
147+
for (let i = 0; i < matchIds.length; i += 1) {
148+
matches.push(await getMatchById(matchIds[i], region));
149+
if (detailDelayMs > 0 && i < matchIds.length - 1) {
150+
await sleep(detailDelayMs);
151+
}
152+
}
153+
return { matchIds, matches };
154+
};
155+
return {
156+
getAccountByRiotId,
157+
getSummonerByPuuid,
158+
getRankEntriesByPuuid,
159+
getRankByPuuid,
160+
getMatchlistByPuuid,
161+
getMatchById,
162+
getMatchTimelineById,
163+
getMatchlistByPuuidAll,
164+
getMatchesWithDetailsByPuuid,
165+
};
166+
}

dist/index.d.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'dotenv/config';
2+
import type { DataDragonEndpointMethods, RegionCode, RiotEndpointMethods } from './types';
3+
export declare class RiotAPI implements RiotEndpointMethods {
4+
readonly apiKey: string;
5+
readonly region: RegionCode;
6+
private readonly client;
7+
getAccountByRiotId: RiotEndpointMethods['getAccountByRiotId'];
8+
getSummonerByPuuid: RiotEndpointMethods['getSummonerByPuuid'];
9+
getRankEntriesByPuuid: RiotEndpointMethods['getRankEntriesByPuuid'];
10+
getRankByPuuid: RiotEndpointMethods['getRankByPuuid'];
11+
getMatchlistByPuuid: RiotEndpointMethods['getMatchlistByPuuid'];
12+
getMatchById: RiotEndpointMethods['getMatchById'];
13+
getMatchTimelineById: RiotEndpointMethods['getMatchTimelineById'];
14+
getMatchlistByPuuidAll: RiotEndpointMethods['getMatchlistByPuuidAll'];
15+
getMatchesWithDetailsByPuuid: RiotEndpointMethods['getMatchesWithDetailsByPuuid'];
16+
constructor();
17+
private _handleError;
18+
}
19+
export declare class DataDragon implements DataDragonEndpointMethods {
20+
version: string | null;
21+
locale: string;
22+
private baseURL;
23+
private baseURLPromise;
24+
getChampions: DataDragonEndpointMethods['getChampions'];
25+
getItems: DataDragonEndpointMethods['getItems'];
26+
constructor(version?: string | null, locale?: string);
27+
private resolveBaseURL;
28+
}

dist/index.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"use strict";
2+
var __importDefault = (this && this.__importDefault) || function (mod) {
3+
return (mod && mod.__esModule) ? mod : { "default": mod };
4+
};
5+
Object.defineProperty(exports, "__esModule", { value: true });
6+
exports.DataDragon = exports.RiotAPI = void 0;
7+
require("dotenv/config");
8+
const axios_1 = __importDefault(require("axios"));
9+
const datadragon_1 = __importDefault(require("./endpoints/datadragon"));
10+
const riot_1 = __importDefault(require("./endpoints/riot"));
11+
const regionMap = {
12+
BR1: { platform: 'br1.api.riotgames.com', shard: 'americas.api.riotgames.com' },
13+
EUN1: { platform: 'eun1.api.riotgames.com', shard: 'europe.api.riotgames.com' },
14+
EUW1: { platform: 'euw1.api.riotgames.com', shard: 'europe.api.riotgames.com' },
15+
JP1: { platform: 'jp1.api.riotgames.com', shard: 'asia.api.riotgames.com' },
16+
KR: { platform: 'kr.api.riotgames.com', shard: 'asia.api.riotgames.com' },
17+
LA1: { platform: 'la1.api.riotgames.com', shard: 'americas.api.riotgames.com' },
18+
LA2: { platform: 'la2.api.riotgames.com', shard: 'americas.api.riotgames.com' },
19+
NA1: { platform: 'na1.api.riotgames.com', shard: 'americas.api.riotgames.com' },
20+
OC1: { platform: 'oc1.api.riotgames.com', shard: 'sea.api.riotgames.com' },
21+
TR1: { platform: 'tr1.api.riotgames.com', shard: 'europe.api.riotgames.com' },
22+
RU: { platform: 'ru.api.riotgames.com', shard: 'europe.api.riotgames.com' },
23+
PH2: { platform: 'ph2.api.riotgames.com', shard: 'sea.api.riotgames.com' },
24+
SG2: { platform: 'sg2.api.riotgames.com', shard: 'sea.api.riotgames.com' },
25+
TH2: { platform: 'th2.api.riotgames.com', shard: 'sea.api.riotgames.com' },
26+
TW2: { platform: 'tw2.api.riotgames.com', shard: 'sea.api.riotgames.com' },
27+
VN2: { platform: 'vn2.api.riotgames.com', shard: 'sea.api.riotgames.com' },
28+
};
29+
const parseRegion = (region) => {
30+
const upper = region.toUpperCase();
31+
if (!regionMap[upper])
32+
throw new Error(`Invalid region: ${region}`);
33+
return upper;
34+
};
35+
class RiotAPI {
36+
constructor() {
37+
this._handleError = (error) => {
38+
if (axios_1.default.isAxiosError(error)) {
39+
if (error.response) {
40+
const status = error.response.status;
41+
const data = error.response.data;
42+
return new Error(`API error ${status}: ${data?.status?.message || 'Unknown error'}`);
43+
}
44+
if (error.request) {
45+
return new Error('No response received from the server');
46+
}
47+
return new Error(`Request error: ${error.message}`);
48+
}
49+
if (error instanceof Error) {
50+
return new Error(`Request error: ${error.message}`);
51+
}
52+
return new Error('Request error: Unknown error');
53+
};
54+
this.apiKey = process.env.RIOT_API_KEY || '';
55+
if (!this.apiKey)
56+
throw new Error('RIOT_API_KEY is required in .env');
57+
this.region = parseRegion(process.env.REGION || 'EUW1');
58+
this.client = axios_1.default.create({
59+
headers: { 'X-Riot-Token': this.apiKey },
60+
});
61+
Object.assign(this, (0, riot_1.default)({
62+
client: this.client,
63+
defaultRegion: this.region,
64+
regionMap,
65+
handleError: this._handleError,
66+
}));
67+
}
68+
}
69+
exports.RiotAPI = RiotAPI;
70+
class DataDragon {
71+
constructor(version = null, locale = 'en_US') {
72+
this.version = version;
73+
this.locale = locale;
74+
this.baseURL = null;
75+
this.baseURLPromise = null;
76+
Object.assign(this, (0, datadragon_1.default)(() => this.resolveBaseURL()));
77+
}
78+
async resolveBaseURL() {
79+
if (this.baseURL)
80+
return this.baseURL;
81+
if (this.baseURLPromise)
82+
return this.baseURLPromise;
83+
this.baseURLPromise = (async () => {
84+
if (!this.version) {
85+
const response = await axios_1.default.get('https://ddragon.leagueoflegends.com/api/versions.json');
86+
const latestVersion = Array.isArray(response.data) ? response.data[0] : null;
87+
if (!latestVersion) {
88+
throw new Error('Could not resolve latest Data Dragon version');
89+
}
90+
this.version = latestVersion;
91+
}
92+
this.baseURL = `https://ddragon.leagueoflegends.com/cdn/${this.version}/data/${this.locale}`;
93+
return this.baseURL;
94+
})();
95+
try {
96+
return await this.baseURLPromise;
97+
}
98+
finally {
99+
this.baseURLPromise = null;
100+
}
101+
}
102+
}
103+
exports.DataDragon = DataDragon;

dist/test-endpoints.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import 'dotenv/config';

0 commit comments

Comments
 (0)