@ 261105.4
current
import path from 'node:path';

import GitHubTree from '../github/tree.js';
import {
  writeFile, readJson, writeJson, removeDirectory
} from '../file.js';
import {
  shortSha
} from '../utils.js';

export class WPTUpdater {
  constructor(path, cli, request, nodedir, commit) {
    this.path = path;
    this.nodedir = nodedir;

    this.cli = cli;
    this.request = request;
    this.treeParams = {
      owner: 'web-platform-tests',
      repo: 'wpt',
      branch: 'master',
      path,
      commit
    };
    this.tree = new GitHubTree(cli, request, this.treeParams);
    this.assets = [];
  }

  fixtures(...args) {
    return path.join(this.nodedir, 'test', 'fixtures', 'wpt', ...args);
  }

  get versionsPath() {
    return this.fixtures('versions.json');
  }

  getLocalVersions() {
    return readJson(this.versionsPath);
  }

  // If filepath starts with '/', the path is relative to WPT project root,
  // otherwise it's relative to the path of this updater
  async pullTextFile(dest, filepath) {
    const content = await this.tree.buffer(filepath);
    const filename = path.join(dest, filepath);
    writeFile(filename, content);
    this.cli.updateSpinner(`Downloaded ${filename}`);
  }

  async getAssetList() {
    this.cli.startSpinner(`Querying asset list for ${this.path}...`);
    const assets = this.assets = await this.tree.getFiles();
    this.cli.stopSpinner(
      `Read asset list, ${assets.length} files in total.`
    );
    return assets;
  }

  async pullAllAssets(assets) {
    const fixtures = this.fixtures(this.path);
    this.cli.separator(`Writing assets to ${fixtures}...`);

    if (!assets) {
      assets = await this.getAssetList();
    }

    this.cli.startSpinner('Removing stale assets...');
    await removeDirectory(this.fixtures(this.path));

    this.cli.startSpinner('Pulling assets...');
    // See https://github.com/nodejs/node-core-utils/issues/810
    for (let i = 0; i < assets.length; i += 10) {
      const chunk = assets.slice(i, i + 10);
      await Promise.all(chunk.map(
        (asset) => this.pullTextFile(fixtures, asset.name)
      ));
    }
    this.cli.stopSpinner(`Downloaded ${assets.length} assets.`);

    return assets;
  }

  getTreeUrl(path, commit) {
    const params = Object.assign({}, this.treeParams, { commit, path });
    const tree = new GitHubTree(this.cli, this.request, params);
    return tree.getPermanentUrl();
  }

  /**
   * @param {Object<string, {commit: string, path: string}>} updated
   */
  async updateVersions(updated) {
    const versionsPath = this.versionsPath;
    let versions = this.getLocalVersions();

    this.cli.startSpinner('Updating versions.json ...');
    Object.assign(versions, updated);
    // Reorder keys alphabetically
    versions = Object.fromEntries(
      Object.entries(versions).sort(([key1], [key2]) =>
        key1.localeCompare(key2)
      )
    );
    writeJson(versionsPath, versions);
    this.cli.stopSpinner(`Updated ${versionsPath}`);
  }

  async updateLicense() {
    this.cli.startSpinner('Updating license...');
    await this.pullTextFile(this.fixtures(), '/LICENSE.md');
    this.cli.stopSpinner(`Updated ${this.fixtures('LICENSE.md')}.`);
  }

  async checkNeedsUpdate(key = this.path) {
    this.cli.separator(`Checking updates for ${key}...`);

    const versions = this.getLocalVersions();
    const localCommit = versions[key] && versions[key].commit;
    if (!localCommit) {
      this.cli.log(`No records for ${key} in local ${this.versionsPath}`);
      this.cli.log('pulling files from scratch...');
      return true;
    }

    const rev = shortSha(localCommit);
    this.cli.log(`Last local update for ${key} is ${rev}`);
    this.cli.startSpinner('checking updates...');
    const lastCommit = await this.tree.getLastCommit();

    if (localCommit === lastCommit) {
      this.cli.stopSpinner(
        `${key} is up to date with upstream (${rev})`
      );
      return false;
    }

    const upstreamRev = shortSha(lastCommit);
    this.cli.stopSpinner(`Last update in upstream is ${upstreamRev}`);
    return true;
  }

  async update() {
    const needsUpdate = await this.checkNeedsUpdate();
    if (!needsUpdate) {
      return;
    }
    await this.pullAllAssets();

    const lastCommit = await this.tree.getLastCommit();
    await this.updateVersions({
      [this.path]: {
        commit: lastCommit,
        path: this.path
      }
    });
  }
}

export class ResourcesUpdater extends WPTUpdater {
  constructor(cli, request, nodedir, commit) {
    super('resources', cli, request, nodedir, commit);
  }

  async update() {  // override
    const needsUpdate = await this.checkNeedsUpdate();
    if (!needsUpdate) {
      return;
    }

    const assets = await this.getAssetList();
    const excludes = ['test/', 'chromium/', 'webidl2/test'];
    const toDownload = assets.filter(asset =>
      !excludes.find(prefix => asset.name.startsWith(prefix))
    );
    const excluded = assets.length - toDownload.length;
    this.cli.log(`Excluded ${excluded} files, ` +
                 `${toDownload.length} files to write.`);

    await this.pullAllAssets(toDownload);

    const lastCommit = this.tree.lastCommit;
    await this.updateVersions({
      [this.path]: {
        commit: lastCommit,
        path: this.path
      }
    });
  }
}

export class InterfacesUpdater extends WPTUpdater {
  constructor(cli, request, nodedir, commit, supported) {
    super('interfaces', cli, request, nodedir, commit);
    this.supported = supported;
  }

  async update() {  // override
    const needsUpdate = await this.checkNeedsUpdate();
    if (!needsUpdate) {
      return;
    }
    const supported = this.supported;
    const assets = await this.getAssetList();
    const found = [];
    for (const mod of supported) {
      const idl = `${mod}.idl`;
      const asset = assets.find(asset => asset.name === idl);
      if (asset) {
        found.push(asset);
      } else {
        this.cli.warn(`Couldn't find ${idl} in the upstream`);
      }
    }

    await this.pullAllAssets(found);

    const lastCommit = this.tree.lastCommit;
    await this.updateVersions({
      [this.path]: {
        commit: lastCommit,
        path: this.path
      }
    });
  }
}