diff --git a/composer.json b/composer.json
index ebf1c88..06c2e97 100644
--- a/composer.json
+++ b/composer.json
@@ -13,5 +13,10 @@
},
"require-dev": {
"heroku/heroku-buildpack-php": "dev-main"
+ },
+ "scripts": {
+ "compile": [
+ "./web/compile.sh"
+ ]
}
}
diff --git a/web/compile.sh b/web/compile.sh
new file mode 100644
index 0000000..74d233d
--- /dev/null
+++ b/web/compile.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+set -e
+
+RELAY_INI=`find $(php-config --ini-dir) -name "*-relay.ini"`
+
+sed -i 's/^;\? \?relay.maxmemory =.*/relay.maxmemory = 100M/' $RELAY_INI
+sed -i 's/^;\? \?relay.eviction_policy =.*/relay.eviction_policy = lru/' $RELAY_INI
\ No newline at end of file
diff --git a/web/header.phtml b/web/header.phtml
index 02c8cc6..9206813 100644
--- a/web/header.phtml
+++ b/web/header.phtml
@@ -35,6 +35,10 @@
+
diff --git a/web/versions.php b/web/versions.php
new file mode 100644
index 0000000..536775e
--- /dev/null
+++ b/web/versions.php
@@ -0,0 +1,244 @@
+connect(getenv('UPSTASH_REDIS_REST_URL'), 6379);
+$relay->auth(['default', getenv('UPSTASH_REDIS_REST_TOKEN')]);
+
+$table = new Relay\Table('packages');
+
+function fetch_json(string $url, $table): ?array
+{
+ if($table->exists($url)) {
+ $cached = $table->get($url);
+ if ($cached) {
+ return json_decode($cached, true);
+ }
+ }
+ $opts = [
+ 'http' => [
+ 'method' => 'GET',
+ 'header' => "User-Agent: PHP\r\n",
+ 'timeout' => 10,
+ ],
+ ];
+ $context = stream_context_create($opts);
+ $json = @file_get_contents($url, false, $context);
+ if ($json === false) {
+ error_log("Failed to fetch: $url");
+ return null;
+ }
+ $table->set($url, $json);
+ $data = json_decode($json, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ error_log("JSON decode error for $url: " . json_last_error_msg());
+ return null;
+ }
+ return $data;
+}
+
+function parse_packages(array $packages_data): array
+{
+ $stacks = [];
+ if (!isset($packages_data['packages'])) {
+ return $stacks;
+ }
+
+ $packages = $packages_data['packages'];
+ $package_list = [];
+
+ // If associative array (package-name => [versions])
+ $is_assoc = array_keys($packages) !== range(0, count($packages) - 1);
+ if ($is_assoc) {
+ foreach ($packages as $package_name => $versions) {
+ if (is_array($versions)) {
+ foreach ($versions as $version_info) {
+ $package_list[] = $version_info;
+ }
+ }
+ }
+ } else {
+ // Flatten lists
+ foreach ($packages as $item) {
+ if (is_array($item) && array_keys($item) === range(0, count($item) - 1)) {
+ foreach ($item as $sub) {
+ $package_list[] = $sub;
+ }
+ } else {
+ $package_list[] = $item;
+ }
+ }
+ }
+
+ foreach ($package_list as $version_info) {
+ if (!is_array($version_info)) {
+ continue;
+ }
+ $package_name = $version_info['name'] ?? null;
+ if (!$package_name || strpos($package_name, 'heroku-sys/ext-') !== 0) {
+ continue;
+ }
+ $ext_name = substr($package_name, strlen('heroku-sys/ext-'));
+ $ext_version = $version_info['version'] ?? null;
+ if (!$ext_version) {
+ continue;
+ }
+ $require = $version_info['require'] ?? [];
+ $php_req = $require['heroku-sys/php'] ?? null;
+ $stack_req = $require['heroku-sys/heroku'] ?? null;
+ if (!$php_req || !$stack_req) {
+ continue;
+ }
+
+ // Parse stack (e.g., ^22.0.0 -> 22 -> heroku-22)
+ if (preg_match('/\^?(\d+)\./', $stack_req, $m)) {
+ $stack = 'heroku-' . $m[1];
+ } else {
+ continue;
+ }
+
+ // Parse PHP version like 7.3.* -> 7.3
+ if (preg_match('/(\d+\.\d+)\.\*/', $php_req, $m2)) {
+ $php_version = $m2[1];
+ } else {
+ continue;
+ }
+
+ $stacks[$stack][$php_version][$ext_name][] = $ext_version;
+ }
+
+ // Deduplicate and sort versions
+ foreach ($stacks as $stack_name => $phps) {
+ foreach ($phps as $php => $exts) {
+ foreach ($exts as $ext => $versions) {
+ $unique = array_values(array_unique($versions));
+ usort($unique, function ($a, $b) {
+ return version_compare($a, $b);
+ });
+ $stacks[$stack_name][$php][$ext] = $unique;
+ }
+ }
+ }
+
+ return $stacks;
+}
+
+// Merge stacks from multiple manifests
+$all_stacks = [];
+foreach ($urls as $url) {
+ $data = fetch_json($url, $table);
+ if ($data === null) {
+ continue;
+ }
+ $st = parse_packages($data);
+ // Merge into all_stacks
+ foreach ($st as $stack => $phps) {
+ foreach ($phps as $php => $exts) {
+ foreach ($exts as $ext => $versions) {
+ if (!isset($all_stacks[$stack][$php][$ext])) {
+ $all_stacks[$stack][$php][$ext] = [];
+ }
+ $all_stacks[$stack][$php][$ext] = array_merge($all_stacks[$stack][$php][$ext], $versions);
+ }
+ }
+ }
+}
+
+// Final dedupe and sort after merge
+foreach ($all_stacks as $stack_name => $phps) {
+ foreach ($phps as $php => $exts) {
+ foreach ($exts as $ext => $versions) {
+ $unique = array_values(array_unique($versions));
+ usort($unique, function ($a, $b) {
+ return version_compare($a, $b);
+ });
+ $all_stacks[$stack_name][$php][$ext] = $unique;
+ }
+ }
+}
+
+// Helper to sort php versions like 7.3, 8.0 numerically
+function sort_php_versions(array $versions): array
+{
+ usort($versions, function ($a, $b) {
+ $pa = array_map('intval', explode('.', $a));
+ $pb = array_map('intval', explode('.', $b));
+ for ($i = 0; $i < max(count($pa), count($pb)); $i++) {
+ $va = $pa[$i] ?? 0;
+ $vb = $pb[$i] ?? 0;
+ if ($va === $vb) continue;
+ return $va <=> $vb;
+ }
+ return 0;
+ });
+ return $versions;
+}
+
+// Render HTML using site header/footer and Tailwind styling
+ini_set('display_errors', 1);
+error_reporting(E_ALL);
+
+require __DIR__ . '/header.phtml';
+
+if (empty($all_stacks)) {
+ echo "\n";
+ require __DIR__ . '/footer.phtml';
+ exit;
+}
+
+foreach ($all_stacks as $stack => $phps) {
+ // gather all extensions and php versions
+ $all_extensions = [];
+ $all_php_versions = [];
+ foreach ($phps as $php => $exts) {
+ $all_php_versions[] = $php;
+ foreach ($exts as $ext => $_) {
+ $all_extensions[$ext] = true;
+ }
+ }
+ $all_extensions = array_keys($all_extensions);
+ sort($all_extensions, SORT_NATURAL | SORT_FLAG_CASE);
+ $all_php_versions = array_values(array_unique($all_php_versions));
+ $all_php_versions = sort_php_versions($all_php_versions);
+
+ echo "\n";
+ echo " " . htmlspecialchars($stack) . "
\n";
+ echo " \n";
+ echo "
\n";
+ echo " | PHP | ";
+ foreach ($all_extensions as $ext) {
+ echo "" . htmlspecialchars($ext) . " | ";
+ }
+ echo "
\n";
+ echo " \n";
+
+ foreach ($all_php_versions as $php_ver) {
+ echo " \n";
+ echo " | " . htmlspecialchars($php_ver) . " | ";
+ foreach ($all_extensions as $ext) {
+ $versions = $all_stacks[$stack][$php_ver][$ext] ?? [];
+ $cells = [];
+ foreach ($versions as $v) {
+ $cells[] = '' . htmlspecialchars($v) . '';
+ }
+ echo "" . implode(' ', $cells) . " | ";
+ }
+ echo "
\n";
+ }
+
+ echo " \n";
+ echo "
\n";
+ echo "
\n";
+ echo "\n";
+}
+
+require __DIR__ . '/footer.phtml';