Bundle Puppeteer Dependency

This commit is contained in:
Salastil
2025-11-23 01:53:57 -05:00
parent c524e2c990
commit d99e44e996
5 changed files with 151 additions and 2 deletions

View File

@@ -1,2 +1,17 @@
# streamed-tui
TUI Application for launching streamed.pk feeds
## Bundled Puppeteer dependencies
The extractor relies on `puppeteer-extra`, `puppeteer-extra-plugin-stealth`, and `puppeteer`. These Node.js packages are
bundled into the final binary via `internal/assets/node_modules.tar.gz`. To refresh the archive (for example after updating
dependency versions), run:
```
scripts/build_node_modules.sh
```
The script installs the dependencies into a temporary directory and regenerates the tarball so the Go binary can extract
them at runtime without requiring `npm install` on the target system. When the binary starts it will automatically unpack the
archive into the user's cache directory (or `$TMPDIR` fallback) and point Puppeteer at that cached `node_modules` tree, so the
program can run as a single self-contained executable even when no dependencies exist alongside it.

Binary file not shown.

104
internal/dependencies.go Normal file
View File

@@ -0,0 +1,104 @@
package internal
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/sha256"
_ "embed"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"time"
)
//go:embed assets/node_modules.tar.gz
var embeddedNodeModules []byte
// ensureEmbeddedNodeModules extracts the bundled Node.js dependencies into a
// deterministic cache directory derived from the archive hash and returns the
// path that contains the resulting node_modules directory.
func ensureEmbeddedNodeModules() (string, error) {
if len(embeddedNodeModules) == 0 {
return "", errors.New("no embedded node modules archive available")
}
sum := sha256.Sum256(embeddedNodeModules)
hashPrefix := hex.EncodeToString(sum[:8])
cacheRoot, err := os.UserCacheDir()
if err != nil {
cacheRoot = os.TempDir()
}
baseDir := filepath.Join(cacheRoot, "streamed-tui", "node_modules", hashPrefix)
marker := filepath.Join(baseDir, ".complete")
if _, err := os.Stat(marker); err == nil {
return baseDir, nil
}
if err := os.RemoveAll(baseDir); err != nil {
return "", fmt.Errorf("failed to clear embedded node cache: %w", err)
}
if err := os.MkdirAll(baseDir, 0o755); err != nil {
return "", fmt.Errorf("failed to create embedded node cache: %w", err)
}
if err := untarGzip(bytes.NewReader(embeddedNodeModules), baseDir); err != nil {
return "", fmt.Errorf("failed to extract embedded node modules: %w", err)
}
if err := os.WriteFile(marker, []byte(time.Now().Format(time.RFC3339)), 0o644); err != nil {
return "", fmt.Errorf("failed to mark embedded node modules ready: %w", err)
}
return baseDir, nil
}
func untarGzip(r io.Reader, dest string) error {
gz, err := gzip.NewReader(r)
if err != nil {
return err
}
defer gz.Close()
tr := tar.NewReader(gz)
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return err
}
target := filepath.Join(dest, hdr.Name)
switch hdr.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(target, os.FileMode(hdr.Mode)); err != nil {
return err
}
case tar.TypeReg:
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(hdr.Mode))
if err != nil {
return err
}
if _, err := io.Copy(f, tr); err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
default:
// Ignore unsupported entries to keep extraction simple.
}
}
return nil
}

View File

@@ -71,7 +71,11 @@ func findNodeModuleBase() (string, error) {
}
}
return "", errors.New("puppeteer-extra not found; install dependencies with npm in the project directory")
if extracted, err := ensureEmbeddedNodeModules(); err == nil {
return extracted, nil
}
return "", errors.New("puppeteer-extra not found; install dependencies with npm in the project directory or rebuild the embedded archive")
}
func (l *logBuffer) Write(p []byte) (int, error) {
@@ -134,7 +138,11 @@ func ensurePuppeteerAvailable(baseDir string) error {
check.Env = append(os.Environ(), fmt.Sprintf("STREAMED_TUI_NODE_BASE=%s", baseDir))
if err := check.Run(); err != nil {
return fmt.Errorf("puppeteer-extra or stealth plugin missing in %s. Run `npm install puppeteer-extra puppeteer-extra-plugin-stealth puppeteer` there: %w", baseDir, err)
if embedded, embErr := ensureEmbeddedNodeModules(); embErr == nil && embedded != baseDir {
return ensurePuppeteerAvailable(embedded)
}
return fmt.Errorf("puppeteer-extra or stealth plugin missing in %s. Run `npm install puppeteer-extra puppeteer-extra-plugin-stealth puppeteer` there or rebuild the embedded archive with scripts/build_node_modules.sh: %w", baseDir, err)
}
return nil

22
scripts/build_node_modules.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ASSETS_DIR="$ROOT_DIR/internal/assets"
ARCHIVE="$ASSETS_DIR/node_modules.tar.gz"
if ! command -v npm >/dev/null 2>&1; then
echo "npm is required to bundle node_modules" >&2
exit 1
fi
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
pushd "$TMP_DIR" >/dev/null
npm install puppeteer-extra puppeteer-extra-plugin-stealth puppeteer
mkdir -p "$ASSETS_DIR"
tar -czf "$ARCHIVE" node_modules
popd >/dev/null
echo "Bundled node_modules into $ARCHIVE"