From d6ccfcfa22df4e2457873970c015656bbf5e064b Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Fri, 3 May 2024 17:41:14 +0100 Subject: [PATCH 1/6] feat: enable validation functionality via importing --- .gitignore | 1 + package-lock.json | 45 +++--- package.json | 8 +- rollup.config.ts | 141 +++++++++++++----- src/{index.ts => cli.ts} | 0 src/common/ordered-array.ts | 14 +- src/main.ts | 13 ++ src/validation/entry.ts | 24 +-- .../{entry-collection.ts => file-result.ts} | 34 ++--- src/validation/index.ts | 5 + src/validation/result.ts | 19 +-- 11 files changed, 200 insertions(+), 104 deletions(-) rename src/{index.ts => cli.ts} (100%) create mode 100644 src/main.ts rename src/validation/{entry-collection.ts => file-result.ts} (58%) create mode 100644 src/validation/index.ts diff --git a/.gitignore b/.gitignore index 3c1a7f6..609d105 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules/ # Build and temporary files bin/ +dist/ /.tmp/ # CLI diff --git a/package-lock.json b/package-lock.json index e684d05..7114bc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "log-symbols": "^5.1.0", "rage-edit": "^1.2.0", "rollup": "^4.0.2", + "rollup-plugin-dts": "^6.1.0", "semver": "^7.6.0", "tar": "^7.0.1", "tslib": "^2.6.2", @@ -95,7 +96,6 @@ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, - "peer": true, "dependencies": { "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" @@ -109,7 +109,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, - "peer": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -122,7 +121,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, - "peer": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -137,7 +135,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, - "peer": true, "dependencies": { "color-name": "1.1.3" } @@ -146,15 +143,13 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, - "peer": true, "engines": { "node": ">=0.8.0" } @@ -284,7 +279,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, - "peer": true, "engines": { "node": ">=6.9.0" } @@ -294,7 +288,6 @@ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -309,7 +302,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, - "peer": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -322,7 +314,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, - "peer": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -337,7 +328,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, - "peer": true, "dependencies": { "color-name": "1.1.3" } @@ -346,15 +336,13 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, - "peer": true, "engines": { "node": ">=0.8.0" } @@ -2819,7 +2807,6 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, - "peer": true, "engines": { "node": ">=4" } @@ -3206,8 +3193,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/js-yaml": { "version": "4.1.0", @@ -4028,6 +4014,28 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-dts": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.1.0.tgz", + "integrity": "sha512-ijSCPICkRMDKDLBK9torss07+8dl9UpY9z1N/zTeA1cIqdzMlpkV3MOOC7zukyvQfDyxa1s3Dl2+DeiP/G6DOw==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.4" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/Swatinem" + }, + "optionalDependencies": { + "@babel/code-frame": "^7.22.13" + }, + "peerDependencies": { + "rollup": "^3.29.4 || ^4", + "typescript": "^4.5 || ^5.0" + } + }, "node_modules/run-async": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", @@ -4337,7 +4345,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, - "peer": true, "dependencies": { "has-flag": "^3.0.0" }, diff --git a/package.json b/package.json index b39a27a..da7194a 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,12 @@ "streamdeck": "bin/streamdeck.mjs", "sd": "bin/streamdeck.mjs" }, + "main": "./dist/index.js", "files": [ - "bin/streamdeck.mjs", - "template" + "./bin/streamdeck.mjs", + "./dist/*.js", + "./dist/*.d.ts", + "./template" ], "type": "module", "engines": { @@ -75,6 +78,7 @@ "log-symbols": "^5.1.0", "rage-edit": "^1.2.0", "rollup": "^4.0.2", + "rollup-plugin-dts": "^6.1.0", "semver": "^7.6.0", "tar": "^7.0.1", "tslib": "^2.6.2", diff --git a/rollup.config.ts b/rollup.config.ts index 31cb967..23101cd 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -3,45 +3,116 @@ import json from "@rollup/plugin-json"; import nodeResolve from "@rollup/plugin-node-resolve"; import terser from "@rollup/plugin-terser"; import typescript from "@rollup/plugin-typescript"; -import path from "node:path"; +import path, { extname } from "node:path"; import url from "node:url"; import { RollupOptions } from "rollup"; +import dts from "rollup-plugin-dts"; const isWatching = !!process.env.ROLLUP_WATCH; -const config: RollupOptions = { - input: "src/index.ts", - output: { - banner: "#!/usr/bin/env node", - file: "bin/streamdeck.mjs", - sourcemap: isWatching, - sourcemapPathTransform: (relativeSourcePath: string, sourcemapPath: string): string => { - return url.pathToFileURL(path.resolve(path.dirname(sourcemapPath), relativeSourcePath)).href; - } - }, - external: [ - /* Ignore @elgato/schema to enable auto-update. */ - "@elgato/schemas", - "@elgato/schemas/streamdeck/plugins/", - "@elgato/schemas/streamdeck/plugins/layout.json", - "@elgato/schemas/streamdeck/plugins/manifest.json" - ], - plugins: [ - typescript(), - json(), - commonjs(), - nodeResolve({ - browser: false, - exportConditions: ["node"], - preferBuiltins: true - }), - !isWatching && - terser({ - format: { - comments: false - } - }) - ] +/** + * Ignore @elgato/schema to enable auto-update. + */ +const external = [ + "@elgato/schemas", + "@elgato/schemas/streamdeck/plugins/", + "@elgato/schemas/streamdeck/plugins/layout.json", + "@elgato/schemas/streamdeck/plugins/manifest.json", +]; + +/** + * Gets the {@link RollupOptions} for the specified options. + * @param opts Options to convert to {@link RollupOptions}. + * @returns The {@link RollupOptions}. + */ +function getOptions(opts: Options): RollupOptions[] { + const { input, output: file, banner, declarations } = opts; + + /** + * TypeScript compiler options. + */ + const rollupOpts: RollupOptions[] = [ + { + input, + output: { + banner, + file, + sourcemap: isWatching, + sourcemapPathTransform: (relativeSourcePath: string, sourcemapPath: string): string => { + return url.pathToFileURL(path.resolve(path.dirname(sourcemapPath), relativeSourcePath)).href; + }, + }, + external, + plugins: [ + typescript(), + json(), + commonjs(), + nodeResolve({ + browser: false, + exportConditions: ["node"], + preferBuiltins: true, + }), + !isWatching && + terser({ + format: { + comments: false, + }, + }), + ], + }, + ]; + + /** + * TypeScript declaration options. + */ + if (declarations) { + rollupOpts.push({ + input, + output: { + file: `${file.slice(0, extname(file).length * -1)}.d.ts`, + }, + external, + plugins: [dts()], + }); + } + + return rollupOpts; +} + +/** + * Minimal required fields that represent {@link RollupOptions}. + */ +type Options = { + /** + * Input file path. + */ + input: string; + + /** + * Output file path. + */ + output: string; + + /** + * Optional banner to prefix to the output contents. + */ + banner?: string; + + /** + * Determines whether to output declarations. + */ + declarations?: boolean; }; -export default config; +export default [ + ...getOptions({ + input: "src/cli.ts", + output: "bin/streamdeck.mjs", + banner: "#!/usr/bin/env node", + }), + ...getOptions({ + input: "src/main.ts", + output: "dist/index.js", + declarations: true, + }), +]; diff --git a/src/index.ts b/src/cli.ts similarity index 100% rename from src/index.ts rename to src/cli.ts diff --git a/src/common/ordered-array.ts b/src/common/ordered-array.ts index 0c26d42..5884a45 100644 --- a/src/common/ordered-array.ts +++ b/src/common/ordered-array.ts @@ -5,7 +5,7 @@ export class OrderedArray extends Array { /** * Delegates responsible for determining the sort order. */ - private readonly compareOn: ((value: T) => number | string)[]; + readonly #compareOn: ((value: T) => number | string)[]; /** * Initializes a new instance of the {@link OrderedArray} class. @@ -13,7 +13,7 @@ export class OrderedArray extends Array { */ constructor(...compareOn: ((value: T) => number | string)[]) { super(); - this.compareOn = compareOn; + this.#compareOn = compareOn; } /** @@ -22,7 +22,7 @@ export class OrderedArray extends Array { * @returns New length of the array. */ public push(value: T): number { - super.splice(this.sortedIndex(value), 0, value); + super.splice(this.#sortedIndex(value), 0, value); return this.length; } @@ -32,8 +32,8 @@ export class OrderedArray extends Array { * @param b Item B. * @returns `-1` when {@link a} is less than {@link b}, `1` when {@link a} is greater than {@link b}, otherwise `0` */ - private compare(a: T, b: T): number { - for (const compareOn of this.compareOn) { + #compare(a: T, b: T): number { + for (const compareOn of this.#compareOn) { const x = compareOn(a); const y = compareOn(b); @@ -53,13 +53,13 @@ export class OrderedArray extends Array { * @param value The value. * @returns Index. */ - private sortedIndex(value: T): number { + #sortedIndex(value: T): number { let low = 0; let high = this.length; while (low < high) { const mid = (low + high) >>> 1; - const comparison = this.compare(value, this[mid]); + const comparison = this.#compare(value, this[mid]); if (comparison === 0) { return mid; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..09d98cc --- /dev/null +++ b/src/main.ts @@ -0,0 +1,13 @@ +import chalk from "chalk"; + +export { + ValidationLevel, + type FileValidationResult, + type ValidationEntry, + type ValidationEntryDetails, + type ValidationResult, +} from "./validation"; + +export { validatePlugin } from "./validation/plugin"; + +chalk.level = 0; diff --git a/src/validation/entry.ts b/src/validation/entry.ts index 9af0446..e517bbf 100644 --- a/src/validation/entry.ts +++ b/src/validation/entry.ts @@ -27,32 +27,32 @@ export class ValidationEntry { this.message = message.slice(0, -1); } + // Determine the location string based on how much location information is provided. if (this.details?.location?.column || this.details?.location?.line) { this.location = `${this.details.location.line}`; if (this.details.location.column) { this.location += `:${this.details.location.column}`; } } + + // Prepend the location's key to the message if there is one; this is typically the JSON property name, for example "CodePath", "Author", etc. + if (this.details?.location?.key) { + this.message = `${chalk.cyan(this.details.location.key)} ${message}`; + } } /** - * Converts the entry to a string. - * @param padding Padding required to align the position of each entry. + * Converts the entry to a summary string. + * @param padding Optional padding required to align the position of each entry. * @returns String that represents the entry. */ - public toString(padding: number): string { + public toSummary(padding?: number): string { // Apply additional padding to the position so entries without position aren't misaligned. - const position = padding === 0 ? "" : `${this.location.padEnd(padding + 2)}`; + const position = padding === undefined || padding === 0 ? "" : `${this.location.padEnd(padding + 2)}`; const level = ValidationLevel[this.level].padEnd(7); - // Construct the base message - let message = this.message; - if (this.details?.location?.key) { - message = `${chalk.cyan(this.details.location.key)} ${message}`; - } - - // Prepend the position and level. - message = ` ${chalk.dim(position)}${this.level === ValidationLevel.error ? chalk.red(level) : chalk.yellow(level)} ${message}`; + // Prepend the position and level to the message. + let message = ` ${chalk.dim(position)}${this.level === ValidationLevel.error ? chalk.red(level) : chalk.yellow(level)} ${this.message}`; // Attach the suggestion; we prefix a hidden position so that errors are clickable within supported terminals (for example, VSCode). if (this.details?.suggestion) { diff --git a/src/validation/entry-collection.ts b/src/validation/file-result.ts similarity index 58% rename from src/validation/entry-collection.ts rename to src/validation/file-result.ts index 1453442..1289069 100644 --- a/src/validation/entry-collection.ts +++ b/src/validation/file-result.ts @@ -5,37 +5,35 @@ import { StdOut } from "../common/stdout"; import type { ValidationEntry } from "./entry"; /** - * Collection of {@link ValidationEntry}. + * Provides validation results for a specific file path. */ -export class ValidationEntryCollection { - /** - * Entries within this collection. - */ - private entries = new OrderedArray( - (x) => x.level, - (x) => x.details?.location?.line ?? Infinity, - (x) => x.details?.location?.column ?? Infinity, - (x) => x.message, - ); - +export class FileValidationResult extends OrderedArray { /** * Tracks the padding required for the location of a validation entry, i.e. the text before the entry level. */ private padding = 0; /** - * Initializes a new instance of the {@link ValidationEntryCollection} class. + * Initializes a new instance of the {@link FileValidationResult} class. * @param path Path that groups the entries together. */ - constructor(public readonly path: string) {} + constructor(public readonly path: string) { + super( + (x) => x.level, + (x) => x.details?.location?.line ?? Infinity, + (x) => x.details?.location?.column ?? Infinity, + (x) => x.message, + ); + } /** * Adds the specified {@link entry} to the collection. * @param entry Entry to add. + * @returns New length of the validation results. */ - public add(entry: ValidationEntry): void { + public push(entry: ValidationEntry): number { this.padding = Math.max(this.padding, entry.location.length); - this.entries.push(entry); + return super.push(entry); } /** @@ -43,7 +41,7 @@ export class ValidationEntryCollection { * @param output Output to write to. */ public writeTo(output: StdOut): void { - if (this.entries.length === 0) { + if (this.length === 0) { return; } @@ -54,7 +52,7 @@ export class ValidationEntryCollection { output.log(); } - this.entries.forEach((entry) => output.log(entry.toString(this.padding))); + this.forEach((entry) => output.log(entry.toSummary(this.padding))); output.log(); } } diff --git a/src/validation/index.ts b/src/validation/index.ts new file mode 100644 index 0000000..e5add25 --- /dev/null +++ b/src/validation/index.ts @@ -0,0 +1,5 @@ +export * from "./entry"; +export * from "./file-result"; +export * from "./result"; +export * from "./rule"; +export * from "./validator"; diff --git a/src/validation/result.ts b/src/validation/result.ts index 094a790..a6e4894 100644 --- a/src/validation/result.ts +++ b/src/validation/result.ts @@ -1,14 +1,11 @@ import { StdOut } from "../common/stdout"; import { type ValidationEntry, ValidationLevel } from "./entry"; -import { ValidationEntryCollection } from "./entry-collection"; +import { FileValidationResult } from "./file-result"; /** - * Validation result containing a collection of {@link ValidationEntryCollection} grouped by the directory or file path they're associated with. + * Validation result containing a collection of {@link FileValidationResult} grouped by the directory or file path they're associated with. */ -export class ValidationResult - extends Array - implements ReadonlyArray -{ +export class ValidationResult extends Array implements ReadonlyArray { /** * Private backing field for {@link Result.errorCount}. */ @@ -31,13 +28,13 @@ export class ValidationResult this.warningCount++; } - let collection = this.find((c) => c.path === path); - if (collection === undefined) { - collection = new ValidationEntryCollection(path); - this.push(collection); + let fileResult = this.find((c) => c.path === path); + if (fileResult === undefined) { + fileResult = new FileValidationResult(path); + this.push(fileResult); } - collection.add(entry); + fileResult.push(entry); } /** From 6c343c92f1014e655b6c5f2f1f9d214954b411c6 Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Fri, 3 May 2024 17:46:53 +0100 Subject: [PATCH 2/6] refactor: rename export --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 09d98cc..c645ff0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,6 @@ export { type ValidationResult, } from "./validation"; -export { validatePlugin } from "./validation/plugin"; +export { validatePlugin as validateStreamDeckPlugin } from "./validation/plugin"; chalk.level = 0; From 6e88a24c54cb63892da02da3f0f79ec722fb4145 Mon Sep 17 00:00:00 2001 From: Andrew Story Date: Thu, 30 May 2024 17:19:20 -0500 Subject: [PATCH 3/6] build: add static schema build option --- package.json | 1 + rollup.config.ts | 3 ++ scripts/inlineRequires.ts | 96 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 scripts/inlineRequires.ts diff --git a/package.json b/package.json index da7194a..bafa74a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "scripts": { "build": "rm -rf ./dist && rollup --config rollup.config.ts --configPlugin typescript", + "build-static-schema": "rm -rf ./dist && rollup --config rollup.config.ts --configPlugin typescript --environment INLINE_REQUIRES", "watch": "rollup --config rollup.config.ts --configPlugin typescript --watch", "lint": "eslint . --ext .ts --max-warnings 0", "lint:fix": "prettier \"./src/**/*.ts\" --write", diff --git a/rollup.config.ts b/rollup.config.ts index 23101cd..4887039 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -7,8 +7,10 @@ import path, { extname } from "node:path"; import url from "node:url"; import { RollupOptions } from "rollup"; import dts from "rollup-plugin-dts"; +import {inlineRequires} from "./scripts/inlineRequires" const isWatching = !!process.env.ROLLUP_WATCH; +const shouldInlineRequires = !!process.env.INLINE_REQUIRES; /** * Ignore @elgato/schema to enable auto-update. @@ -58,6 +60,7 @@ function getOptions(opts: Options): RollupOptions[] { comments: false, }, }), + shouldInlineRequires && inlineRequires(), ], }, ]; diff --git a/scripts/inlineRequires.ts b/scripts/inlineRequires.ts new file mode 100644 index 0000000..b760f46 --- /dev/null +++ b/scripts/inlineRequires.ts @@ -0,0 +1,96 @@ +// Statically inline all uses of createRequire +// This is necessary for use in environments where the node_modules directory isn't available at run time. +// +// This script makes a few assumptions about the use of createRequire, such as: +// * createRequire is imported via named import +// * The creation of a createRequire function is done on a single line +// * Use of such functions is done only once on any given line and on a single line + +import * as fs from 'node:fs'; +import * as nodePath from 'node:path'; +import { createFilter } from '@rollup/pluginutils'; +import { RollupTypescriptPluginOptions } from '@rollup/plugin-typescript'; +import { Plugin } from 'rollup'; + +function processTS(code : string, nodeDirPath : string){ + let modified = false; + const lines = code.replace("\r", "").split("\n"); + + let result = ""; + const requireRegex : RegExp[] = []; + + for(const rawLine of lines){ + let handled = false; + const line = rawLine.trim(); + + // Detect import of createRequire from node:module + let matches = line.match(/^import\s*{([\s]*|[^}]*,\s*)createRequire(\s*|,[^}]*)}\s*from\s+['"]node:module['"]\s*;$/); + if(matches && matches.length >= 3){ + // Splice out the createRequire + let begin = matches[1].trim(); + let end = matches[2].trim(); + // If the length is more than zero, we definitely have a trailing comma + if(begin.length > 0){ + begin = begin.substring(0, begin.length - 1); + } + // If the length is more than zero, we definitely have a leading comma + if(end.length > 0){ + end = end.substring(1); + } + const comma = begin.length > 0 && end.length > 0 ? ", ": ""; + const imports = begin + comma + end; + if(imports.length > 0){ + result += `import { ${imports} } from "node:module";\n`; + } + handled = true; + } + + // Detect invocation of createRequire + matches = line.match(/^(const|let|var)\s+([^\s=]+)\s*=\s*createRequire\s*\(.*/); + if(matches && matches.length >= 3){ + // Create a regex that detects use of a createRequire function + requireRegex.push(new RegExp(`(.*[^a-zA-Z])${matches[2].trim()}\\(['"]([^)]+)['"]\\)(.*)`)); + handled = true; + } + + // Check against all require functions discovered so far + for( const requireCheck of requireRegex){ + matches = requireCheck.exec(line); + if( matches && matches.length >= 4){ + // Splice in the fule + const fileData = fs.readFileSync(nodePath.join(nodeDirPath, matches[2])); + + result += matches[1] + fileData.toString() + matches[3] + "\n"; + handled = true; + break; + } + } + + if(!handled){ + result += rawLine + "\n"; + } + else { + modified = true; + } + } + + if(modified){ + return result; + } + return code; +} + + + +export function inlineRequires(options : RollupTypescriptPluginOptions = {}) : Plugin { + const filter = createFilter(options.include, options.exclude); + + return { + name: 'inline-require', + transform(code : string, id : string) : string | undefined { + if (!filter(id)) return; + + return processTS(code, "node_modules"); + } + }; +} From 4b59ad2e67555f292b74bca69491577e4defdccf Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Wed, 5 Jun 2024 18:02:25 +0100 Subject: [PATCH 4/6] refactor: import JSON schemas as objects --- package-lock.json | 8 +-- package.json | 2 +- rollup.config.ts | 6 +-- scripts/inlineRequires.ts | 96 --------------------------------- src/validation/plugin/index.ts | 5 +- src/validation/plugin/plugin.ts | 17 +++--- 6 files changed, 18 insertions(+), 116 deletions(-) delete mode 100644 scripts/inlineRequires.ts diff --git a/package-lock.json b/package-lock.json index 7114bc4..23468d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.3.0", "license": "MIT", "dependencies": { - "@elgato/schemas": "^0.3.0" + "@elgato/schemas": "^0.3.5" }, "bin": { "sd": "bin/streamdeck.mjs", @@ -469,9 +469,9 @@ } }, "node_modules/@elgato/schemas": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@elgato/schemas/-/schemas-0.3.0.tgz", - "integrity": "sha512-5Ckn9M7LYEmNgw48WKAQYygt6o1rr38wqY0pCFlu+RWMd9ntCl6u1MpzJoxvA0YlT0l/dqTsp3cbQ4iBrg5RzA==" + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@elgato/schemas/-/schemas-0.3.5.tgz", + "integrity": "sha512-6o2eiuKI6TVah50t3CvJVRoh0wPhldvNXo/S5zu5cwowKktC+5sQ5PSuVPpEAbJ6uyKEiLF5pQ1dM3PrNm/F+A==" }, "node_modules/@es-joy/jsdoccomment": { "version": "0.41.0", diff --git a/package.json b/package.json index bafa74a..1b30dff 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,6 @@ "typescript": "^5.2.2" }, "dependencies": { - "@elgato/schemas": "^0.3.0" + "@elgato/schemas": "^0.3.5" } } diff --git a/rollup.config.ts b/rollup.config.ts index 4887039..74eb5a8 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -7,10 +7,8 @@ import path, { extname } from "node:path"; import url from "node:url"; import { RollupOptions } from "rollup"; import dts from "rollup-plugin-dts"; -import {inlineRequires} from "./scripts/inlineRequires" const isWatching = !!process.env.ROLLUP_WATCH; -const shouldInlineRequires = !!process.env.INLINE_REQUIRES; /** * Ignore @elgato/schema to enable auto-update. @@ -18,8 +16,7 @@ const shouldInlineRequires = !!process.env.INLINE_REQUIRES; const external = [ "@elgato/schemas", "@elgato/schemas/streamdeck/plugins/", - "@elgato/schemas/streamdeck/plugins/layout.json", - "@elgato/schemas/streamdeck/plugins/manifest.json", + "@elgato/schemas/streamdeck/plugins/json", ]; /** @@ -60,7 +57,6 @@ function getOptions(opts: Options): RollupOptions[] { comments: false, }, }), - shouldInlineRequires && inlineRequires(), ], }, ]; diff --git a/scripts/inlineRequires.ts b/scripts/inlineRequires.ts deleted file mode 100644 index b760f46..0000000 --- a/scripts/inlineRequires.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Statically inline all uses of createRequire -// This is necessary for use in environments where the node_modules directory isn't available at run time. -// -// This script makes a few assumptions about the use of createRequire, such as: -// * createRequire is imported via named import -// * The creation of a createRequire function is done on a single line -// * Use of such functions is done only once on any given line and on a single line - -import * as fs from 'node:fs'; -import * as nodePath from 'node:path'; -import { createFilter } from '@rollup/pluginutils'; -import { RollupTypescriptPluginOptions } from '@rollup/plugin-typescript'; -import { Plugin } from 'rollup'; - -function processTS(code : string, nodeDirPath : string){ - let modified = false; - const lines = code.replace("\r", "").split("\n"); - - let result = ""; - const requireRegex : RegExp[] = []; - - for(const rawLine of lines){ - let handled = false; - const line = rawLine.trim(); - - // Detect import of createRequire from node:module - let matches = line.match(/^import\s*{([\s]*|[^}]*,\s*)createRequire(\s*|,[^}]*)}\s*from\s+['"]node:module['"]\s*;$/); - if(matches && matches.length >= 3){ - // Splice out the createRequire - let begin = matches[1].trim(); - let end = matches[2].trim(); - // If the length is more than zero, we definitely have a trailing comma - if(begin.length > 0){ - begin = begin.substring(0, begin.length - 1); - } - // If the length is more than zero, we definitely have a leading comma - if(end.length > 0){ - end = end.substring(1); - } - const comma = begin.length > 0 && end.length > 0 ? ", ": ""; - const imports = begin + comma + end; - if(imports.length > 0){ - result += `import { ${imports} } from "node:module";\n`; - } - handled = true; - } - - // Detect invocation of createRequire - matches = line.match(/^(const|let|var)\s+([^\s=]+)\s*=\s*createRequire\s*\(.*/); - if(matches && matches.length >= 3){ - // Create a regex that detects use of a createRequire function - requireRegex.push(new RegExp(`(.*[^a-zA-Z])${matches[2].trim()}\\(['"]([^)]+)['"]\\)(.*)`)); - handled = true; - } - - // Check against all require functions discovered so far - for( const requireCheck of requireRegex){ - matches = requireCheck.exec(line); - if( matches && matches.length >= 4){ - // Splice in the fule - const fileData = fs.readFileSync(nodePath.join(nodeDirPath, matches[2])); - - result += matches[1] + fileData.toString() + matches[3] + "\n"; - handled = true; - break; - } - } - - if(!handled){ - result += rawLine + "\n"; - } - else { - modified = true; - } - } - - if(modified){ - return result; - } - return code; -} - - - -export function inlineRequires(options : RollupTypescriptPluginOptions = {}) : Plugin { - const filter = createFilter(options.include, options.exclude); - - return { - name: 'inline-require', - transform(code : string, id : string) : string | undefined { - if (!filter(id)) return; - - return processTS(code, "node_modules"); - } - }; -} diff --git a/src/validation/plugin/index.ts b/src/validation/plugin/index.ts index 87f59a0..ba1c0ea 100644 --- a/src/validation/plugin/index.ts +++ b/src/validation/plugin/index.ts @@ -16,8 +16,9 @@ import { pathIsDirectoryAndUuid } from "./rules/path-input"; * @param path Path to the plugin. * @returns The validation result. */ -export function validatePlugin(path: string): Promise { - return validate(path, createContext(path), [ +export async function validatePlugin(path: string): Promise { + const ctx = await createContext(path); + return validate(path, ctx, [ pathIsDirectoryAndUuid, manifestExistsAndSchemaIsValid, manifestFilesExist, diff --git a/src/validation/plugin/plugin.ts b/src/validation/plugin/plugin.ts index 7b88b2b..2cfbef9 100644 --- a/src/validation/plugin/plugin.ts +++ b/src/validation/plugin/plugin.ts @@ -1,13 +1,11 @@ import type { Layout, Manifest } from "@elgato/schemas/streamdeck/plugins"; -import { createRequire } from "node:module"; +import type { AnySchema } from "ajv"; import { basename, dirname, join, resolve } from "node:path"; import { JsonLocation, LocationRef } from "../../common/location"; import { JsonFileContext, JsonSchema } from "../../json"; import { isPredefinedLayoutLike, isValidPluginId } from "../../stream-deck"; -const nodeRequire = createRequire(import.meta.url); - /** * Suffixed associated with a plugin directory. */ @@ -18,12 +16,13 @@ export const directorySuffix = ".sdPlugin"; * @param path Plugin directory. * @returns Plugin context. */ -export function createContext(path: string): PluginContext { +export async function createContext(path: string): Promise { const id = basename(path).replace(/\.sdPlugin$/, ""); + const { manifest, layout } = await import("@elgato/schemas/streamdeck/plugins/json"); return { hasValidId: isValidPluginId(id), - manifest: new ManifestJsonFileContext(join(path, "manifest.json")), + manifest: new ManifestJsonFileContext(join(path, "manifest.json"), manifest, layout), id, }; } @@ -40,11 +39,13 @@ class ManifestJsonFileContext extends JsonFileContext { /** * Initializes a new instance of the {@link ManifestJsonFileContext} class. * @param path Path to the manifest file. + * @param manifestSchema JSON schema that defines the manifest. + * @param layoutSchema JSON schema that defines a layout. */ - constructor(path: string) { - super(path, new JsonSchema(nodeRequire("@elgato/schemas/streamdeck/plugins/manifest.json"))); + constructor(path: string, manifestSchema: AnySchema, layoutSchema: AnySchema) { + super(path, new JsonSchema(manifestSchema)); - const compiledLayoutSchema = new JsonSchema(nodeRequire("@elgato/schemas/streamdeck/plugins/layout.json")); + const compiledLayoutSchema = new JsonSchema(layoutSchema); this.value.Actions?.forEach((action) => { if (action.Encoder?.layout !== undefined && !isPredefinedLayoutLike(action.Encoder?.layout.value)) { const filePath = resolve(dirname(path), action.Encoder.layout.value); From 6c12bc17ba15c14a3fb60846373bb80114b5feb0 Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Tue, 25 Jun 2024 18:56:58 +0100 Subject: [PATCH 5/6] chore: remove unused script, and add 0.3.1 changelog --- CHANGELOG.md | 6 ++++++ package.json | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f98e1..bb9d753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ # Change Log +## 0.3.1 + +### ✨ New + +- Enable validation of Stream Deck plugins programmatically. + ## 0.3.0 ### ✨ New diff --git a/package.json b/package.json index 1b30dff..bb4375f 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ }, "scripts": { "build": "rm -rf ./dist && rollup --config rollup.config.ts --configPlugin typescript", - "build-static-schema": "rm -rf ./dist && rollup --config rollup.config.ts --configPlugin typescript --environment INLINE_REQUIRES", "watch": "rollup --config rollup.config.ts --configPlugin typescript --watch", "lint": "eslint . --ext .ts --max-warnings 0", "lint:fix": "prettier \"./src/**/*.ts\" --write", From bcc100b8b8dfefa7c28cfdd5b9bfd7ae91cb3099 Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Tue, 25 Jun 2024 19:02:52 +0100 Subject: [PATCH 6/6] deps: fix security alerts --- package-lock.json | 22 +++++++++++----------- package.json | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 23468d5..121945e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "ajv": "^8.12.0", "chalk": "^5.3.0", "commander": "^11.0.0", - "ejs": "^3.1.9", + "ejs": "^3.1.10", "eslint": "^8.51.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-jsdoc": "^46.8.2", @@ -1714,12 +1714,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2034,9 +2034,9 @@ "dev": true }, "node_modules/ejs": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", - "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, "dependencies": { "jake": "^10.8.5" @@ -2548,9 +2548,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" diff --git a/package.json b/package.json index bb4375f..ea6260a 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "ajv": "^8.12.0", "chalk": "^5.3.0", "commander": "^11.0.0", - "ejs": "^3.1.9", + "ejs": "^3.1.10", "eslint": "^8.51.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-jsdoc": "^46.8.2",