emmercm/igir

View on GitHub
package.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import * as child_process from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';

import { path7za } from '7zip-bin';
import caxa from 'caxa';
import fg, { Options as GlobOptions } from 'fast-glob';
import yargs from 'yargs';

import Logger from './src/console/logger.js';
import LogLevel from './src/console/logLevel.js';
import Package from './src/globals/package.js';
import FsPoly from './src/polyfill/fsPoly.js';
import ExpectedError from './src/types/expectedError.js';

interface FileFilter extends GlobOptions {
  include?: string;
  exclude?: string;
}

const fileFilter = (filters: FileFilter[]): string[] => {
  let results: string[] = [];
  filters.forEach((filter) => {
    if (filter.include) {
      const include = fg
        .globSync(filter.include.replace(/\\/g, '/'), filter)
        .map((file) => path.resolve(file));
      if (include.length === 0) {
        throw new ExpectedError(`glob pattern '${filter.include}' returned no paths`);
      }
      results = [...results, ...include];
    }
    if (filter.exclude) {
      const exclude = new Set(
        fg.globSync(filter.exclude.replace(/\\/g, '/'), filter).map((file) => path.resolve(file)),
      );
      if (exclude.size === 0) {
        throw new ExpectedError(`glob pattern '${filter.exclude}' returned no paths`);
      }
      results = results.filter((result) => !exclude.has(result));
    }
  });
  return results;
};

(async (): Promise<void> => {
  const logger = new Logger(LogLevel.TRACE);

  const argv = await yargs(process.argv.slice(2))
    .locale('en')
    .usage('Usage: $0 <input> <output>')
    .positional('input', {
      description: 'input directory',
      type: 'string',
      default: '.',
    })
    .check((_argv) => {
      if (!_argv.input || !fs.existsSync(_argv.input)) {
        throw new ExpectedError(`input directory '${_argv.input}' doesn't exist`);
      }
      return true;
    })
    .positional('output', {
      description: 'output file',
      type: 'string',
      default: Package.NAME + (process.platform === 'win32' ? '.exe' : ''),
    }).argv;

  const input = path.resolve(argv.input);
  logger.info(`Input: '${input}'`);

  const include = new Set(
    fileFilter([
      // Start with the files we need
      { include: 'dist{,/**}', onlyFiles: false },
      { include: 'node_modules{,/**}', onlyFiles: false },
      { include: 'package*.json' },
      // Exclude unnecessary JavaScript files
      { exclude: 'dist/test/**' },
      { exclude: 'dist/{**/,}*.test.*' },
      { exclude: '**/jest.config.(js|ts|mjs|cjs|json)' },
      { exclude: '**/tsconfig*' },
      { exclude: '**/*.d.ts' },
      { exclude: '**/*.(js|ts).map' },
      // Exclude unnecessary docs files
      { exclude: 'node_modules/**/docs/{**/,}*.md' },
      {
        exclude:
          'node_modules/**/(AUTHORS|CHANGELOG|CHANGES|CODE_OF_CONDUCT|CONTRIBUTING|GOVERNANCE|HISTORY|LICENSE|README|RELEASE|RELEASE-NOTES|SECURITY|TROUBLESHOOTING){,*.md,*.markdown,*.txt}',
        caseSensitiveMatch: false,
      },
      // Only include the exact 7zip-bin we need
      { exclude: 'node_modules/{**/,}7zip-bin/**/7z*' },
      { include: path7za },
      // Only include the exact chdman bin we need
      { exclude: 'node_modules/{**/,}chdman/bin/*/*/chdman*' },
      { include: `node_modules/{**/,}chdman/bin/${process.platform}/${process.arch}/chdman*` },
      // Only include the exact maxcso bin we need
      { exclude: 'node_modules/{**/,}maxcso/bin/*/*/maxcso*' },
      { include: `node_modules/{**/,}maxcso/bin/${process.platform}/${process.arch}/maxcso*` },
    ]),
  );
  const includeSize = (
    await Promise.all(
      [...include].map(async (file) => {
        if (await FsPoly.isDirectory(file)) {
          return 0;
        }
        return FsPoly.size(file);
      }),
    )
  ).reduce((sum, size) => sum + size, 0);
  logger.info(
    `Include: found ${FsPoly.sizeReadable(includeSize)} of ${include.size.toLocaleString()} file${include.size !== 1 ? 's' : ''} to include`,
  );

  const exclude = fileFilter([{ include: '*{,/**}', onlyFiles: false, dot: true }]).filter(
    (file) => !include.has(file),
  );
  const excludeSize = (
    await Promise.all(
      exclude.map(async (file) => {
        if (await FsPoly.isDirectory(file)) {
          return 0;
        }
        return FsPoly.size(file);
      }),
    )
  ).reduce((sum, size) => sum + size, 0);
  logger.info(
    `Exclude: found ${FsPoly.sizeReadable(excludeSize)} of ${exclude.length.toLocaleString()} file${exclude.length !== 1 ? 's' : ''} to exclude`,
  );
  const excludeGlobs = exclude.map((glob) => fg.convertPathToPattern(glob));

  const output = path.resolve(argv.output);
  logger.info(`Output: '${input}'`);

  logger.info('Building ...');
  await caxa({
    input,
    output,
    exclude: excludeGlobs,
    command: [
      `{{caxa}}/node_modules/.bin/node${process.platform === 'win32' ? '.exe' : ''}`,
      '{{caxa}}/dist/index.js',
    ],
  });

  if (!(await FsPoly.exists(output))) {
    throw new ExpectedError(`output file '${output}' doesn't exist`);
  }
  logger.info(`Output: ${FsPoly.sizeReadable(await FsPoly.size(output))}`);

  const proc = child_process.spawn(output, ['--help'], { windowsHide: true });
  let procOutput = '';
  proc.stdout.on('data', (chunk) => {
    procOutput += chunk.toString();
  });
  proc.stderr.on('data', (chunk) => {
    procOutput += chunk.toString();
  });
  await new Promise((resolve, reject) => {
    proc.on('close', resolve);
    proc.on('error', reject);
  });
  logger.trace(procOutput);
})();