maxmilton/new-tab

View on GitHub
build.ts

Summary

Maintainability
A
0 mins
Test Coverage
/* eslint-disable no-console */

import { importPlugin } from '@ekscss/plugin-import';
import * as xcss from 'ekscss';
import * as lightningcss from 'lightningcss';
import { readdir } from 'node:fs/promises';
import { basename } from 'node:path';
import * as terser from 'terser';
import { createManifest } from './manifest.config';

const mode = Bun.env.NODE_ENV;
const dev = mode === 'development';

/**
 * Generate minified CSS from XCSS source.
 */
function compileCSS(src: string, from: string) {
  const compiled = xcss.compile(src, {
    from,
    plugins: [importPlugin],
  });

  for (const warning of compiled.warnings) {
    console.error('XCSS:', warning.message);

    if (warning.file) {
      console.log(
        `  at ${[warning.file, warning.line, warning.column]
          .filter((x) => x != null)
          .join(':')}`,
      );
    }
  }

  const minified = lightningcss.transform({
    filename: from,
    code: Buffer.from(compiled.css),
    minify: !dev,
    // eslint-disable-next-line no-bitwise
    targets: { chrome: 116 << 16 }, // matches manifest minimum_chrome_version
  });

  for (const warning of minified.warnings) {
    console.error('CSS:', warning.message);
  }

  return minified.code.toString();
}

/**
 * Construct HTML and CSS files and save them to disk.
 */
async function makeHTML(pageName: string, stylePath: string) {
  const styleSrc = await Bun.file(stylePath).text();
  const css = compileCSS(styleSrc, stylePath);
  const template = `
    <!doctype html>
    <meta charset=utf-8>
    <meta name=google value=notranslate>
    <title>New Tab</title>
    <link href=${pageName}.css rel=stylesheet>
    <script src=${pageName}.js type=module async></script>
  `
    .trim()
    .replaceAll(/\n\s+/g, '\n'); // remove leading whitespace

  await Bun.write(`dist/${pageName}.css`, css);
  await Bun.write(`dist/${pageName}.html`, template);
}

/**
 * Compile all themes, combine into a single JSON file, and save it to disk.
 */
async function makeThemes() {
  const themeFiles = await readdir('src/css/themes');
  const themes: Record<string, string> = {};

  for (const theme of themeFiles.sort()) {
    if (theme.endsWith('.xcss')) {
      const path = `src/css/themes/${theme}`;
      // eslint-disable-next-line no-await-in-loop
      const code = await Bun.file(path).text();
      themes[basename(theme, '.xcss')] = compileCSS(code, path);
    }
  }

  await Bun.write('dist/themes.json', JSON.stringify(themes));
}

async function minifyJS(artifact: Blob & { path: string }) {
  let source = await artifact.text();

  // Improve collapsing variables; terser doesn't do this so we do it manually.
  source = source.replaceAll('const ', 'let ');

  const result = await terser.minify(source, {
    ecma: 2020,
    module: true,
    compress: {
      reduce_funcs: false, // prevent functions being inlined
      // XXX: Comment out to keep performance markers for debugging.
      pure_funcs: ['performance.mark', 'performance.measure'],
      passes: 3,
    },
    mangle: {
      properties: {
        regex: /^\$\$|^__click$/,
      },
    },
  });

  await Bun.write(artifact.path, result.code!);
}

// Extension manifest
await Bun.write('dist/manifest.json', JSON.stringify(createManifest()));

console.time('html+css');
await makeHTML('newtab', 'src/css/newtab.xcss');
await makeHTML('settings', 'src/css/settings.xcss');
console.timeEnd('html+css');

console.time('themes');
await makeThemes();
console.timeEnd('themes');

// New Tab & Settings apps
console.time('build');
const out = await Bun.build({
  entrypoints: ['src/newtab.ts', 'src/settings.ts'],
  outdir: 'dist',
  target: 'browser',
  minify: !dev,
  sourcemap: dev ? 'external' : 'none',
});
console.timeEnd('build');
console.log(out);

// Background service worker script
console.time('build2');
const out2 = await Bun.build({
  entrypoints: ['src/sw.ts'],
  outdir: 'dist',
  target: 'browser',
  minify: !dev,
});
console.timeEnd('build2');
console.log(out2);

if (!dev) {
  console.time('minify');
  await minifyJS(out.outputs[0]);
  await minifyJS(out.outputs[1]);
  await minifyJS(out2.outputs[0]);
  console.timeEnd('minify');
}