diff --git a/.gitignore b/.gitignore index 1da96fa..b2c43af 100644 --- a/.gitignore +++ b/.gitignore @@ -107,4 +107,7 @@ dist # TernJS port file .tern-port +# VS code +.vscode/ + .DS_Store diff --git a/src/docsStore.ts b/src/docsStore.ts new file mode 100644 index 0000000..e9ccbf6 --- /dev/null +++ b/src/docsStore.ts @@ -0,0 +1,61 @@ +import { IParsed } from "./interfaces"; +import { fetchSampInc, fetchActorInc, fetchHttpInc, fetchNPCInc, fetchObjectsInc, fetchPlayersInc, fetchSampDBInc, fetchVehiclesInc } from './requests'; +import { parseInclude } from './parser'; +import ora from "ora"; + +export class DocsStore { + private constructor( + public readonly a_samp: IParsed, + public readonly a_actor: IParsed, + public readonly a_http: IParsed, + public readonly a_npc: IParsed, + public readonly a_objects: IParsed, + public readonly a_players: IParsed, + public readonly a_sampdb: IParsed, + public readonly a_vehicles: IParsed, + ) {} + + public static async fromSampStdlib() { + const parsingSpinner = ora('Parsing docs from samp-stdlib...').start(); + + // A_SAMP + const a_sampPromise = fetchSampInc().then(data => parseInclude(data, true, true)); + // A_ACTOR + const a_actorPromise = fetchActorInc().then(data => parseInclude(data, true, true)); + // A_HTTP + const a_httpPromise = fetchHttpInc().then(data => parseInclude(data, true, true)); + + // A_NPC + const a_npcPromise = fetchNPCInc().then(data => parseInclude(data, false, true)); + // Removed because it contains most of the things a_samp already has and you shouldn't include both in a pawn gamemode either + + // A_OBJECTS + const a_objectsPromise = fetchObjectsInc().then(data => parseInclude(data, true, true)); + // A_PLAYERS + const a_playersPromise = fetchPlayersInc().then(data => parseInclude(data, true, true)); + // A_SAMPDB + const a_sampdbPromise = fetchSampDBInc().then(data => parseInclude(data, false, true)); + // A_VEHICLES + const a_vehiclesPromise = fetchVehiclesInc().then(data => parseInclude(data, true, true)); + + const results = await Promise.all([ + a_sampPromise, + a_actorPromise, + a_httpPromise, + a_npcPromise, + a_objectsPromise, + a_playersPromise, + a_sampdbPromise, + a_vehiclesPromise, + ]); + + parsingSpinner.succeed("Parsing docs finished."); + + return new DocsStore(...results); + } +} + +// Equals to `"a_samp" | "a_actor" | ...` etc. +export type ParsedModules = { + [P in keyof DocsStore]: DocsStore[P] extends IParsed ? P : never; +}[keyof DocsStore]; diff --git a/src/enums/paths.ts b/src/enums/paths.ts index c931a71..bcda6bb 100644 --- a/src/enums/paths.ts +++ b/src/enums/paths.ts @@ -2,7 +2,10 @@ export enum EPaths { TEMPLATE_EVENTS = './src/templates/events.d.ts.hjs', TEMPLATE_SAMP = './src/templates/samp.d.ts.hjs', TEMPLATE_GLOBALS = './src/templates/globals.d.ts.hjs', - GENERATED_EVENTS = './generated/events.d.ts', - GENERATED_SAMP = './generated/samp.d.ts', - GENERATED_GLOBALS = './generated/globals.d.ts', + TEMPLATE_WRAPPERS = './src/templates/wrappers/wrappers.ts.hjs', + TEMPLATE_WRAPPERS_INDEX = './src/templates/wrappers/index.ts.hjs', + GENERATED_EVENT_TYPES = './generated/types/events.d.ts', + GENERATED_SAMP_TYPES = './generated/types/samp.d.ts', + GENERATED_GLOBAL_TYPES = './generated/types/globals.d.ts', + GENERATED_WRAPPERS_FOLDER = './generated/wrappers', } diff --git a/src/generators/globals.ts b/src/generators/globals.ts index 91b200c..a678dd8 100644 --- a/src/generators/globals.ts +++ b/src/generators/globals.ts @@ -16,7 +16,7 @@ export const generateGlobalConstants = async () => { }; const template = Handlebars.compile(await fs.readFile(EPaths.TEMPLATE_GLOBALS, 'utf8')); - await fs.outputFile(EPaths.GENERATED_GLOBALS, template({ globalConstants })); + await fs.outputFile(EPaths.GENERATED_GLOBAL_TYPES, template({ globalConstants })); }; export const applyFixes = () => {}; // Apply fixes to generated code diff --git a/src/generators/handlebarsHelper.ts b/src/generators/handlebarsHelper.ts new file mode 100644 index 0000000..7b85fd6 --- /dev/null +++ b/src/generators/handlebarsHelper.ts @@ -0,0 +1,78 @@ +import { IParam, IPawnDoc } from '../interfaces'; +import Handlebars from 'handlebars'; + +export function initHandlerbars() { + Handlebars.registerHelper({ + eq: (v1, v2) => v1 === v2, + ne: (v1, v2) => v1 !== v2, + lt: (v1, v2) => v1 < v2, + gt: (v1, v2) => v1 > v2, + lte: (v1, v2) => v1 <= v2, + gte: (v1, v2) => v1 >= v2, + and: (v1, v2) => v1 && v2, + or: (v1, v2) => v1 || v2, + }); + + Handlebars.registerHelper({ + // has variadic params + hvp: (pa: Array) => pa.some(p => p.isVariadic), + // specifier values string + svs: (pa: Array) => pa.map(p => p.isReference ? p.type.toUpperCase() : p.type).join(''), + // specifier values without references + svw: (pa: Array) => pa.filter(p => !p.isReference), + // specifier values without references length + svwl: (pa: Array) => pa.filter(p => !p.isReference).length, + // reference values + rv: (pa: Array) => pa.filter(p => p.isReference), + // reference values string + rvs: (pa: Array) => pa.filter(p => p.isReference).map(p => p.type.toUpperCase()).join(''), + // reference values length + rvl: (pa: Array) => pa.filter(p => p.isReference).length, + + // pawndoc param description + ppd: (pd: IPawnDoc, pa: IParam) => pd.param?.find(pdpa => pdpa.name === pa.name)?.description, + // typescript type for specifier + tts: (t: IParam['type'], b = false) => { + const type = referenceToTsType(t); + return b ? `{${type}}` : type; + }, + restriction: (param: IParam) => { + if (param.type === 'i' || param.type === 'd') { + return "Must be a whole number." + } else if (param.type === 'a') { + return "All numbers must be whole"; + } + return null; + }, + outputtype: (params: Array) => { + const types = params.filter(p => p.isReference).map(r => referenceToTsType(r.type)); + if (!types.length) { + return "number"; + } + if (types.length === 1) { + return types[0]; + } + return `[${types.join(", ")}]`; + } + }); +} + +function referenceToTsType(t: IParam['type']) { + switch (t) { + case 's': + case 'S': + return "string"; + case 'f': + case 'F': + case 'd': + case 'D': + case 'i': + case 'I': + return "number"; + case 'a': + case 'A': + case 'v': + case 'V': + return "Array"; + } +} diff --git a/src/generators/index.ts b/src/generators/index.ts index b5178b0..f4b734f 100644 --- a/src/generators/index.ts +++ b/src/generators/index.ts @@ -1,4 +1,4 @@ -import * as typeDefinitions from './typeDefinitions'; -import * as globals from './globals'; - -export { typeDefinitions, globals }; +export * as typeDefinitions from './typeDefinitions'; +export * as globals from './globals'; +export * as handlebarsHelper from './handlebarsHelper'; +export * as wrappers from './wrappers'; diff --git a/src/generators/typeDefinitions.ts b/src/generators/typeDefinitions.ts index ede509a..dc48a5a 100644 --- a/src/generators/typeDefinitions.ts +++ b/src/generators/typeDefinitions.ts @@ -2,114 +2,29 @@ import fs from 'fs-extra'; import Handlebars from 'handlebars'; import { constantCase } from 'constant-case'; import ora from 'ora'; - -import { fetchSampInc, fetchActorInc, fetchHttpInc, fetchObjectsInc, fetchPlayersInc, fetchSampDBInc, fetchVehiclesInc } from '../requests'; -import { parseInclude } from '../parser'; -import { IParsed, IParam, IPawnDoc } from '../interfaces'; +import { IParsed } from '../interfaces'; import { EPaths } from '../enums'; +import { DocsStore } from '../docsStore'; -let a_sampParsed: IParsed; -let a_actorParsed: IParsed; -let a_httpParsed: IParsed; -// let a_npcParsed: IParsed; -let a_objectsParsed: IParsed; -let a_playersParsed: IParsed; -let a_sampdbParsed: IParsed; -let a_vehiclesParsed: IParsed; - -Handlebars.registerHelper({ - eq: (v1, v2) => v1 === v2, - ne: (v1, v2) => v1 !== v2, - lt: (v1, v2) => v1 < v2, - gt: (v1, v2) => v1 > v2, - lte: (v1, v2) => v1 <= v2, - gte: (v1, v2) => v1 >= v2, - and: (v1, v2) => v1 && v2, - or: (v1, v2) => v1 || v2, -}); - -Handlebars.registerHelper({ - // has variadic params - hvp: (pa: Array) => pa.some(p => p.isVariadic), - // specifier values string - svs: (pa: Array) => pa.map(p => p.isReference ? p.type.toUpperCase() : p.type).join(''), - // specifier values without references - svw: (pa: Array) => pa.filter(p => !p.isReference), - // specifier values without references length - svwl: (pa: Array) => pa.filter(p => !p.isReference).length, - // reference values string - rvs: (pa: Array) => pa.filter(p => p.isReference).map(p => p.type.toUpperCase()).join(''), - // reference values length - rvl: (pa: Array) => pa.filter(p => p.isReference).length, - - // pawndoc param description - ppd: (pd: IPawnDoc, pa: IParam) => pd.param?.find(pdpa => pdpa.name === pa.name)?.description, - // typescript type for specifier - tts: (t: IParam['type'], b = false) => { - let r: string; - if (t === 's') r = 'string'; - else if (t === 'a' || t === 'v') r = 'Array'; - else r = 'number'; - return b ? `{${r}}` : r; - }, -}); - -export const generate = async () => { - const generating = ora('Generating type definitions...').start(); - - // A_SAMP - const a_samp = await fetchSampInc(); - a_sampParsed = await parseInclude(a_samp, true, true); - - // A_ACTOR - const a_actor = await fetchActorInc(); - a_actorParsed = await parseInclude(a_actor, true, true); - - // A_HTTP - const a_http = await fetchHttpInc(); - a_httpParsed = await parseInclude(a_http, true, true); - - // A_NPC - // const a_npc = await fetchNPCInc(); - // a_npcParsed = await parseInclude(a_npc, false, true); - // Removed because it contains most of the things a_samp already has and you shouldn't include both in a pawn gamemode either - - // A_OBJECTS - const a_objects = await fetchObjectsInc(); - a_objectsParsed = await parseInclude(a_objects, true, true); - - // A_PLAYERS - const a_players = await fetchPlayersInc(); - a_playersParsed = await parseInclude(a_players, true, true); - - // A_SAMPDB - const a_sampdb = await fetchSampDBInc(); - a_sampdbParsed = await parseInclude(a_sampdb, false, true); - - // A_VEHICLES - const a_vehicles = await fetchVehiclesInc(); - a_vehiclesParsed = await parseInclude(a_vehicles, true, true); - +export const generate = async (docsStore: DocsStore) => { const eventsDefinitionsSpinner = ora('Generating event type definitions...').start(); - await generateEventsDefinitions(); + await generateEventsDefinitions(docsStore); eventsDefinitionsSpinner.succeed('Events type definitions generated.'); const sampDefinitionsSpinner = ora('Generating samp type definitions...').start(); - await generateSampDefinitions(); + await generateSampDefinitions(docsStore); sampDefinitionsSpinner.succeed('Samp type definitions generated.'); - - generating.succeed('All type definitions generated.'); }; -export const generateEventsDefinitions = async () => { - const a_sampEvents = getEventConstants(a_sampParsed); - const a_actorEvents = getEventConstants(a_actorParsed); - const a_httpEvents = getEventConstants(a_httpParsed); - // const a_npcEvents = getEventConstants(a_npcParsed); - const a_objectsEvents = getEventConstants(a_objectsParsed); - const a_playersEvents = getEventConstants(a_playersParsed); - const a_sampdbEvents = getEventConstants(a_sampdbParsed); - const a_vehiclesEvents = getEventConstants(a_vehiclesParsed); +export const generateEventsDefinitions = async (docsStore: DocsStore) => { + const a_sampEvents = getEventConstants(docsStore.a_samp); + const a_actorEvents = getEventConstants(docsStore.a_actor); + const a_httpEvents = getEventConstants(docsStore.a_http); + // const a_npcEvents = getEventConstants(docsStore.a_npc); + const a_objectsEvents = getEventConstants(docsStore.a_objects); + const a_playersEvents = getEventConstants(docsStore.a_players); + const a_sampdbEvents = getEventConstants(docsStore.a_sampdb); + const a_vehiclesEvents = getEventConstants(docsStore.a_vehicles); const eventConstants = { ...a_sampEvents, @@ -123,26 +38,26 @@ export const generateEventsDefinitions = async () => { }; const template = Handlebars.compile(await fs.readFile(EPaths.TEMPLATE_EVENTS, 'utf8')); - await fs.outputFile(EPaths.GENERATED_EVENTS, template({ eventConstants })); + await fs.outputFile(EPaths.GENERATED_EVENT_TYPES, template({ eventConstants })); }; -export const generateSampDefinitions = async () => { +export const generateSampDefinitions = async (docsStore: DocsStore) => { const eventListenerAliases = ['on', 'addListener', 'addEventListener']; const removeEventListenerAliases = ['removeListener', 'removeEventListener']; const parsedIncludes = [ - a_sampParsed, - a_actorParsed, - a_httpParsed, - // a_npcParsed, - a_objectsParsed, - a_playersParsed, - a_sampdbParsed, - a_vehiclesParsed, + docsStore.a_samp, + docsStore.a_actor, + docsStore.a_http, + // docsStore.a_npc, + docsStore.a_objects, + docsStore.a_players, + docsStore.a_sampdb, + docsStore.a_vehicles, ]; const template = Handlebars.compile(await fs.readFile(EPaths.TEMPLATE_SAMP, 'utf8')); - await fs.outputFile(EPaths.GENERATED_SAMP, template({ eventListenerAliases, removeEventListenerAliases, parsedIncludes })); + await fs.outputFile(EPaths.GENERATED_SAMP_TYPES, template({ eventListenerAliases, removeEventListenerAliases, parsedIncludes })); }; export const getEventConstants = (parsed: IParsed) => { diff --git a/src/generators/wrappers.ts b/src/generators/wrappers.ts new file mode 100644 index 0000000..dadf456 --- /dev/null +++ b/src/generators/wrappers.ts @@ -0,0 +1,38 @@ +import fs from 'fs-extra'; +import Handlebars from 'handlebars'; +import ora from 'ora'; +import { EPaths } from '../enums'; +import { DocsStore, ParsedModules } from '../docsStore'; + +export const generate = async (docsStore: DocsStore) => { + // TODO: generate default values + const generating = ora('Generating native wrappers definitions...').start(); + await generateNativeWrappers(docsStore); + generating.succeed('All native wrappers generated.'); +}; + +const generateNativeWrappers = async (docsStore: DocsStore) => { + const moduleNames: Array = [ + "a_samp", + "a_actor", + "a_http", + // "a_npc", + "a_objects", + "a_players", + "a_sampdb", + "a_vehicles", + ]; + + const wrapperTemplate = Handlebars.compile(await fs.readFile(EPaths.TEMPLATE_WRAPPERS, 'utf8')); + const indexTemplate = Handlebars.compile(await fs.readFile(EPaths.TEMPLATE_WRAPPERS_INDEX, 'utf8')); + await Promise.all(moduleNames.map(moduleName => { + const module = docsStore[moduleName]; + return fs.outputFile(generatedWrapperPath(moduleName), wrapperTemplate({ module })); + })); + + await fs.outputFile(generatedWrapperPath("index"), indexTemplate({ moduleNames })); +}; + +function generatedWrapperPath(moduleName: string) { + return `${EPaths.GENERATED_WRAPPERS_FOLDER}/${moduleName}.ts`; +} diff --git a/src/index.ts b/src/index.ts index a1436c1..293fe13 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,18 @@ -import { typeDefinitions, globals } from './generators'; +import { typeDefinitions, globals, handlebarsHelper, wrappers } from './generators'; +import { DocsStore } from './docsStore'; -typeDefinitions.generate(); -globals.generate(); +import ora from 'ora'; + +async function generateAll() { + const generating = ora('Generating definitions...').start(); + + const docsStore = await DocsStore.fromSampStdlib(); + handlebarsHelper.initHandlerbars(); + await typeDefinitions.generate(docsStore); + await wrappers.generate(docsStore); + await globals.generate(); + + generating.succeed('All type definitions generated.'); +} + +generateAll(); diff --git a/src/templates/samp.d.ts.hjs b/src/templates/samp.d.ts.hjs index b90eb34..2c4fe2c 100644 --- a/src/templates/samp.d.ts.hjs +++ b/src/templates/samp.d.ts.hjs @@ -181,7 +181,7 @@ declare class samp { * @returns {*} {{/if}} */ - static callNative{{#if this.returnsFloat}}Float{{/if}}(nativeName: '{{this.name}}', paramTypes: '{{svs this.params}}'{{#if (gt (svwl this.params) 0)}}, {{#each (svw this.params)}}{{this.name}}: {{tts this.type false}}{{#if @last}}{{else}}, {{/if}}{{/each}}{{/if}}): {{#if (gt (rvl this.params) 1)}}Array{{else if (eq (rvs this.params) 'S')}}string{{else}}number{{/if}}; + static callNative{{#if this.returnsFloat}}Float{{/if}}(nativeName: '{{this.name}}', paramTypes: '{{svs this.params}}'{{#each (svw this.params)}}, {{this.name}}: {{tts this.type false}}{{/each}}): {{outputtype this.params}}; {{/unless}} {{/each}} {{/each}} diff --git a/src/templates/wrappers/index.ts.hjs b/src/templates/wrappers/index.ts.hjs new file mode 100644 index 0000000..240819d --- /dev/null +++ b/src/templates/wrappers/index.ts.hjs @@ -0,0 +1,3 @@ +{{#each moduleNames~}} +export * from './{{this}}'; +{{/each}} diff --git a/src/templates/wrappers/wrappers.ts.hjs b/src/templates/wrappers/wrappers.ts.hjs new file mode 100644 index 0000000..3e88a0a --- /dev/null +++ b/src/templates/wrappers/wrappers.ts.hjs @@ -0,0 +1,44 @@ +// Auto-generated wrappers for natives + +{{#each module.native}} + {{#unless (hvp this.params)}} + /** + * Calls the AMX native {{this.name}} {{#if this.returnsFloat}}that returns a value with a Float tag{{/if}} + * + * @name {{this.name}} + {{#if (gt (svwl this.params) 0)}} + {{#each (svw this.params)}} + * @param {{tts this.type true}} {{this.name}} + {{~#if ../this.pawnDoc}} {{ppd ../this.pawnDoc this}}{{/if}} + {{~#with (restriction this) as |r|}}{{#if r}} - {{r}}{{/if}}{{/with}} + {{/each}} + {{/if}} + {{#if this.pawnDoc}} + {{#each this.pawnDoc.summary}} + * @summary {{this}} + {{/each}} + {{#each this.pawnDoc.see}} + * @see {{this}} + {{/each}} + {{#each this.pawnDoc.remarks}} + * @remarks {{this}} + {{/each}} + {{#each this.pawnDoc.returns}} + * @returns {{this}} + {{/each}} + {{else}} + * @returns {*} + {{/if}} + */ + export function {{this.name}}({{#each (svw this.params) ~}} + {{this.name}}: {{tts this.type false}}{{#unless @last}}, {{/unless}} + {{~/each}}) { + {{#if (gt (rvl this.params) 1)}} + const [{{#each (rv this.params)}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}}] = samp.callNative{{#if this.returnsFloat}}Float{{/if}}('{{this.name}}', '{{svs this.params}}'{{#each (svw this.params)}}, {{this.name}}{{/each}}); + return { {{#each (rv this.params)}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}} }; + {{else}} + return samp.callNative{{#if this.returnsFloat}}Float{{/if}}('{{this.name}}', '{{svs this.params}}'{{#each (svw this.params)}}, {{this.name}}{{/each}}); + {{/if}} + } + {{/unless}} +{{/each}}