Add Swoole alternative backend
This commit is contained in:
154
alt/swoole/asset-manifest.php
Normal file
154
alt/swoole/asset-manifest.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/** Runtime asset manifest and static-byte preparation, kept independent of the HTTP server. */
|
||||
final class AssetManifest
|
||||
{
|
||||
public array $manifest;
|
||||
public array $delivery;
|
||||
public string $index;
|
||||
public string $serviceWorker;
|
||||
public array $compressed = [];
|
||||
|
||||
public function __construct(private string $root, bool $hashing = true, bool $assetSync = true, ?string $override = null)
|
||||
{
|
||||
$this->root = rtrim($root, '/');
|
||||
$assetSync = $hashing && $assetSync;
|
||||
$raw = [];
|
||||
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->root, FilesystemIterator::SKIP_DOTS));
|
||||
foreach ($it as $entry) if ($entry->isFile()) {
|
||||
$path = '/' . str_replace(DIRECTORY_SEPARATOR, '/', substr($entry->getPathname(), strlen($this->root) + 1));
|
||||
$raw[$path] = file_get_contents($entry->getPathname());
|
||||
}
|
||||
ksort($raw, SORT_STRING);
|
||||
$definition = isset($raw['/assets.json']) ? json_decode($raw['/assets.json'], true, 512, JSON_THROW_ON_ERROR) : ['groups' => ['core' => ['contract' => 1, 'eager' => true, 'files' => array_keys($raw)]]];
|
||||
$include = $definition['include'] ?? [];
|
||||
$runtime = [];
|
||||
foreach ($raw as $path => $body) if (in_array($path, $include, true) || self::runtimePath($path)) $runtime[$path] = $body;
|
||||
|
||||
$groups = $definition['groups'] ?? [];
|
||||
ksort($groups, SORT_STRING);
|
||||
$contracts = [];
|
||||
$membership = [];
|
||||
foreach ($groups as $name => $group) {
|
||||
$contracts[$name] = (int)($group['contract'] ?? 1);
|
||||
foreach (($group['files'] ?? []) as $path) {
|
||||
if (!isset($runtime[$path])) throw new RuntimeException("Asset group $name references missing or excluded $path");
|
||||
if (isset($membership[$path])) throw new RuntimeException("Duplicate asset group membership for $path");
|
||||
$membership[$path] = $name;
|
||||
}
|
||||
}
|
||||
$files = [];
|
||||
$delivery = [];
|
||||
foreach ($raw as $path => $body) {
|
||||
$entry = ['h' => self::hash10($body), 's' => strlen($body), 'g' => $membership[$path] ?? 'core'];
|
||||
$delivery[$path] = $entry;
|
||||
if (isset($runtime[$path])) $files[$path] = $entry;
|
||||
}
|
||||
|
||||
$sourceIndex = $raw['/index.html'] ?? '';
|
||||
$sourceIndex = str_replace('__APP_SCRIPT_HASH__', base64_encode(hash('sha256', $raw['/app.js'] ?? '', true)), $sourceIndex);
|
||||
$canonicalIndex = $this->stampIndex($sourceIndex, $files, '__BUILD_TAG__', true);
|
||||
$canonicalIndex = $this->embedAssets($canonicalIndex, $files, $groups, '__BUILD_TAG__', true, $assetSync);
|
||||
$canonicalFiles = $files;
|
||||
if ($sourceIndex !== '') $canonicalFiles['/index.html'] = [
|
||||
'h' => self::hash10($canonicalIndex), 's' => strlen($canonicalIndex), 'g' => $membership['/index.html'] ?? 'core', 'source' => $files['/index.html']['h'] ?? '',
|
||||
];
|
||||
$canonical = ['assetSync' => $assetSync, 'files' => $canonicalFiles, 'groups' => $groups, 'contracts' => $contracts];
|
||||
$canonicalJson = json_encode($canonical, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
||||
$legacy = '';
|
||||
foreach ($runtime as $path => $body) $legacy .= './public' . $path . $body;
|
||||
$buildTag = $hashing ? substr(hash('sha256', $canonicalJson), 0, 12) : ($override ?: substr(hash('sha256', $legacy), 0, 12));
|
||||
|
||||
$this->index = $this->embedAssets($this->stampIndex($sourceIndex, $files, $buildTag, $hashing), $files, $groups, $buildTag, $hashing, $assetSync);
|
||||
$swSource = $raw['/sw.js'] ?? '';
|
||||
$this->serviceWorker = preg_replace("/typeof __BUILD_TAG__ !== 'undefined' \\? __BUILD_TAG__ : '[^']*'/", '"' . $buildTag . '"', $swSource) ?? $swSource;
|
||||
$this->serviceWorker = str_replace("typeof __ASSET_SYNC__ !== 'undefined' ? __ASSET_SYNC__ : true", $assetSync ? 'true' : 'false', $this->serviceWorker);
|
||||
$this->serviceWorker = str_replace("importScripts('/asset-sync-core.js')", "importScripts('/asset-sync-core.js?v=" . ($files['/asset-sync-core.js']['h'] ?? '') . "')", $this->serviceWorker);
|
||||
$files['/index.html'] = ['h' => self::hash10($this->index), 's' => strlen($this->index), 'g' => 'core'];
|
||||
$files['/sw.js'] = ['h' => self::hash10($this->serviceWorker), 's' => strlen($this->serviceWorker), 'g' => 'core'];
|
||||
$delivery['/index.html'] = $files['/index.html'];
|
||||
$delivery['/sw.js'] = $files['/sw.js'];
|
||||
$this->delivery = $delivery;
|
||||
$this->manifest = ['buildTag' => $buildTag, 'files' => $files, 'groups' => $groups, 'contracts' => $contracts];
|
||||
}
|
||||
|
||||
public static function hash10(string $body): string { return substr(hash('sha256', $body), 0, 10); }
|
||||
public static function runtimePath(string $path): bool
|
||||
{
|
||||
return $path !== '/admin.html' && !preg_match('~(?:^|/)perf(?:/|$)~i', $path)
|
||||
&& !preg_match('~\.(?:test\.(?:js|mjs)|md|txt|c|entry\.js)$~i', $path);
|
||||
}
|
||||
private function embedAssets(string $source, array $files, array $groups, string $tag, bool $hashing, bool $assetSync): string
|
||||
{
|
||||
$pageFiles = [];
|
||||
foreach ($groups as $group) foreach (($group['files'] ?? []) as $path) if ($path !== '/index.html' && $path !== '/sw.js' && isset($files[$path])) {
|
||||
$pageFiles[$path] = $files[$path];
|
||||
if (!$hashing) $pageFiles[$path]['h'] = $tag;
|
||||
}
|
||||
$json = json_encode(['buildTag' => $tag, 'groups' => (object)$groups, 'files' => (object)$pageFiles, 'appCache' => $assetSync], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
||||
$json = str_replace('<', '\\u003c', $json);
|
||||
return preg_replace('~(<script type="application/json" id="ytp-assets">).*?(</script>)~s', '$1' . $json . '$2', $source, 1) ?? $source;
|
||||
}
|
||||
private function stampIndex(string $source, array $files, string $tag, bool $hashing): string
|
||||
{
|
||||
if (!$hashing) {
|
||||
$source = str_replace('__BUILD_TAG__', $tag, $source);
|
||||
return preg_replace_callback('~((?:href|src)=")((?![a-z]+:|/)[^"?]+\.(?:css|js))(\")~i', fn($m) => $m[1] . $m[2] . '?v=' . $tag . $m[3], $source) ?? $source;
|
||||
}
|
||||
$source = preg_replace_callback('~<(?:script|link)\b[^>]*>~i', function($match) use ($files) {
|
||||
return preg_replace_callback('~\b(href|src)=(["\'])([^"\']+)\2~i', function($attr) use ($files) {
|
||||
$url = $attr[3];
|
||||
if (preg_match('~^(?:[a-z][a-z\d+.-]*:|//|#)~i', $url)) return $attr[0];
|
||||
$parts = parse_url($url);
|
||||
$path = $parts['path'] ?? $url;
|
||||
$path = '/' . ltrim(rawurldecode($path), '/');
|
||||
if (!isset($files[$path]) || !preg_match('~\.(?:css|js)$~i', $path) || $path === '/sw.js') return $attr[0];
|
||||
parse_str($parts['query'] ?? '', $query); $query['v'] = $files[$path]['h'];
|
||||
$newUrl = ($parts['path'] ?? $url) . '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986) . (isset($parts['fragment']) ? '#' . $parts['fragment'] : '');
|
||||
return $attr[1] . '=' . $attr[2] . $newUrl . $attr[2];
|
||||
}, $match[0]) ?? $match[0];
|
||||
}, $source) ?? $source;
|
||||
return str_replace('__BUILD_TAG__', $tag, $source);
|
||||
}
|
||||
|
||||
public function prepareCompression(): void
|
||||
{
|
||||
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->root, FilesystemIterator::SKIP_DOTS));
|
||||
foreach ($it as $entry) if ($entry->isFile()) {
|
||||
$path = '/' . str_replace(DIRECTORY_SEPARATOR, '/', substr($entry->getPathname(), strlen($this->root) + 1));
|
||||
$raw = file_get_contents($entry->getPathname());
|
||||
$this->compressed[$path] = preg_match('~\.(?:js|css|html|json|webmanifest|svg|txt)$~i', $path)
|
||||
? self::compress($raw) : ['raw'=>$raw,'gz'=>null,'br'=>null,'etag'=>'"'.substr(hash('sha256',$raw),0,32).'"'];
|
||||
}
|
||||
$this->compressed['/index.html'] = self::compress($this->index);
|
||||
$this->compressed['/sw.js'] = self::compress($this->serviceWorker);
|
||||
}
|
||||
public static function compress(string $raw): array
|
||||
{
|
||||
$br = null;
|
||||
if (function_exists('brotli_compress')) $br = brotli_compress($raw, 5, BROTLI_TEXT);
|
||||
elseif (is_executable('/usr/bin/brotli')) {
|
||||
$output = sys_get_temp_dir() . '/ytp-brotli-' . bin2hex(random_bytes(8));
|
||||
$p = proc_open(['/usr/bin/brotli', '-q', '5', '-c'], [['pipe','r'],['file',$output,'w'],['file','/dev/null','w']], $pipes, null, null, ['bypass_shell'=>true]);
|
||||
if (is_resource($p)) {
|
||||
fwrite($pipes[0], $raw); fclose($pipes[0]);
|
||||
if (proc_close($p) === 0 && is_file($output)) $br = file_get_contents($output);
|
||||
@unlink($output);
|
||||
}
|
||||
}
|
||||
return ['raw' => $raw, 'gz' => gzencode($raw, 9), 'br' => $br, 'etag' => '"' . substr(hash('sha256', $raw), 0, 32) . '"'];
|
||||
}
|
||||
public function cacheControl(string $path, ?string $version, bool $hashing): string
|
||||
{
|
||||
$short = preg_match('~(^|/)(fonts|icons)/~', $path) ? 'public, max-age=2592000' : 'no-cache';
|
||||
if (!$hashing && $path === '/index.html') return 'no-cache';
|
||||
$binaryLegacy = !$hashing && !preg_match('~\.(?:js|css|html|json|webmanifest|svg|txt)$~i', $path);
|
||||
if ($version !== null) {
|
||||
$current = $hashing ? ($this->delivery[$path]['h'] ?? null) : $this->manifest['buildTag'];
|
||||
if ($current !== null && hash_equals((string)$current, $version)) return 'public, max-age=31536000, immutable';
|
||||
return $hashing ? 'no-store' : 'no-cache';
|
||||
}
|
||||
return preg_match('~(^|/)(fonts|icons)/~', $path) && ($hashing || $binaryLegacy) ? 'public, max-age=2592000' : 'no-cache';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user