From d258bcc91948424711dd79fde57254e1384d0091 Mon Sep 17 00:00:00 2001 From: igor Date: Fri, 13 Feb 2026 09:54:08 +0100 Subject: [PATCH] separated JavaScript and TypeScript export, added AGENTS.md for AI bot --- AGENTS.md | 37 ++++++++++ README.md | 98 ++++++------------------- bin/apilite-files | 2 +- composer.json | 1 + src/APIlite.php | 34 +++++++-- src/help.tpl.php | 1 + src/javascript.tpl.php | 69 +++++++++++++++++ src/typescript.tpl.php | 163 +++++++++++++++++++++++++++++++---------- 8 files changed, 288 insertions(+), 117 deletions(-) create mode 100644 AGENTS.md create mode 100644 src/javascript.tpl.php diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ed33b60 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +# AGENTS.md + +## Project overview +APIlite is a lightweight PHP library. A class extends `TPsoft\APIlite\APIlite`, public methods become API actions, requests are processed automatically, and responses are returned as JSON. + +The project also generates frontend clients: +- JavaScript client: `format=javascript` (`.js`) +- TypeScript client: `format=typescript` (`.ts`, real typed output) + +## Core files +- `src/APIlite.php`: runtime, routing, docs JSON/HTML/client generation +- `src/help.tpl.php`: HTML documentation template +- `src/javascript.tpl.php`: JavaScript client template +- `src/typescript.tpl.php`: TypeScript client template +- `bin/apilite-files`: helper script for bootstrapping files in consumer projects + +## Working rules +- Keep API behavior backward compatible unless explicitly requested. +- Maintain both generated clients: + - JavaScript stays plain JS. + - TypeScript stays typed (interfaces + typed method signatures). +- If you change output field names in JSON help, update README and templates consistently. +- Prefer small, focused changes over broad rewrites. + +## Verification checklist +- Run syntax checks on edited PHP files: + - `php -l src/APIlite.php` + - `php -l src/help.tpl.php` + - `php -l src/javascript.tpl.php` + - `php -l src/typescript.tpl.php` + - `php -l bin/apilite-files` +- If client generation changes, test with: + - `php test/APIcalculator.php --javascript` + - `php test/APIcalculator.php --typescript` + +## Documentation rule +When output format names/flags/URLs change, update `README.md` in the same change set. diff --git a/README.md b/README.md index 2165a31..835c077 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ When you run this subfile through the webserver, you will see the JSON documenta { "name": "APIcalculator", "html_version": "http://localhost/APIlite/test/APIcalculator.php?format=html", + "javascript_version": "http://localhost/APIlite/test/APIcalculator.php?format=javascript", "typescript_version": "http://localhost/APIlite/test/APIcalculator.php?format=typescript", "actions": [ { @@ -230,84 +231,33 @@ and there is also an HTML version available -To connect to the API from TypeScript (e.g. Vue application) it is possible to download the backend script +You can also download generated frontend clients: -```ts -/** - * Generated by APIlite - * https://gitea.tpsoft.org/TPsoft.org/APIlite - * - * 2025-06-12 06:24:33 */ +* JavaScript: `?format=javascript` +* TypeScript (typed for Vue/TS projects): `?format=typescript` -class APIcalculator { - endpont = "http://"; +JavaScript usage example: - /* ---------------------------------------------------- - * General API call - */ - call(method, data, callback) { - var xhttp = new XMLHttpRequest(); - xhttp.withCredentials = true; - xhttp.onreadystatechange = function() { - if (this.readyState == 4) { - if (this.status == 200) { - if (callback != null) callback(JSON.parse(this.responseText)); - } else { - if (callback != null) callback({'status': 'ERROR', 'message': 'HTTP STATUS ' + this.status}); - } - } - } - var form_data = new FormData(); - Object.keys(data).forEach(key => { - let val = data[key]; - if (typeof val == 'object') val = JSON.stringify(val); - form_data.append(key, val); - }); - xhttp.open('POST', this.endpont + '?action=' + method); - xhttp.send(form_data); - } +```js +import backend from './backend.js'; - callPromise(method, data) { - return new Promise((resolve, reject) => { - this.call(method, data, function(response) { - if (response.status == 'OK') { - resolve(response.data); - } else { - reject(response.msg); - } - }); - }) - } - - /* ---------------------------------------------------- - * API actions - */ - help() { - return this.callPromise('__HELP__', {}); - } - - add(a, b) { - return this.callPromise('add', {a: a, b: b}); - } - - subtract(a, b) { - return this.callPromise('subtract', {a: a, b: b}); - } - - multiply(a, b) { - return this.callPromise('multiply', {a: a, b: b}); - } - - divide(a, b) { - return this.callPromise('divide', {a: a, b: b}); - } - -}; - -export default new BackendAPI(); +backend.add(1, 2).then((response) => { + console.log(response.data); +}); ``` -These outputs can also be generated in the command line as follows +TypeScript usage example (Vue + TS): -* for HTML`$> php APIcalculator.php --html` -* for TypeScript`$> php APIcalculator.php --typescript` +```ts +import backend from './backend'; + +backend.add(1, 2).then((response) => { + console.log(response.data); // typed value based on PHP return type +}); +``` + +These outputs can also be generated in command line: + +* HTML: `$> php APIcalculator.php --html` +* JavaScript: `$> php APIcalculator.php --javascript > backend.js` (`--js` alias is available) +* TypeScript: `$> php APIcalculator.php --typescript > backend.ts` diff --git a/bin/apilite-files b/bin/apilite-files index 98c4670..318bd3a 100644 --- a/bin/apilite-files +++ b/bin/apilite-files @@ -55,7 +55,7 @@ $backend_api = new TPsoft\BugreportBackend\API(\'typescript\', \'import.meta.env $output = ob_get_contents(); ob_end_clean(); -$ts_path = realpath(__DIR__ . \'/../../frontend/src\').\'/backend.js\'; +$ts_path = realpath(__DIR__ . \'/../../frontend/src\').\'/backend.ts\'; $suc = file_put_contents($ts_path, $output); if ($suc === false) { echo "✗ TypeScript store into file failed\n"; diff --git a/composer.json b/composer.json index 1604b0f..58d7d9a 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "rest", "json", "php", + "javascript", "typescript" ], "authors": [ diff --git a/src/APIlite.php b/src/APIlite.php index 712bf8b..c22c714 100644 --- a/src/APIlite.php +++ b/src/APIlite.php @@ -39,6 +39,9 @@ class APIlite if (in_array('--html', $switches)) { $format = 'html'; } + if (in_array('--javascript', $switches) || in_array('--js', $switches)) { + $format = 'javascript'; + } if (in_array('--typescript', $switches)) { $format = 'typescript'; } @@ -47,6 +50,10 @@ class APIlite case 'html': $this->printHelpHTML(); break; + case 'javascript': + case 'js': + $this->printHelpJavascript(); + break; case 'typescript': $this->printHelpTypescript(); break; @@ -189,6 +196,7 @@ class APIlite $this->response(array( 'name' => $this->apiName, 'html_version' => $this->getCurrentUrl().'?format=html', + 'javascript_version' => $this->getCurrentUrl().'?format=javascript', 'typescript_version' => $this->getCurrentUrl().'?format=typescript', 'actions' => $this->methods )); @@ -199,20 +207,36 @@ class APIlite include __DIR__ . '/help.tpl.php'; } - private function printHelpTypescript(): void + private function printHelpJavascript(): void { ob_clean(); header('Content-Type: application/javascript'); header('Content-Disposition: attachment; filename="' . $this->apiName . '.js"'); + include __DIR__ . '/javascript.tpl.php'; + } + + private function printHelpTypescript(): void + { + ob_clean(); + header('Content-Type: application/typescript'); + header('Content-Disposition: attachment; filename="' . $this->apiName . '.ts"'); include __DIR__ . '/typescript.tpl.php'; } private function getCurrentUrl(): string { - $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || - $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://"; - $host = $_SERVER['HTTP_HOST']; - $uri = $_SERVER['REQUEST_URI']; + $https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; + $serverPort = isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : null; + $protocol = ($https || $serverPort === 443) ? 'https://' : 'http://'; + $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; + $uri = $_SERVER['REQUEST_URI'] ?? ($_SERVER['SCRIPT_NAME'] ?? '/'); $path = parse_url($uri, PHP_URL_PATH); + if (!is_string($path) || $path === '') { + $path = '/'; + } + $path = str_replace('\\', '/', $path); + if (substr($path, 0, 1) !== '/') { + $path = '/' . ltrim($path, '/'); + } return $protocol . $host . $path; } diff --git a/src/help.tpl.php b/src/help.tpl.php index 6ea053f..006f948 100644 --- a/src/help.tpl.php +++ b/src/help.tpl.php @@ -396,6 +396,7 @@ diff --git a/src/javascript.tpl.php b/src/javascript.tpl.php new file mode 100644 index 0000000..33a7591 --- /dev/null +++ b/src/javascript.tpl.php @@ -0,0 +1,69 @@ +/** + * Generated by APIlite + * https://gitea.tpsoft.org/TPsoft.org/APIlite + * + * + */ + +class apiName; ?> { + endpoint = endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>; + + /* ---------------------------------------------------- + * General API call + */ + call(method, data, callback) { + var xhttp = new XMLHttpRequest(); + xhttp.withCredentials = true; + xhttp.onreadystatechange = function() { + if (this.readyState === 4) { + if (this.status === 200) { + if (callback != null) callback(JSON.parse(this.responseText)); + } else { + if (callback != null) callback({'status': 'ERROR', 'msg': 'HTTP STATUS ' + this.status}); + } + } + } + var form_data = new FormData(); + Object.keys(data).forEach(key => { + let val = data[key]; + if (typeof val === 'undefined') return; + if (typeof val == 'object') val = JSON.stringify(val); + form_data.append(key, val); + }); + xhttp.open('POST', this.endpoint + '?action=' + method); + xhttp.send(form_data); + } + + callPromise(method, data) { + return new Promise((resolve, reject) => { + this.call(method, data, function(response) { + if (method === '__HELP__') { + resolve(response); + return; + } + if (response.status === 'OK') { + resolve(response.data); + } else { + reject(response.msg); + } + }); + }) + } + + /* ---------------------------------------------------- + * API actions + */ + help() { + return this.callPromise('__HELP__', {}); + } + +methods)) foreach ($this->methods as $method) { + echo "\t".$method['name'].'('.implode(', ', array_map(function($param) { return $param['name']; }, $method['params'])).') {'; + echo "\n\t\treturn this.callPromise('".$method['name']."', {".implode(', ', array_map(function($param) { return $param['name'].': '.$param['name']; }, $method['params']))."});"; + echo "\n\t}\n\n"; + } +?> + +}; + +export default new apiName; ?>(); diff --git a/src/typescript.tpl.php b/src/typescript.tpl.php index 10a8b33..e82bd64 100644 --- a/src/typescript.tpl.php +++ b/src/typescript.tpl.php @@ -1,3 +1,39 @@ + 'number', + 'string' => 'string', + 'bool', 'boolean' => 'boolean', + 'array', 'iterable' => 'unknown[]', + 'object', 'stdclass' => 'Record', + 'mixed', 'resource' => 'unknown', + 'null', 'void' => 'null', + 'scalar' => 'string | number | boolean', + 'callable', 'closure' => '(...args: unknown[]) => unknown', + 'never' => 'never', + default => 'unknown', + }; +}; + +$mapUnionType = function (mixed $type) use ($mapType): string { + if (is_array($type)) { + $parts = array(); + foreach ($type as $singleType) { + $parts[] = $mapType(is_string($singleType) ? $singleType : null); + } + $parts = array_values(array_unique($parts)); + return empty($parts) ? 'unknown' : implode(' | ', $parts); + } + + return $mapType(is_string($type) ? $type : null); +}; +?> /** * Generated by APIlite * https://gitea.tpsoft.org/TPsoft.org/APIlite @@ -5,64 +41,117 @@ * */ -class apiName; ?> { - endpoint = endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>; +export interface APIliteActionResponse { + status: 'OK'; + data: T; +} - /* ---------------------------------------------------- - * General API call - */ - call(method, data, callback) { - var xhttp = new XMLHttpRequest(); +export interface APIliteErrorResponse { + status: 'ERROR'; + msg: string; +} + +export interface APIliteMethodParam { + name: string; + type: string | null; + optional: boolean; + default: unknown; + doc: string | null; +} + +export interface APIliteMethodDoc { + name: string; + doc: string | null; + description: string | null; + params: APIliteMethodParam[]; + return: string | string[] | null; +} + +export interface APIliteHelpResponse { + name: string; + html_version: string; + javascript_version: string; + typescript_version: string; + actions: APIliteMethodDoc[]; +} + +class apiName; ?> { + endpoint: string = endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>; + + private call( + method: string, + data: Record, + callback: (response: APIliteHelpResponse | APIliteActionResponse | APIliteErrorResponse) => void + ): void { + const xhttp = new XMLHttpRequest(); xhttp.withCredentials = true; xhttp.onreadystatechange = function() { - if (this.readyState == 4) { - if (this.status == 200) { - if (callback != null) callback(JSON.parse(this.responseText)); + if (this.readyState === 4) { + if (this.status === 200) { + const response = JSON.parse(this.responseText) as APIliteHelpResponse | APIliteActionResponse | APIliteErrorResponse; + callback(response); } else { - if (callback != null) callback({'status': 'ERROR', 'message': 'HTTP STATUS ' + this.status}); + callback({ status: 'ERROR', msg: 'HTTP STATUS ' + this.status }); } } - } - var form_data = new FormData(); - Object.keys(data).forEach(key => { - let val = data[key]; - if (typeof val == 'object') val = JSON.stringify(val); - form_data.append(key, val); + }; + + const formData = new FormData(); + Object.keys(data).forEach((key) => { + const rawValue = data[key]; + if (typeof rawValue === 'undefined') { + return; + } + let value: string | Blob; + if (rawValue instanceof Blob) { + value = rawValue; + } else if (typeof rawValue === 'object' && rawValue !== null) { + value = JSON.stringify(rawValue); + } else { + value = String(rawValue); + } + formData.append(key, value); }); + xhttp.open('POST', this.endpoint + '?action=' + method); - xhttp.send(form_data); + xhttp.send(formData); } - callPromise(method, data) { - return new Promise((resolve, reject) => { - this.call(method, data, function(response) { - if (method == '__HELP__') { - resolve(response); + private callPromise(method: string, data: Record): Promise { + return new Promise((resolve, reject) => { + this.call(method, data, (response) => { + if (method === '__HELP__') { + resolve(response as T); return; } - if (response.status == 'OK') { - resolve(response.data); - } else { - reject(response.msg); + if (response.status === 'OK') { + resolve(response.data as T); + return; } + reject(response.msg); }); - }) + }); } - /* ---------------------------------------------------- - * API actions - */ - help() { - return this.callPromise('__HELP__', {}); + help(): Promise { + return this.callPromise('__HELP__', {}); } methods)) foreach ($this->methods as $method) { - echo "\t".$method['name'].'('.implode(', ', array_map(function($param) { return $param['name']; }, $method['params'])).') {'; - echo "\n\t\treturn this.callPromise('".$method['name']."', {".implode(', ', array_map(function($param) { return $param['name'].': '.$param['name']; }, $method['params']))."});"; - echo "\n\t}\n\n"; + $paramsSignature = array(); + $paramsPayload = array(); + foreach ($method['params'] as $param) { + $paramType = $mapType($param['type']); + $paramsSignature[] = $param['name'] . ($param['optional'] ? '?' : '') . ': ' . $paramType; + $paramsPayload[] = $param['name']; } + $returnType = $mapUnionType($method['return']); ?> + (): Promise>> { + return this.callPromise>>('', { }); + } -}; + +} export default new apiName; ?>();