Skip to content

Commit

Permalink
feat: add suggestions for incorrect ids
Browse files Browse the repository at this point in the history
Aligns the id validation of the UUID and Actions[0].UUID within the manifest to match the directory name (minus the .sdPlugin suffix)
  • Loading branch information
GeekyEggo committed Feb 19, 2024
1 parent c59fcc7 commit ad9bb4f
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 40 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"node": "^20.1.0"
},
"scripts": {
"build": "rollup --config rollup.config.ts --configPlugin typescript",
"build": "rm -rf ./dist && rollup --config rollup.config.ts --configPlugin typescript",
"watch": "rollup --config rollup.config.ts --configPlugin typescript --watch",
"lint": "eslint . --ext .ts --max-warnings 0",
"lint:fix": "prettier --config .prettierrc \"./src/**/*.ts\" --write",
Expand Down
4 changes: 2 additions & 2 deletions src/validation/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { createContext, type PluginContext } from "./plugin";
import { layoutItemsAreWithinBoundsAndNoOverlap } from "./rules/layout-item-bounds";
import { layoutItemKeysAreUnique } from "./rules/layout-item-keys";
import { layoutsExistAndSchemasAreValid } from "./rules/layout-schema";
import { actionUuidIsUniqueAndPrefixed } from "./rules/manifest-action-uuids";
import { categoryMatchesName } from "./rules/manifest-category";
import { manifestFilesExist } from "./rules/manifest-files-exist";
import { manifestExistsAndSchemaIsValid } from "./rules/manifest-schema";
import { manifestUrlsExist } from "./rules/manifest-urls-exist";
import { manifestUuids } from "./rules/manifest-uuids";
import { pathIsDirectoryAndUuid } from "./rules/path-input";

/**
Expand All @@ -21,8 +21,8 @@ export function validatePlugin(path: string): Promise<ValidationResult> {
pathIsDirectoryAndUuid,
manifestExistsAndSchemaIsValid,
manifestFilesExist,
manifestUuids,
manifestUrlsExist,
actionUuidIsUniqueAndPrefixed,
categoryMatchesName,
layoutsExistAndSchemasAreValid,
layoutItemKeysAreUnique,
Expand Down
16 changes: 11 additions & 5 deletions src/validation/plugin/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createRequire } from "node:module";
import { basename, dirname, join, resolve } from "node:path";
import { JsonLocation, LocationRef } from "../../common/location";
import { JsonFileContext, JsonSchema } from "../../json";
import { isPredefinedLayoutLike } from "../../stream-deck";
import { isPredefinedLayoutLike, isValidPluginId } from "../../stream-deck";

const r = createRequire(import.meta.url);
const layoutSchema = r("@elgato/schemas/streamdeck/plugins/layout.json");
Expand All @@ -20,11 +20,12 @@ export const directorySuffix = ".sdPlugin";
* @returns Plugin context.
*/
export function createContext(path: string): PluginContext {
const uuid = basename(path).replace(/\.sdPlugin$/, "");
const id = basename(path).replace(/\.sdPlugin$/, "");

return {
uuid,
manifest: new ManifestJsonFileContext(join(path, "manifest.json"))
hasValidId: isValidPluginId(id),
manifest: new ManifestJsonFileContext(join(path, "manifest.json")),
id
};
}

Expand Down Expand Up @@ -71,6 +72,11 @@ export type LayoutFile = LocationRef<JsonLocation> & {
* Provides information about the plugin.
*/
export type PluginContext = {
/**
* Gets a value indicating whether the {@link id} is valid.
*/
readonly hasValidId: boolean;

/**
* Manifest associated with the plugin.
*/
Expand All @@ -79,5 +85,5 @@ export type PluginContext = {
/**
* Unique identifier parsed from the path to the plugin.
*/
readonly uuid: string;
readonly id: string;
};
27 changes: 0 additions & 27 deletions src/validation/plugin/rules/manifest-action-uuids.ts

This file was deleted.

17 changes: 17 additions & 0 deletions src/validation/plugin/rules/manifest-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,28 @@ import { existsSync } from "node:fs";
* Validates the JSON schema of the manifest; this validation fulfils the same role as if the JSON were validated in an IDE, i.e. custom keyword validation is not applied.
*/
export const manifestExistsAndSchemaIsValid = rule<PluginContext>(function (plugin: PluginContext) {
// Do nothing if the specified path (of the plugin) does not exist.
if (!existsSync(this.path)) {
return;
}

// Check if the manifest exists.
if (!existsSync(plugin.manifest.path)) {
this.addError(plugin.manifest.path, "Manifest not found");
}

plugin.manifest.errors.forEach(({ message, location }) => {
// When the directory name is a valid identifier, but the UUID is not specified in the manifest, show a suggestion.
if (plugin.hasValidId && location?.instancePath === "" && message === "must contain property: UUID") {
this.addError(plugin.manifest.path, message, {
location,
suggestion: `Expected: ${plugin.id}`
});

return;
}

// Otherwise add the error.
this.addError(plugin.manifest.path, message, { location });
});
});
38 changes: 38 additions & 0 deletions src/validation/plugin/rules/manifest-uuids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { colorize } from "../../../common/stdout";
import { rule } from "../../rule";
import { type PluginContext } from "../plugin";

/**
* Validates the unique identifiers within the manifest, ensuring the `UUID` matches the parent directory, and the actions' `UUID` are unique and are prefixed with the plugin's `UUID`.
*/
export const manifestUuids = rule<PluginContext>(async function (plugin: PluginContext) {
const { value: manifest } = plugin.manifest;

// When the directory name is a valid identifier, check it matches the identifier in the manifest.
if (plugin.hasValidId && manifest.UUID?.value !== undefined && plugin.id !== manifest.UUID.value) {
this.addError(plugin.manifest.path, "must match parent directory name", {
location: manifest.UUID.location,
suggestion: `Expected: ${plugin.id}`
});
}

// Check all of the action identifiers.
const uuids = new Set<string>();
manifest.Actions?.forEach(({ UUID: uuid }) => {
if (uuid?.value === undefined) {
return;
}

// Validate the action identifier is unique.
if (uuids.has(uuid.value)) {
this.addError(plugin.manifest.path, "must be unique", uuid);
} else {
uuids.add(uuid.value);
}

// Check if the action identifier is prefixed with the plugin identifier.
if (plugin.hasValidId && !uuid.value.startsWith(plugin.id)) {
this.addWarning(plugin.manifest.path, `should be prefixed with ${colorize(plugin.id)}`, uuid);
}
});
});
19 changes: 14 additions & 5 deletions src/validation/plugin/rules/path-input.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import { existsSync, lstatSync } from "node:fs";
import { basename } from "node:path";
import { colorize } from "../../../common/stdout";
import { isValidPluginId } from "../../../stream-deck";
import { rule } from "../../rule";
import { directorySuffix, type PluginContext } from "../plugin";

export const pathIsDirectoryAndUuid = rule<PluginContext>(function (plugin: PluginContext) {
const name = basename(this.path);

// Path exists.
if (!existsSync(this.path)) {
this.addError(this.path, "Path does not exist");
} else if (!lstatSync(this.path).isDirectory()) {
this.addError(this.path, "Directory not found");
return;
}

// Path is a directory.
if (!lstatSync(this.path).isDirectory()) {
this.addError(this.path, "Path must be a directory");
return;
}

// Directory name suffix.
if (!name.endsWith(directorySuffix)) {
this.addError(this.path, "Name must be suffixed with '.sdPlugin'");
this.addError(this.path, `Name must be suffixed with ${colorize(".sdPlugin")}`);
}

if (!isValidPluginId(plugin.uuid)) {
// Directory name is a valid identifier.
if (!isValidPluginId(plugin.id)) {
this.addError(this.path, "Name must be in reverse DNS format, and must only contain lowercase alphanumeric characters (a-z, 0-9), hyphens (-), and periods (.)", {
suggestion: "Example: 'com.elgato.wave-link'"
suggestion: "Example: com.elgato.wave-link"
});
}
});

0 comments on commit ad9bb4f

Please sign in to comment.