Ctrl K
ComunidadGuías…

setup-assets.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * Emberline — Asset download script
 * ──────────────────────────────────
 * Run once: node setup-assets.js
 *
 * Downloads the fonts and copies the crypto library (from node_modules) so
 * Emberline runs fully self-hosted with zero external requests.
 */

const https = require('https');
const fs    = require('fs');
const path  = require('path');

const FONTS_DIR = path.join(__dirname, 'public', 'fonts');
const JS_DIR    = path.join(__dirname, 'public', 'vendor');

// Create directories
if (!fs.existsSync(FONTS_DIR)) fs.mkdirSync(FONTS_DIR, { recursive: true });
if (!fs.existsSync(JS_DIR))    fs.mkdirSync(JS_DIR, { recursive: true });

// Robust file download:
//   - rejects on any non-2xx status (the original would write a 404 HTML body
//     into nacl-fast.min.js and produce a silently broken build)
//   - follows 301, 302, 303, 307, 308 redirects up to 5 hops
//   - resolves relative Location headers against the current URL
//   - cleans up partial files on any failure path
function download(url, dest, redirectsLeft = 5) {
  return new Promise((resolve, reject) => {
    const file    = fs.createWriteStream(dest);
    const cleanup = () => { try { file.close(); } catch {} fs.unlink(dest, () => {}); };

    const req = https.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, res => {
      const code = res.statusCode;

      // Redirect: drain body, clean placeholder, recurse
      if ([301, 302, 303, 307, 308].includes(code)) {
        res.resume();
        file.close();
        fs.unlink(dest, () => {});
        if (redirectsLeft <= 0) return reject(new Error(`Too many redirects: ${url}`));
        const next = new URL(res.headers.location, url).toString();
        return download(next, dest, redirectsLeft - 1).then(resolve, reject);
      }

      // Any other non-2xx is a hard failure — don't save the body
      if (code < 200 || code >= 300) {
        res.resume();
        cleanup();
        return reject(new Error(`HTTP ${code} fetching ${url}`));
      }

      res.pipe(file);
      file.on('finish', () => { file.close(); resolve(dest); });
      file.on('error',  err => { cleanup(); reject(err); });
    });

    req.on('error', err => { cleanup(); reject(err); });
  });
}

// Google Fonts picks the font format from the User-Agent. A generic UA gets
// .ttf links; a current desktop browser UA gets .woff2 split by unicode-range.
const FONT_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';

// Unicode subsets to self-host. Browsers only fetch the subsets a page needs,
// so cyrillic/greek/vietnamese would just be dead weight in the image.
const FONT_SUBSETS = new Set(['latin', 'latin-ext']);

function fetchFontCSS(url) {
  return new Promise((resolve, reject) => {
    https.get(url, { headers: { 'User-Agent': FONT_UA } }, res => {
      if (res.statusCode < 200 || res.statusCode >= 300) {
        res.resume();
        return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
      }
      let data = '';
      res.on('data',  chunk => data += chunk);
      res.on('end',   () => resolve(data));
      res.on('error', reject);
    }).on('error', reject);
  });
}

async function main() {
  console.log('Emberline asset setup\n');

  // ── 1. TweetNaCl ────────────────────────────────────────────────────────────
  // Copied from node_modules rather than downloaded: npm verifies each package
  // against the integrity hash pinned in package-lock.json, so a tampered
  // CDN or registry response fails `npm ci` instead of shipping to users.
  console.log('Copying NaCl libraries from node_modules...');
  for (const [pkgFile, name] of [
    ['tweetnacl/nacl-fast.min.js',      'nacl-fast.min.js'],
    ['tweetnacl-util/nacl-util.min.js', 'nacl-util.min.js'],
  ]) {
    fs.copyFileSync(require.resolve(pkgFile), path.join(JS_DIR, name));
    console.log(`  ✓ public/vendor/${name}`);
  }

  // ── 2. Google Fonts ──────────────────────────────────────────────────────────
  console.log('\nFetching font CSS...');
  const fontCSS = await fetchFontCSS(
    'https://fonts.googleapis.com/css2?family=Manrope:wght@300;400;500;700&family=Playfair+Display:ital,wght@1,400&display=swap'
  );

  // Google prefixes each @font-face block with a /* subset */ comment.
  // Keep only the blocks for the subsets we self-host.
  const blocks = [...fontCSS.matchAll(/\/\*\s*([\w-]+)\s*\*\/\s*(@font-face\s*\{[^}]+\})/g)]
    .filter(m => FONT_SUBSETS.has(m[1]))
    .map(m => m[2]);
  if (blocks.length === 0) throw new Error('No @font-face blocks found for subsets: ' + [...FONT_SUBSETS].join(', '));

  const urlRegex = /url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.woff2)\)/g;
  const uniqueUrls = [...new Set(blocks.flatMap(b => [...b.matchAll(urlRegex)].map(m => m[1])))];
  console.log(`  Found ${blocks.length} @font-face blocks, ${uniqueUrls.length} font files`);

  // Download each font file and track filename → url mapping
  const fontMap = {};
  for (const url of uniqueUrls) {
    const filename = url.split('/').pop().split('?')[0];
    const dest = path.join(FONTS_DIR, filename);
    await download(url, dest);
    fontMap[url] = `/fonts/${filename}`;
    console.log(`  ✓ public/fonts/${filename}`);
  }

  // ── 3. Build local @font-face CSS ────────────────────────────────────────────
  console.log('\nGenerating public/fonts/fonts.css...');
  const fontFaceBlocks = blocks
    .map(b => b.replace(urlRegex, (_, remoteUrl) => `url(${fontMap[remoteUrl]})`))
    .join('\n\n');

  // Any remaining remote URL would be blocked by CSP (font-src 'self') and
  // silently fall back to system fonts. Fail the build instead.
  if (/url\(\s*['"]?https?:/i.test(fontFaceBlocks)) {
    throw new Error('fonts.css still references remote URLs (unexpected format from Google Fonts?)');
  }

  fs.writeFileSync(path.join(FONTS_DIR, 'fonts.css'), fontFaceBlocks);
  console.log('  ✓ public/fonts/fonts.css');

  console.log('\nAll assets downloaded. Your folder structure:');
  console.log('  (project root)');
  console.log('  ├── server.js');
  console.log('  ├── package.json');
  console.log('  └── public/');
  console.log('      ├── index.html');
  console.log('      ├── vendor/');
  console.log('      │   ├── nacl-fast.min.js');
  console.log('      │   └── nacl-util.min.js');
  console.log('      └── fonts/');
  console.log('          ├── fonts.css');
  console.log('          └── [woff2 font files]');
  console.log('\nRestart your server and you\'re fully self-hosted.');
}

main().catch(err => {
  console.error('Setup failed:', err.message);
  process.exit(1);
});