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; ?>();