All files / src/tools/shared paths.ts

90.9% Statements 20/22
100% Branches 20/20
100% Functions 6/6
90.9% Lines 20/22

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98                        18x             4x 4x 4x   4x 4x                         22x 19x     3x 6x                             22x 22x   22x       18x     18x     18x             18x                       1x 1x   1x          
/**
 * Where the framework sits, and where the repo mounting it sits.
 *
 * A face or a family mounts the framework one folder down, at paf/ when paf fills it, so its faces, build
 * sandboxes, and node_modules/ live one level above the framework. The folder can have any name. Checked
 * out on its own, the framework is its own workspace and holds no faces.
 */
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
 
/** The appinfo that makes a folder a face, which sits at the face's own root. */
export const APPINFO_REL = 'pebble.appinfo.json';
 
/**
 * Whether a folder's package.json lists a workspace by name, as `name`, `./name`, or `name/`, in the plain
 * list form or under packages.
 */
function listsWorkspace(dir: string, name: string): boolean {
  try {
    const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
    const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces && pkg.workspaces.packages;
 
    return Array.isArray(workspaces)
      && workspaces.some((entry: unknown) => typeof entry === 'string' && entry.replace(/^\.\//, '').replace(/\/+$/, '') === name);
  } catch (error) {
    return false;
  }
}
 
/**
 * Whether a folder is a family: a core/ beside at least one face.
 *
 * @param dir The folder to look in.
 * @return True when the folder holds a core/ and a face beside it.
 */
export function isFamilyRoot(dir: string): boolean {
  if (!fs.existsSync(path.join(dir, 'core'))) {
    return false;
  }
 
  return fs.readdirSync(dir, { withFileTypes: true })
    .some((entry) => entry.isDirectory() && fs.existsSync(path.join(dir, entry.name, APPINFO_REL)));
}
 
/**
 * The workspace a framework folder belongs to: the repo of faces above it, or the framework itself.
 *
 * The folder above counts when its package.json lists the framework's folder in workspaces and it is
 * a face, or a family with its faces beside a core/. The name the framework's folder was given plays no
 * part. The workspaces listing is what stops a framework cloned on its own from mistaking whatever
 * folder it sits in for a repo of faces.
 *
 * @param engine The framework's folder.
 * @return The repo of faces mounting it, or the framework's own folder when nothing mounts it.
 */
export function workspaceFor(engine: string): string {
  const parent = path.dirname(engine);
  const holdsFaces = fs.existsSync(path.join(parent, APPINFO_REL)) || isFamilyRoot(parent);
 
  return holdsFaces && listsWorkspace(parent, path.basename(engine)) ? parent : engine;
}
 
/** The framework root. */
export const ENGINE = path.resolve(import.meta.dirname, '..', '..');
 
/** The repo mounting the framework, or the framework itself when it is checked out on its own. */
export const WORKSPACE = workspaceFor(ENGINE);
 
/** Whether a repo of faces mounts the framework. */
export const MOUNTED = WORKSPACE !== ENGINE;
 
/**
 * The framework's folder relative to the workspace, with forward slashes: paf in a unit paf fills,
 * or . when the framework is checked out on its own. Anything that writes the framework's path into
 * a build uses this rather than assuming a name.
 */
export const ENGINE_REL = path.relative(WORKSPACE, ENGINE).split(path.sep).join('/') || '.';
 
/**
 * Where ical.js's prebuilt ES5 CommonJS file sits, found the way node finds the package from the
 * framework: its own node_modules first, then the mounting repo's once npm workspaces hoist it.
 *
 * The package's `exports` map blocks asking for the file by its subpath, so this resolves the
 * package's `require` entry, which sits in the same dist/ folder.
 *
 * @return The file's absolute path, or null when ical.js is not installed.
 */
export function icaljsBundle(): string | null {
  try {
    const entry = createRequire(path.join(ENGINE, 'package.json')).resolve('ical.js');
 
    return path.join(path.dirname(entry), 'ical.es5.min.cjs');
  } catch {
    return null;
  }
}