Skip to content

Commit

Permalink
refactor: add portable bindings for the filesystem
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone committed Nov 22, 2024
1 parent dcec676 commit 449f354
Show file tree
Hide file tree
Showing 34 changed files with 641 additions and 196 deletions.
11 changes: 8 additions & 3 deletions packages/config/maintenance/importConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ process.on("unhandledRejection", (r) => {
});

import { CommandClasses, getIntegerLimits } from "@zwave-js/core";
import { fs as nodeFS } from "@zwave-js/core/bindings/fs/node";
import {
enumFilesRecursive,
formatId,
Expand Down Expand Up @@ -852,6 +853,7 @@ async function parseZWAFiles(): Promise<void> {
let jsonData = [];

const configFiles = await enumFilesRecursive(
nodeFS,
zwaTempDir,
(file) => file.endsWith(".json"),
);
Expand Down Expand Up @@ -1602,23 +1604,25 @@ async function maintenanceParse(): Promise<void> {
const zwaData = [];

// Load the zwa files
await fs.mkdir(zwaTempDir, { recursive: true });
await nodeFS.ensureDir(zwaTempDir);
const zwaFiles = await enumFilesRecursive(
nodeFS,
zwaTempDir,
(file) => file.endsWith(".json"),
);
for (const file of zwaFiles) {
// zWave Alliance numbering isn't always continuous and an html page is
// returned when a device number doesn't. Test for and delete such files.
try {
zwaData.push(await readJSON(file));
zwaData.push(await readJSON(nodeFS, file));
} catch {
await fs.unlink(file);
}
}

// Build the list of device files
const configFiles = await enumFilesRecursive(
nodeFS,
processedDir,
(file) => file.endsWith(".json"),
);
Expand Down Expand Up @@ -2033,7 +2037,7 @@ async function importConfigFilesOH(): Promise<void> {
}
}
outFilename += ".json";
await fs.ensureDir(path.dirname(outFilename));
await nodeFS.ensureDir(path.dirname(outFilename));

const output = stringify(parsed, "\t") + "\n";
await fs.writeFile(outFilename, output, "utf8");
Expand Down Expand Up @@ -2305,6 +2309,7 @@ function getLatestConfigVersion(
/** Changes the manufacturer names in all device config files to match manufacturers.json */
async function updateManufacturerNames(): Promise<void> {
const configFiles = await enumFilesRecursive(
nodeFS,
processedDir,
(file) => file.endsWith(".json") && !file.endsWith("index.json"),
);
Expand Down
3 changes: 3 additions & 0 deletions packages/config/maintenance/lintConfigFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getLegalRangeForBitMask,
getMinimumShiftForBitMask,
} from "@zwave-js/core";
import { fs } from "@zwave-js/core/bindings/fs/node";
import { reportProblem } from "@zwave-js/maintenance";
import {
enumFilesRecursive,
Expand Down Expand Up @@ -262,6 +263,7 @@ async function lintDevices(): Promise<void> {
const rootDir = path.join(configDir, "devices");

const forbiddenFiles = await enumFilesRecursive(
fs,
rootDir,
(filename) => !filename.endsWith(".json"),
);
Expand All @@ -286,6 +288,7 @@ async function lintDevices(): Promise<void> {
let conditionalConfig: ConditionalDeviceConfig;
try {
conditionalConfig = await ConditionalDeviceConfig.from(
fs,
filePath,
true,
{
Expand Down
1 change: 1 addition & 0 deletions packages/config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"ansi-colors": "^4.1.3",
"json-logic-js": "^2.0.5",
"json5": "^2.2.3",
"pathe": "^1.1.2",
"semver": "^7.6.3",
"winston": "^3.15.0"
},
Expand Down
33 changes: 28 additions & 5 deletions packages/config/src/ConfigManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
isZWaveError,
} from "@zwave-js/core";
import { getErrorMessage, pathExists } from "@zwave-js/shared";
import path from "node:path";
import { type FileSystem } from "@zwave-js/shared/bindings";
import path from "pathe";
import { ConfigLogger } from "./Logger.js";
import {
type ManufacturersMap,
Expand All @@ -32,13 +33,15 @@ import {
} from "./utils.js";

export interface ConfigManagerOptions {
bindings?: FileSystem;
logContainer?: ZWaveLogContainer;
deviceConfigPriorityDir?: string;
deviceConfigExternalDir?: string;
}

export class ConfigManager {
public constructor(options: ConfigManagerOptions = {}) {
this._fs = options.bindings;
this.logger = new ConfigLogger(
options.logContainer ?? new ZWaveLogContainer({ enabled: false }),
);
Expand All @@ -48,6 +51,12 @@ export class ConfigManager {
this._configVersion = PACKAGE_VERSION;
}

private _fs: FileSystem | undefined;
private async getFS(): Promise<FileSystem> {
this._fs ??= (await import("@zwave-js/core/bindings/fs/node")).fs;
return this._fs;
}

private _configVersion: string;
public get configVersion(): string {
return this._configVersion;
Expand Down Expand Up @@ -88,6 +97,7 @@ export class ConfigManager {
const externalConfigDir = this.externalConfigDir;
if (externalConfigDir) {
syncResult = await syncExternalConfigDir(
await this.getFS(),
externalConfigDir,
this.logger,
);
Expand All @@ -112,6 +122,7 @@ export class ConfigManager {
public async loadManufacturers(): Promise<void> {
try {
this._manufacturers = await loadManufacturersInternal(
await this.getFS(),
this._useExternalConfig && this.externalConfigDir || undefined,
);
} catch (e) {
Expand Down Expand Up @@ -139,7 +150,10 @@ export class ConfigManager {
);
}

await saveManufacturersInternal(this._manufacturers);
await saveManufacturersInternal(
await this.getFS(),
this._manufacturers,
);
}

/**
Expand Down Expand Up @@ -177,18 +191,21 @@ export class ConfigManager {
}

public async loadDeviceIndex(): Promise<void> {
const fs = await this.getFS();
try {
// The index of config files included in this package
const embeddedIndex = await loadDeviceIndexInternal(
fs,
this.logger,
this._useExternalConfig && this.externalConfigDir || undefined,
);
// A dynamic index of the user-defined priority device config files
const priorityIndex: DeviceConfigIndex = [];
if (this.deviceConfigPriorityDir) {
if (await pathExists(this.deviceConfigPriorityDir)) {
if (await pathExists(fs, this.deviceConfigPriorityDir)) {
priorityIndex.push(
...(await generatePriorityDeviceIndex(
fs,
this.deviceConfigPriorityDir,
this.logger,
)),
Expand Down Expand Up @@ -230,7 +247,10 @@ export class ConfigManager {
}

public async loadFulltextDeviceIndex(): Promise<void> {
this.fulltextIndex = await loadFulltextDeviceIndexInternal(this.logger);
this.fulltextIndex = await loadFulltextDeviceIndexInternal(
await this.getFS(),
this.logger,
);
}

public getFulltextIndex(): FulltextDeviceConfigIndex | undefined {
Expand All @@ -254,6 +274,8 @@ export class ConfigManager {
// Load/regenerate the index if necessary
if (!this.index) await this.loadDeviceIndex();

const fs = await this.getFS();

// Look up the device in the index
const indexEntries = this.index!.filter(
getDeviceEntryPredicate(
Expand All @@ -274,7 +296,7 @@ export class ConfigManager {
const filePath = path.isAbsolute(indexEntry.filename)
? indexEntry.filename
: path.join(devicesDir, indexEntry.filename);
if (!(await pathExists(filePath))) return;
if (!(await pathExists(fs, filePath))) return;

// A config file is treated as am embedded one when it is located under the devices root dir
// or the external config dir
Expand All @@ -291,6 +313,7 @@ export class ConfigManager {

try {
return await ConditionalDeviceConfig.from(
fs,
filePath,
isEmbedded,
{ rootDir, fallbackDirs },
Expand Down
20 changes: 14 additions & 6 deletions packages/config/src/JsonTemplate.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core/safe";
import { pathExists } from "@zwave-js/shared";
import { pathExists, readTextFile } from "@zwave-js/shared";
import { type FileSystem } from "@zwave-js/shared/bindings";
import { getErrorMessage } from "@zwave-js/shared/safe";
import { isArray, isObject } from "alcalzone-shared/typeguards";
import JSON5 from "json5";
import fs from "node:fs/promises";
import * as path from "node:path";
import path from "pathe";

const IMPORT_KEY = "$import";
const importSpecifierRegex =
Expand All @@ -22,10 +22,11 @@ export function clearTemplateCache(): void {

/** Parses a JSON file with $import keys and replaces them with the selected objects */
export async function readJsonWithTemplate(
fs: FileSystem,
filename: string,
rootDirs?: string | string[],
): Promise<Record<string, unknown>> {
if (!(await pathExists(filename))) {
if (!(await pathExists(fs, filename))) {
throw new ZWaveError(
`Could not open config file ${filename}: not found!`,
ZWaveErrorCodes.Config_NotFound,
Expand All @@ -37,6 +38,7 @@ export async function readJsonWithTemplate(
// Try to use the cached versions of the template files to speed up the loading
const fileCache = new Map(templateCache);
const ret = await readJsonWithTemplateInternal(
fs,
filename,
undefined,
[],
Expand Down Expand Up @@ -127,6 +129,7 @@ function getImportStack(
}

async function readJsonWithTemplateInternal(
fs: FileSystem,
filename: string,
selector: string | undefined,
visited: string[],
Expand Down Expand Up @@ -172,7 +175,7 @@ ${getImportStack(visited, selector)}`,
json = fileCache.get(filename)!;
} else {
try {
const fileContent = await fs.readFile(filename, "utf8");
const fileContent = await readTextFile(fs, filename, "utf8");
json = JSON5.parse(fileContent);
fileCache.set(filename, json);
} catch (e) {
Expand All @@ -188,6 +191,7 @@ ${getImportStack(visited, selector)}`,
}
// Resolve the JSON imports for (a subset) of the file and return the compound file
return resolveJsonImports(
fs,
selector ? select(json, selector) : json,
filename,
[...visited, specifier],
Expand All @@ -198,6 +202,7 @@ ${getImportStack(visited, selector)}`,

/** Replaces all `$import` properties in a JSON object with object spreads of the referenced file/property */
async function resolveJsonImports(
fs: FileSystem,
json: Record<string, unknown>,
filename: string,
visited: string[],
Expand Down Expand Up @@ -225,7 +230,7 @@ async function resolveJsonImports(
rootDir,
importFilename.slice(2),
);
if (await pathExists(newFilename)) {
if (await pathExists(fs, newFilename)) {
break;
} else {
// Try the next
Expand Down Expand Up @@ -275,6 +280,7 @@ async function resolveJsonImports(

// const importFilename = path.join(path.dirname(filename), val);
const imported = await readJsonWithTemplateInternal(
fs,
newFilename,
selector,
visited,
Expand All @@ -285,6 +291,7 @@ async function resolveJsonImports(
} else if (isObject(val)) {
// We're looking at an object, recurse into it
ret[prop] = await resolveJsonImports(
fs,
val,
filename,
visited,
Expand All @@ -298,6 +305,7 @@ async function resolveJsonImports(
if (isObject(v)) {
vals.push(
await resolveJsonImports(
fs,
v,
filename,
visited,
Expand Down
20 changes: 14 additions & 6 deletions packages/config/src/Manufacturers.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
import { ZWaveError, ZWaveErrorCodes, isZWaveError } from "@zwave-js/core";
import { formatId, pathExists, stringify } from "@zwave-js/shared";
import {
formatId,
pathExists,
readTextFile,
stringify,
writeTextFile,
} from "@zwave-js/shared";
import { type FileSystem } from "@zwave-js/shared/bindings";
import { isObject } from "alcalzone-shared/typeguards";
import JSON5 from "json5";
import fs from "node:fs/promises";
import path from "node:path";
import path from "pathe";
import { configDir } from "./utils.js";
import { hexKeyRegex4Digits, throwInvalidConfig } from "./utils_safe.js";

export type ManufacturersMap = Map<number, string>;

/** @internal */
export async function loadManufacturersInternal(
fs: FileSystem,
externalConfigDir?: string,
): Promise<ManufacturersMap> {
const configPath = path.join(
externalConfigDir || configDir,
"manufacturers.json",
);

if (!(await pathExists(configPath))) {
if (!(await pathExists(fs, configPath))) {
throw new ZWaveError(
"The manufacturer config file does not exist!",
ZWaveErrorCodes.Config_Invalid,
);
}
try {
const fileContents = await fs.readFile(configPath, "utf8");
const fileContents = await readTextFile(fs, configPath, "utf8");
const definition = JSON5.parse(fileContents);
if (!isObject(definition)) {
throwInvalidConfig(
Expand Down Expand Up @@ -66,6 +73,7 @@ export async function loadManufacturersInternal(
* Write current manufacturers map to json
*/
export async function saveManufacturersInternal(
fs: FileSystem,
manufacturers: ManufacturersMap,
): Promise<void> {
const data: Record<string, string> = {};
Expand All @@ -79,5 +87,5 @@ export async function saveManufacturersInternal(
}

const configPath = path.join(configDir, "manufacturers.json");
await fs.writeFile(configPath, stringify(data, "\t") + "\n");
await writeTextFile(fs, configPath, stringify(data, "\t") + "\n");
}
Loading

0 comments on commit 449f354

Please sign in to comment.