Refactor to enforce strictNullChecks
This commit applies `strictNullChecks` to the entire codebase to improve maintainability and type safety. Key changes include: - Remove some explicit null-checks where unnecessary. - Add necessary null-checks. - Refactor static factory functions for a more functional approach. - Improve some test names and contexts for better debugging. - Add unit tests for any additional logic introduced. - Refactor `createPositionFromRegexFullMatch` to its own function as the logic is reused. - Prefer `find` prefix on functions that may return `undefined` and `get` prefix for those that always return a value.
This commit is contained in:
@@ -12,9 +12,6 @@ export class ApplicationFactory implements IApplicationFactory {
|
||||
private readonly getter: AsyncLazy<IApplication>;
|
||||
|
||||
protected constructor(costlyGetter: ApplicationGetterType) {
|
||||
if (!costlyGetter) {
|
||||
throw new Error('missing getter');
|
||||
}
|
||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// Compares to Array<T> objects for equality, ignoring order
|
||||
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||
if (!array1) { throw new Error('missing first array'); }
|
||||
if (!array2) { throw new Error('missing second array'); }
|
||||
const sortedArray1 = sort(array1);
|
||||
const sortedArray2 = sort(array2);
|
||||
return sequenceEqual(sortedArray1, sortedArray2);
|
||||
@@ -12,8 +10,6 @@ export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||
|
||||
// Compares to Array<T> objects for equality in same order
|
||||
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||
if (!array1) { throw new Error('missing first array'); }
|
||||
if (!array2) { throw new Error('missing second array'); }
|
||||
if (array1.length !== array2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -20,23 +20,30 @@ export abstract class CustomError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export const Environment = {
|
||||
interface ErrorPrototypeManipulation {
|
||||
getSetPrototypeOf: () => (typeof Object.setPrototypeOf | undefined);
|
||||
getCaptureStackTrace: () => (typeof Error.captureStackTrace | undefined);
|
||||
}
|
||||
|
||||
export const PlatformErrorPrototypeManipulation: ErrorPrototypeManipulation = {
|
||||
getSetPrototypeOf: () => Object.setPrototypeOf,
|
||||
getCaptureStackTrace: () => Error.captureStackTrace,
|
||||
};
|
||||
|
||||
function fixPrototype(target: Error, prototype: CustomError) {
|
||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
||||
const setPrototypeOf = Environment.getSetPrototypeOf();
|
||||
if (!functionExists(setPrototypeOf)) {
|
||||
// This is recommended by TypeScript guidelines.
|
||||
// Source: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
||||
// Snapshots: https://web.archive.org/web/20231111234849/https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget, https://archive.ph/tr7cX#support-for-newtarget
|
||||
const setPrototypeOf = PlatformErrorPrototypeManipulation.getSetPrototypeOf();
|
||||
if (!isFunction(setPrototypeOf)) {
|
||||
return;
|
||||
}
|
||||
setPrototypeOf(target, prototype);
|
||||
}
|
||||
|
||||
function ensureStackTrace(target: Error) {
|
||||
const captureStackTrace = Environment.getCaptureStackTrace();
|
||||
if (!functionExists(captureStackTrace)) {
|
||||
const captureStackTrace = PlatformErrorPrototypeManipulation.getCaptureStackTrace();
|
||||
if (!isFunction(captureStackTrace)) {
|
||||
// captureStackTrace is only available on V8, if it's not available
|
||||
// modern JS engines will usually generate a stack trace on error objects when they're thrown.
|
||||
return;
|
||||
@@ -44,7 +51,7 @@ function ensureStackTrace(target: Error) {
|
||||
captureStackTrace(target, target.constructor);
|
||||
}
|
||||
|
||||
function functionExists(func: unknown): boolean {
|
||||
// Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
function isFunction(func: unknown): func is Function {
|
||||
return typeof func === 'function';
|
||||
}
|
||||
|
||||
@@ -54,9 +54,6 @@ export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
||||
value: TEnumValue,
|
||||
enumVariable: EnumVariable<T, TEnumValue>,
|
||||
) {
|
||||
if (value === undefined || value === null) {
|
||||
throw new Error('absent enum value');
|
||||
}
|
||||
if (!(value in enumVariable)) {
|
||||
throw new RangeError(`enum value "${value}" is out of range`);
|
||||
}
|
||||
|
||||
@@ -9,19 +9,16 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
|
||||
|
||||
public create(language: ScriptingLanguage): T {
|
||||
assertInRange(language, ScriptingLanguage);
|
||||
if (!this.getters.has(language)) {
|
||||
const getter = this.getters.get(language);
|
||||
if (!getter) {
|
||||
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||
}
|
||||
const getter = this.getters.get(language);
|
||||
const instance = getter();
|
||||
return instance;
|
||||
}
|
||||
|
||||
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
||||
assertInRange(language, ScriptingLanguage);
|
||||
if (!getter) {
|
||||
throw new Error('missing getter');
|
||||
}
|
||||
if (this.getters.has(language)) {
|
||||
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ export class ApplicationContext implements IApplicationContext {
|
||||
public readonly app: IApplication,
|
||||
initialContext: OperatingSystem,
|
||||
) {
|
||||
validateApp(app);
|
||||
this.states = initializeStates(app);
|
||||
this.changeContext(initialContext);
|
||||
}
|
||||
@@ -36,10 +35,8 @@ export class ApplicationContext implements IApplicationContext {
|
||||
if (this.currentOs === os) {
|
||||
return;
|
||||
}
|
||||
this.collection = this.app.getCollection(os);
|
||||
if (!this.collection) {
|
||||
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
||||
}
|
||||
const collection = this.app.getCollection(os);
|
||||
this.collection = collection;
|
||||
const event: IApplicationContextChangedEvent = {
|
||||
newState: this.states[os],
|
||||
oldState: this.states[this.currentOs],
|
||||
@@ -49,12 +46,6 @@ export class ApplicationContext implements IApplicationContext {
|
||||
}
|
||||
}
|
||||
|
||||
function validateApp(app: IApplication) {
|
||||
if (!app) {
|
||||
throw new Error('missing app');
|
||||
}
|
||||
}
|
||||
|
||||
function initializeStates(app: IApplication): StateMachine {
|
||||
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||
for (const collection of app.collections) {
|
||||
|
||||
@@ -10,18 +10,23 @@ export async function buildContext(
|
||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||
environment = RuntimeEnvironment.CurrentEnvironment,
|
||||
): Promise<IApplicationContext> {
|
||||
if (!factory) { throw new Error('missing factory'); }
|
||||
if (!environment) { throw new Error('missing environment'); }
|
||||
const app = await factory.getApp();
|
||||
const os = getInitialOs(app, environment.os);
|
||||
return new ApplicationContext(app, os);
|
||||
}
|
||||
|
||||
function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
|
||||
function getInitialOs(
|
||||
app: IApplication,
|
||||
currentOs: OperatingSystem | undefined,
|
||||
): OperatingSystem {
|
||||
const supportedOsList = app.getSupportedOsList();
|
||||
if (supportedOsList.includes(currentOs)) {
|
||||
if (currentOs !== undefined && supportedOsList.includes(currentOs)) {
|
||||
return currentOs;
|
||||
}
|
||||
return getMostSupportedOs(supportedOsList, app);
|
||||
}
|
||||
|
||||
function getMostSupportedOs(supportedOsList: OperatingSystem[], app: IApplication) {
|
||||
supportedOsList.sort((os1, os2) => {
|
||||
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
||||
return getPriority(os2) - getPriority(os1);
|
||||
|
||||
@@ -21,9 +21,6 @@ export class ApplicationCode implements IApplicationCode {
|
||||
private readonly scriptingDefinition: IScriptingDefinition,
|
||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
||||
) {
|
||||
if (!userSelection) { throw new Error('missing userSelection'); }
|
||||
if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); }
|
||||
if (!generator) { throw new Error('missing generator'); }
|
||||
this.setCode(userSelection.selectedScripts);
|
||||
userSelection.changed.on((scripts) => {
|
||||
this.setCode(scripts);
|
||||
|
||||
@@ -36,7 +36,11 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
}
|
||||
|
||||
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||
return this.scripts.get(script);
|
||||
const position = this.scripts.get(script);
|
||||
if (!position) {
|
||||
throw new Error('Unknown script: Position could not be found for the script');
|
||||
}
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
||||
return this;
|
||||
}
|
||||
const lines = code.match(/[^\r\n]+/g);
|
||||
this.lines.push(...lines);
|
||||
if (lines) {
|
||||
this.lines.push(...lines);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@ export class UserScriptGenerator implements IUserScriptGenerator {
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
scriptingDefinition: IScriptingDefinition,
|
||||
): IUserScript {
|
||||
if (!selectedScripts) { throw new Error('missing scripts'); }
|
||||
if (!scriptingDefinition) { throw new Error('missing definition'); }
|
||||
if (!selectedScripts.length) {
|
||||
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
||||
}
|
||||
@@ -68,8 +66,19 @@ function appendSelection(
|
||||
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
||||
const { script } = selection;
|
||||
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
||||
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
|
||||
const scriptCode = getSelectedCode(selection);
|
||||
return builder
|
||||
.appendLine()
|
||||
.appendFunction(name, scriptCode);
|
||||
}
|
||||
|
||||
function getSelectedCode(selection: SelectedScript): string {
|
||||
const { code } = selection.script;
|
||||
if (!selection.revert) {
|
||||
return code.execute;
|
||||
}
|
||||
if (!code.revert) {
|
||||
throw new Error('Reverted script lacks revert code.');
|
||||
}
|
||||
return code.revert;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||
import { FilterActionType } from './FilterActionType';
|
||||
import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from './IFilterChangeDetails';
|
||||
import {
|
||||
IFilterChangeDetails, IFilterChangeDetailsVisitor,
|
||||
ApplyFilterAction, ClearFilterAction,
|
||||
} from './IFilterChangeDetails';
|
||||
|
||||
export class FilterChange implements IFilterChangeDetails {
|
||||
public static forApply(filter: IFilterResult) {
|
||||
if (!filter) {
|
||||
throw new Error('missing filter');
|
||||
}
|
||||
return new FilterChange(FilterActionType.Apply, filter);
|
||||
public static forApply(
|
||||
filter: IFilterResult,
|
||||
): IFilterChangeDetails {
|
||||
return new FilterChange({ type: FilterActionType.Apply, filter });
|
||||
}
|
||||
|
||||
public static forClear() {
|
||||
return new FilterChange(FilterActionType.Clear);
|
||||
public static forClear(): IFilterChangeDetails {
|
||||
return new FilterChange({ type: FilterActionType.Clear });
|
||||
}
|
||||
|
||||
private constructor(
|
||||
public readonly actionType: FilterActionType,
|
||||
public readonly filter?: IFilterResult,
|
||||
) { }
|
||||
private constructor(public readonly action: ApplyFilterAction | ClearFilterAction) { }
|
||||
|
||||
public visit(visitor: IFilterChangeDetailsVisitor): void {
|
||||
if (!visitor) {
|
||||
throw new Error('missing visitor');
|
||||
}
|
||||
switch (this.actionType) {
|
||||
switch (this.action.type) {
|
||||
case FilterActionType.Apply:
|
||||
visitor.onApply(this.filter);
|
||||
if (visitor.onApply) {
|
||||
visitor.onApply(this.action.filter);
|
||||
}
|
||||
break;
|
||||
case FilterActionType.Clear:
|
||||
visitor.onClear();
|
||||
if (visitor.onClear) {
|
||||
visitor.onClear();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown action type: ${this.actionType}`);
|
||||
throw new Error(`Unknown action: ${this.action}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,22 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'
|
||||
import { FilterActionType } from './FilterActionType';
|
||||
|
||||
export interface IFilterChangeDetails {
|
||||
readonly actionType: FilterActionType;
|
||||
readonly filter?: IFilterResult;
|
||||
|
||||
readonly action: FilterAction;
|
||||
visit(visitor: IFilterChangeDetailsVisitor): void;
|
||||
}
|
||||
|
||||
export interface IFilterChangeDetailsVisitor {
|
||||
onClear(): void;
|
||||
onApply(filter: IFilterResult): void;
|
||||
readonly onClear?: () => void;
|
||||
readonly onApply?: (filter: IFilterResult) => void;
|
||||
}
|
||||
|
||||
export type ApplyFilterAction = {
|
||||
readonly type: FilterActionType.Apply,
|
||||
readonly filter: IFilterResult;
|
||||
};
|
||||
|
||||
export type ClearFilterAction = {
|
||||
readonly type: FilterActionType.Clear,
|
||||
};
|
||||
|
||||
export type FilterAction = ApplyFilterAction | ClearFilterAction;
|
||||
|
||||
@@ -9,8 +9,6 @@ export class FilterResult implements IFilterResult {
|
||||
public readonly query: string,
|
||||
) {
|
||||
if (!query) { throw new Error('Query is empty or undefined'); }
|
||||
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
|
||||
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
|
||||
}
|
||||
|
||||
public hasAnyMatches(): boolean {
|
||||
|
||||
@@ -43,7 +43,7 @@ export class UserSelection implements IUserSelection {
|
||||
}
|
||||
|
||||
public removeAllInCategory(categoryId: number): void {
|
||||
const category = this.collection.findCategory(categoryId);
|
||||
const category = this.collection.getCategory(categoryId);
|
||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||
.filter((script) => this.scripts.exists(script.id));
|
||||
if (!scriptsToRemove.length) {
|
||||
@@ -57,7 +57,7 @@ export class UserSelection implements IUserSelection {
|
||||
|
||||
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
||||
const scriptsToAddOrUpdate = this.collection
|
||||
.findCategory(categoryId)
|
||||
.getCategory(categoryId)
|
||||
.getAllScriptsRecursively()
|
||||
.filter(
|
||||
(script) => !this.scripts.exists(script.id)
|
||||
@@ -74,17 +74,14 @@ export class UserSelection implements IUserSelection {
|
||||
}
|
||||
|
||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||
const script = this.collection.findScript(scriptId);
|
||||
if (!script) {
|
||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
||||
}
|
||||
const script = this.collection.getScript(scriptId);
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addItem(selectedScript);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
||||
const script = this.collection.findScript(scriptId);
|
||||
const script = this.collection.getScript(scriptId);
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addOrUpdateItem(selectedScript);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
@@ -130,7 +127,7 @@ export class UserSelection implements IUserSelection {
|
||||
}
|
||||
|
||||
public selectOnly(scripts: readonly IScript[]): void {
|
||||
if (!scripts || scripts.length === 0) {
|
||||
if (!scripts.length) {
|
||||
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
||||
}
|
||||
let totalChanged = 0;
|
||||
|
||||
@@ -32,10 +32,7 @@ const PreParsedCollections: readonly CollectionData [] = [
|
||||
];
|
||||
|
||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||
if (!collections?.length) {
|
||||
if (!collections.length) {
|
||||
throw new Error('missing collections');
|
||||
}
|
||||
if (collections.some((collection) => !collection)) {
|
||||
throw new Error('missing collection provided');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,7 @@ export function parseCategoryCollection(
|
||||
}
|
||||
|
||||
function validate(content: CollectionData): void {
|
||||
if (!content) {
|
||||
throw new Error('missing content');
|
||||
}
|
||||
if (!content.actions || content.actions.length <= 0) {
|
||||
if (!content.actions.length) {
|
||||
throw new Error('content does not define any action');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {
|
||||
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
|
||||
CategoryData, ScriptData, CategoryOrScriptData,
|
||||
} from '@/application/collections/';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { Category } from '@/domain/Category';
|
||||
@@ -16,7 +16,6 @@ export function parseCategory(
|
||||
context: ICategoryCollectionParseContext,
|
||||
factory: CategoryFactoryType = CategoryFactory,
|
||||
): Category {
|
||||
if (!context) { throw new Error('missing context'); }
|
||||
return parseCategoryRecursively({
|
||||
categoryData: category,
|
||||
context,
|
||||
@@ -30,8 +29,8 @@ interface ICategoryParseContext {
|
||||
readonly factory: CategoryFactoryType,
|
||||
readonly parentCategory?: CategoryData,
|
||||
}
|
||||
// eslint-disable-next-line consistent-return
|
||||
function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
||||
|
||||
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
|
||||
ensureValidCategory(context.categoryData, context.parentCategory);
|
||||
const children: ICategoryChildren = {
|
||||
subCategories: new Array<Category>(),
|
||||
@@ -55,7 +54,7 @@ function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
||||
/* scripts: */ children.subScripts,
|
||||
);
|
||||
} catch (err) {
|
||||
new NodeValidator({
|
||||
return new NodeValidator({
|
||||
type: NodeType.Category,
|
||||
selfNode: context.categoryData,
|
||||
parentNode: context.parentCategory,
|
||||
@@ -72,7 +71,7 @@ function ensureValidCategory(category: CategoryData, parentCategory?: CategoryDa
|
||||
.assertDefined(category)
|
||||
.assertValidName(category.category)
|
||||
.assert(
|
||||
() => category.children && category.children.length > 0,
|
||||
() => category.children.length > 0,
|
||||
`"${category.category}" has no children.`,
|
||||
);
|
||||
}
|
||||
@@ -94,14 +93,14 @@ function parseNode(context: INodeParseContext) {
|
||||
validator.assertDefined(context.nodeData);
|
||||
if (isCategory(context.nodeData)) {
|
||||
const subCategory = parseCategoryRecursively({
|
||||
categoryData: context.nodeData as CategoryData,
|
||||
categoryData: context.nodeData,
|
||||
context: context.context,
|
||||
factory: context.factory,
|
||||
parentCategory: context.parent,
|
||||
});
|
||||
context.children.subCategories.push(subCategory);
|
||||
} else if (isScript(context.nodeData)) {
|
||||
const script = parseScript(context.nodeData as ScriptData, context.context);
|
||||
const script = parseScript(context.nodeData, context.context);
|
||||
context.children.subScripts.push(script);
|
||||
} else {
|
||||
validator.throw('Node is neither a category or a script.');
|
||||
@@ -109,19 +108,18 @@ function parseNode(context: INodeParseContext) {
|
||||
}
|
||||
|
||||
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||
const holder = (data as InstructionHolder);
|
||||
return hasCode(holder) || hasCall(holder);
|
||||
return hasCode(data) || hasCall(data);
|
||||
}
|
||||
|
||||
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||
return hasProperty(data, 'category');
|
||||
}
|
||||
|
||||
function hasCode(data: InstructionHolder): boolean {
|
||||
function hasCode(data: unknown): boolean {
|
||||
return hasProperty(data, 'code');
|
||||
}
|
||||
|
||||
function hasCall(data: InstructionHolder) {
|
||||
function hasCall(data: unknown) {
|
||||
return hasProperty(data, 'call');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||
|
||||
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
||||
if (!documentable) {
|
||||
throw new Error('missing documentable');
|
||||
}
|
||||
const { docs } = documentable;
|
||||
if (!docs) {
|
||||
return [];
|
||||
|
||||
@@ -32,7 +32,7 @@ export class NodeValidator {
|
||||
return this;
|
||||
}
|
||||
|
||||
public throw(errorMessage: string) {
|
||||
public throw(errorMessage: string): never {
|
||||
throw new NodeDataError(errorMessage, this.context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,7 @@ export class CategoryCollectionParseContext implements ICategoryCollectionParseC
|
||||
scripting: IScriptingDefinition,
|
||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||
) {
|
||||
if (!scripting) { throw new Error('missing scripting'); }
|
||||
this.syntax = syntaxFactory.create(scripting.language);
|
||||
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
||||
this.compiler = new ScriptCompiler(functionsData ?? [], this.syntax);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,10 @@ export class Expression implements IExpression {
|
||||
public readonly evaluator: ExpressionEvaluator,
|
||||
parameters?: IReadOnlyFunctionParameterCollection,
|
||||
) {
|
||||
if (!position) {
|
||||
throw new Error('missing position');
|
||||
}
|
||||
if (!evaluator) {
|
||||
throw new Error('missing evaluator');
|
||||
}
|
||||
this.parameters = parameters ?? new FunctionParameterCollection();
|
||||
}
|
||||
|
||||
public evaluate(context: IExpressionEvaluationContext): string {
|
||||
if (!context) {
|
||||
throw new Error('missing context');
|
||||
}
|
||||
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||
const args = filterUnusedArguments(this.parameters, context.args);
|
||||
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||
|
||||
@@ -12,8 +12,5 @@ export class ExpressionEvaluationContext implements IExpressionEvaluationContext
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
||||
) {
|
||||
if (!args) {
|
||||
throw new Error('missing args, send empty collection instead.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
|
||||
export function createPositionFromRegexFullMatch(
|
||||
match: RegExpMatchArray,
|
||||
): ExpressionPosition {
|
||||
const startPos = match.index;
|
||||
if (startPos === undefined) {
|
||||
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
|
||||
}
|
||||
const fullMatch = match[0];
|
||||
if (!fullMatch.length) {
|
||||
throw new Error(`Regex match is empty: ${JSON.stringify(match)}`);
|
||||
}
|
||||
const endPos = startPos + fullMatch.length;
|
||||
return new ExpressionPosition(startPos, endPos);
|
||||
}
|
||||
@@ -11,14 +11,11 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||
) { }
|
||||
|
||||
public compileExpressions(
|
||||
code: string | undefined,
|
||||
code: string,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
): string {
|
||||
if (!args) {
|
||||
throw new Error('missing args, send empty collection instead.');
|
||||
}
|
||||
if (!code) {
|
||||
return code;
|
||||
return '';
|
||||
}
|
||||
const context = new ExpressionEvaluationContext(args);
|
||||
const compiledCode = compileRecursively(code, context, this.extractor);
|
||||
@@ -145,7 +142,7 @@ function ensureParamsUsedInCodeHasArgsProvided(
|
||||
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
): void {
|
||||
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||
if (!usedParameterNames?.length) {
|
||||
if (!usedParameterNames.length) {
|
||||
return;
|
||||
}
|
||||
const notProvidedParameters = usedParameterNames
|
||||
|
||||
@@ -2,6 +2,7 @@ import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argume
|
||||
|
||||
export interface IExpressionsCompiler {
|
||||
compileExpressions(
|
||||
code: string | undefined,
|
||||
args: IReadOnlyFunctionCallArgumentCollection): string;
|
||||
code: string,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
): string;
|
||||
}
|
||||
|
||||
@@ -10,12 +10,9 @@ const Parsers = [
|
||||
|
||||
export class CompositeExpressionParser implements IExpressionParser {
|
||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||
if (!leafs) {
|
||||
if (!leafs.length) {
|
||||
throw new Error('missing leafs');
|
||||
}
|
||||
if (leafs.some((leaf) => !leaf)) {
|
||||
throw new Error('missing leaf');
|
||||
}
|
||||
}
|
||||
|
||||
public findExpressions(code: string): IExpression[] {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IExpressionParser } from '../IExpressionParser';
|
||||
import { ExpressionPosition } from '../../Expression/ExpressionPosition';
|
||||
import { IExpression } from '../../Expression/IExpression';
|
||||
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
|
||||
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
||||
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
||||
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
|
||||
|
||||
export abstract class RegexParser implements IExpressionParser {
|
||||
protected abstract readonly regex: RegExp;
|
||||
@@ -21,7 +21,7 @@ export abstract class RegexParser implements IExpressionParser {
|
||||
const matches = code.matchAll(this.regex);
|
||||
for (const match of matches) {
|
||||
const primitiveExpression = this.buildExpression(match);
|
||||
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
|
||||
const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code);
|
||||
const parameters = createParameters(primitiveExpression);
|
||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||
yield expression;
|
||||
@@ -37,12 +37,6 @@ export abstract class RegexParser implements IExpressionParser {
|
||||
}
|
||||
}
|
||||
|
||||
function createPosition(match: RegExpMatchArray): ExpressionPosition {
|
||||
const startPos = match.index;
|
||||
const endPos = startPos + match[0].length;
|
||||
return new ExpressionPosition(startPos, endPos);
|
||||
}
|
||||
|
||||
function createParameters(
|
||||
expression: IPrimitiveExpression,
|
||||
): FunctionParameterCollection {
|
||||
|
||||
@@ -28,7 +28,7 @@ function hasLines(text: string) {
|
||||
*/
|
||||
function inlineComments(code: string): string {
|
||||
const makeInlineComment = (comment: string) => {
|
||||
const value = comment?.trim();
|
||||
const value = comment.trim();
|
||||
if (!value) {
|
||||
return '<##>';
|
||||
}
|
||||
|
||||
@@ -15,12 +15,6 @@ export class PipeFactory implements IPipeFactory {
|
||||
private readonly pipes = new Map<string, IPipe>();
|
||||
|
||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||
if (!pipes) {
|
||||
throw new Error('missing pipes');
|
||||
}
|
||||
if (pipes.some((pipe) => !pipe)) {
|
||||
throw new Error('missing pipe in list');
|
||||
}
|
||||
for (const pipe of pipes) {
|
||||
this.registerPipe(pipe);
|
||||
}
|
||||
@@ -28,10 +22,11 @@ export class PipeFactory implements IPipeFactory {
|
||||
|
||||
public get(pipeName: string): IPipe {
|
||||
validatePipeName(pipeName);
|
||||
if (!this.pipes.has(pipeName)) {
|
||||
const pipe = this.pipes.get(pipeName);
|
||||
if (!pipe) {
|
||||
throw new Error(`Unknown pipe: "${pipeName}"`);
|
||||
}
|
||||
return this.pipes.get(pipeName);
|
||||
return pipe;
|
||||
}
|
||||
|
||||
private registerPipe(pipe: IPipe): void {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function
|
||||
import { IExpression } from '../Expression/IExpression';
|
||||
import { ExpressionPosition } from '../Expression/ExpressionPosition';
|
||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
|
||||
|
||||
export class WithParser implements IExpressionParser {
|
||||
public findExpressions(code: string): IExpression[] {
|
||||
@@ -42,31 +43,25 @@ function parseAllWithExpressions(
|
||||
expressions.push({
|
||||
type: WithStatementType.Start,
|
||||
parameterName: match[1],
|
||||
position: createPosition(match),
|
||||
position: createPositionFromRegexFullMatch(match),
|
||||
});
|
||||
}
|
||||
for (const match of input.matchAll(WithStatementEndRegEx)) {
|
||||
expressions.push({
|
||||
type: WithStatementType.End,
|
||||
position: createPosition(match),
|
||||
position: createPositionFromRegexFullMatch(match),
|
||||
});
|
||||
}
|
||||
for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) {
|
||||
expressions.push({
|
||||
type: WithStatementType.ContextVariable,
|
||||
position: createPosition(match),
|
||||
position: createPositionFromRegexFullMatch(match),
|
||||
pipeline: match[1],
|
||||
});
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
function createPosition(match: RegExpMatchArray): ExpressionPosition {
|
||||
const startPos = match.index;
|
||||
const endPos = startPos + match[0].length;
|
||||
return new ExpressionPosition(startPos, endPos);
|
||||
}
|
||||
|
||||
class WithStatementBuilder {
|
||||
private readonly contextVariables = new Array<{
|
||||
readonly positionInScope: ExpressionPosition;
|
||||
@@ -125,7 +120,7 @@ class WithStatementBuilder {
|
||||
|
||||
private substituteContextVariables(
|
||||
scope: string,
|
||||
substituter: (pipeline: string) => string,
|
||||
substituter: (pipeline?: string) => string,
|
||||
): string {
|
||||
if (!this.contextVariables.length) {
|
||||
return scope;
|
||||
@@ -157,7 +152,7 @@ function parseWithExpressions(input: string): IExpression[] {
|
||||
.sort((a, b) => b.position.start - a.position.start);
|
||||
const expressions = new Array<IExpression>();
|
||||
const builders = new Array<WithStatementBuilder>();
|
||||
const throwWithContext = (message: string) => {
|
||||
const throwWithContext = (message: string): never => {
|
||||
throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`);
|
||||
};
|
||||
while (sortedStatements.length > 0) {
|
||||
@@ -178,12 +173,15 @@ function parseWithExpressions(input: string): IExpression[] {
|
||||
}
|
||||
builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline);
|
||||
break;
|
||||
case WithStatementType.End:
|
||||
if (builders.length === 0) {
|
||||
case WithStatementType.End: {
|
||||
const builder = builders.pop();
|
||||
if (!builder) {
|
||||
throwWithContext('Redundant `end` statement, missing `with`?');
|
||||
break;
|
||||
}
|
||||
expressions.push(builders.pop().buildExpression(statement.position, input));
|
||||
expressions.push(builder.buildExpression(statement.position, input));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (builders.length > 0) {
|
||||
|
||||
@@ -5,9 +5,6 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
|
||||
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||
|
||||
public addArgument(argument: IFunctionCallArgument): void {
|
||||
if (!argument) {
|
||||
throw new Error('missing argument');
|
||||
}
|
||||
if (this.hasArgument(argument.parameterName)) {
|
||||
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
||||
}
|
||||
|
||||
@@ -3,18 +3,22 @@ import { CodeSegmentMerger } from './CodeSegmentMerger';
|
||||
|
||||
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
||||
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
|
||||
if (!codeSegments?.length) {
|
||||
if (!codeSegments.length) {
|
||||
throw new Error('missing segments');
|
||||
}
|
||||
return {
|
||||
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
||||
revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)),
|
||||
revertCode: joinCodeParts(
|
||||
codeSegments
|
||||
.map((f) => f.revertCode)
|
||||
.filter((code): code is string => Boolean(code)),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function joinCodeParts(codeSegments: readonly string[]): string {
|
||||
return codeSegments
|
||||
.filter((segment) => segment?.length > 0)
|
||||
.filter((segment) => segment.length > 0)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
@@ -21,9 +21,7 @@ export class FunctionCallSequenceCompiler implements FunctionCallCompiler {
|
||||
calls: readonly FunctionCall[],
|
||||
functions: ISharedFunctionCollection,
|
||||
): CompiledCode {
|
||||
if (!functions) { throw new Error('missing functions'); }
|
||||
if (!calls?.length) { throw new Error('missing calls'); }
|
||||
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
|
||||
if (!calls.length) { throw new Error('missing calls'); }
|
||||
const context: FunctionCallCompilationContext = {
|
||||
allFunctions: functions,
|
||||
rootCallSequence: calls,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||
@@ -12,19 +12,33 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||
}
|
||||
|
||||
public canCompile(func: ISharedFunction): boolean {
|
||||
return func.body.code !== undefined;
|
||||
return func.body.type === FunctionBodyType.Code;
|
||||
}
|
||||
|
||||
public compileFunction(
|
||||
calledFunction: ISharedFunction,
|
||||
callToFunction: FunctionCall,
|
||||
): CompiledCode[] {
|
||||
if (calledFunction.body.type !== FunctionBodyType.Code) {
|
||||
throw new Error([
|
||||
'Unexpected function body type.',
|
||||
`\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`,
|
||||
`\tActual: "${FunctionBodyType[calledFunction.body.type]}"`,
|
||||
'Function:',
|
||||
`\t${JSON.stringify(callToFunction)}`,
|
||||
].join('\n'));
|
||||
}
|
||||
const { code } = calledFunction.body;
|
||||
const { args } = callToFunction;
|
||||
return [
|
||||
{
|
||||
code: this.expressionsCompiler.compileExpressions(code.execute, args),
|
||||
revertCode: this.expressionsCompiler.compileExpressions(code.revert, args),
|
||||
revertCode: (() => {
|
||||
if (!code.revert) {
|
||||
return undefined;
|
||||
}
|
||||
return this.expressionsCompiler.compileExpressions(code.revert, args);
|
||||
})(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { CallFunctionBody, FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
@@ -13,7 +13,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||
}
|
||||
|
||||
public canCompile(func: ISharedFunction): boolean {
|
||||
return func.body.calls !== undefined;
|
||||
return func.body.type === FunctionBodyType.Calls;
|
||||
}
|
||||
|
||||
public compileFunction(
|
||||
@@ -21,7 +21,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||
callToFunction: FunctionCall,
|
||||
context: FunctionCallCompilationContext,
|
||||
): CompiledCode[] {
|
||||
const nestedCalls = calledFunction.body.calls;
|
||||
const nestedCalls = (calledFunction.body as CallFunctionBody).calls;
|
||||
return nestedCalls.map((nestedCall) => {
|
||||
try {
|
||||
const compiledParentCall = this.argumentCompiler
|
||||
|
||||
@@ -5,9 +5,6 @@ import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
||||
import { ParsedFunctionCall } from './ParsedFunctionCall';
|
||||
|
||||
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
|
||||
if (calls === undefined) {
|
||||
throw new Error('missing call data');
|
||||
}
|
||||
const sequence = getCallSequence(calls);
|
||||
return sequence.map((call) => parseFunctionCall(call));
|
||||
}
|
||||
@@ -19,22 +16,21 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
||||
if (calls instanceof Array) {
|
||||
return calls as FunctionCallData[];
|
||||
}
|
||||
return [calls as FunctionCallData];
|
||||
const singleCall = calls;
|
||||
return [singleCall];
|
||||
}
|
||||
|
||||
function parseFunctionCall(call: FunctionCallData): FunctionCall {
|
||||
if (!call) {
|
||||
throw new Error('missing call data');
|
||||
}
|
||||
const callArgs = parseArgs(call.parameters);
|
||||
return new ParsedFunctionCall(call.function, callArgs);
|
||||
}
|
||||
|
||||
function parseArgs(
|
||||
parameters: FunctionCallParametersData,
|
||||
parameters: FunctionCallParametersData | undefined,
|
||||
): FunctionCallArgumentCollection {
|
||||
return Object.keys(parameters || {})
|
||||
.map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName]))
|
||||
const parametersMap = parameters ?? {};
|
||||
return Object.keys(parametersMap)
|
||||
.map((parameterName) => new FunctionCallArgument(parameterName, parametersMap[parameterName]))
|
||||
.reduce((args, arg) => {
|
||||
args.addArgument(arg);
|
||||
return args;
|
||||
|
||||
@@ -9,8 +9,5 @@ export class ParsedFunctionCall implements FunctionCall {
|
||||
if (!functionName) {
|
||||
throw new Error('missing function name in function call');
|
||||
}
|
||||
if (!args) {
|
||||
throw new Error('missing args');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,21 @@ import { FunctionCall } from './Call/FunctionCall';
|
||||
export interface ISharedFunction {
|
||||
readonly name: string;
|
||||
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||
readonly body: ISharedFunctionBody;
|
||||
readonly body: SharedFunctionBody;
|
||||
}
|
||||
|
||||
export interface ISharedFunctionBody {
|
||||
readonly type: FunctionBodyType;
|
||||
readonly code: IFunctionCode | undefined;
|
||||
readonly calls: readonly FunctionCall[] | undefined;
|
||||
export interface CallFunctionBody {
|
||||
readonly type: FunctionBodyType.Calls,
|
||||
readonly calls: readonly FunctionCall[],
|
||||
}
|
||||
|
||||
export interface CodeFunctionBody {
|
||||
readonly type: FunctionBodyType.Code;
|
||||
readonly code: IFunctionCode,
|
||||
}
|
||||
|
||||
export type SharedFunctionBody = CallFunctionBody | CodeFunctionBody;
|
||||
|
||||
export enum FunctionBodyType {
|
||||
Code,
|
||||
Calls,
|
||||
|
||||
@@ -18,9 +18,6 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
|
||||
}
|
||||
|
||||
private ensureValidParameter(parameter: IFunctionParameter) {
|
||||
if (!parameter) {
|
||||
throw new Error('missing parameter');
|
||||
}
|
||||
if (this.includesName(parameter.name)) {
|
||||
throw new Error(`duplicate parameter name: "${parameter.name}"`);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FunctionCall } from './Call/FunctionCall';
|
||||
|
||||
import {
|
||||
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
||||
FunctionBodyType, IFunctionCode, ISharedFunction, SharedFunctionBody,
|
||||
} from './ISharedFunction';
|
||||
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||
|
||||
@@ -10,7 +10,7 @@ export function createCallerFunction(
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
callSequence: readonly FunctionCall[],
|
||||
): ISharedFunction {
|
||||
if (!callSequence || !callSequence.length) {
|
||||
if (!callSequence.length) {
|
||||
throw new Error(`missing call sequence in function "${name}"`);
|
||||
}
|
||||
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
|
||||
@@ -33,7 +33,7 @@ export function createFunctionWithInlineCode(
|
||||
}
|
||||
|
||||
class SharedFunction implements ISharedFunction {
|
||||
public readonly body: ISharedFunctionBody;
|
||||
public readonly body: SharedFunctionBody;
|
||||
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
@@ -42,11 +42,22 @@ class SharedFunction implements ISharedFunction {
|
||||
bodyType: FunctionBodyType,
|
||||
) {
|
||||
if (!name) { throw new Error('missing function name'); }
|
||||
if (!parameters) { throw new Error('missing parameters'); }
|
||||
this.body = {
|
||||
type: bodyType,
|
||||
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
||||
calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined,
|
||||
};
|
||||
|
||||
switch (bodyType) {
|
||||
case FunctionBodyType.Code:
|
||||
this.body = {
|
||||
type: FunctionBodyType.Code,
|
||||
code: content as IFunctionCode,
|
||||
};
|
||||
break;
|
||||
case FunctionBodyType.Calls:
|
||||
this.body = {
|
||||
type: FunctionBodyType.Calls,
|
||||
calls: content as readonly FunctionCall[],
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown body type: ${FunctionBodyType[bodyType]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
|
||||
private readonly functionsByName = new Map<string, ISharedFunction>();
|
||||
|
||||
public addFunction(func: ISharedFunction): void {
|
||||
if (!func) { throw new Error('missing function'); }
|
||||
if (this.has(func.name)) {
|
||||
throw new Error(`function with name ${func.name} already exists`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { FunctionData, InstructionHolder } from '@/application/collections/';
|
||||
import type {
|
||||
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, CallInstruction,
|
||||
} from '@/application/collections/';
|
||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||
@@ -23,9 +25,8 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||
functions: readonly FunctionData[],
|
||||
syntax: ILanguageSyntax,
|
||||
): ISharedFunctionCollection {
|
||||
if (!syntax) { throw new Error('missing syntax'); }
|
||||
const collection = new SharedFunctionCollection();
|
||||
if (!functions || !functions.length) {
|
||||
if (!functions.length) {
|
||||
return collection;
|
||||
}
|
||||
ensureValidFunctions(functions);
|
||||
@@ -55,16 +56,18 @@ function parseFunction(
|
||||
}
|
||||
|
||||
function validateCode(
|
||||
data: FunctionData,
|
||||
data: CodeFunctionData,
|
||||
syntax: ILanguageSyntax,
|
||||
validator: ICodeValidator,
|
||||
): void {
|
||||
[data.code, data.revertCode].forEach(
|
||||
(code) => validator.throwIfInvalid(
|
||||
code,
|
||||
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||
),
|
||||
);
|
||||
[data.code, data.revertCode]
|
||||
.filter((code): code is string => Boolean(code))
|
||||
.forEach(
|
||||
(code) => validator.throwIfInvalid(
|
||||
code,
|
||||
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
||||
@@ -85,19 +88,18 @@ function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollecti
|
||||
}, new FunctionParameterCollection());
|
||||
}
|
||||
|
||||
function hasCode(data: FunctionData): boolean {
|
||||
return Boolean(data.code);
|
||||
function hasCode(data: FunctionData): data is CodeFunctionData {
|
||||
return (data as CodeInstruction).code !== undefined;
|
||||
}
|
||||
|
||||
function hasCall(data: FunctionData): boolean {
|
||||
return Boolean(data.call);
|
||||
function hasCall(data: FunctionData): data is CallFunctionData {
|
||||
return (data as CallInstruction).call !== undefined;
|
||||
}
|
||||
|
||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||
ensureNoUndefinedItem(functions);
|
||||
ensureNoDuplicatesInFunctionNames(functions);
|
||||
ensureNoDuplicateCode(functions);
|
||||
ensureEitherCallOrCodeIsDefined(functions);
|
||||
ensureNoDuplicateCode(functions);
|
||||
ensureExpectedParametersType(functions);
|
||||
}
|
||||
|
||||
@@ -105,7 +107,7 @@ function printList(list: readonly string[]): string {
|
||||
return `"${list.join('","')}"`;
|
||||
}
|
||||
|
||||
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
|
||||
function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
|
||||
// Ensure functions do not define both call and code
|
||||
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
||||
if (withBothCallAndCode.length) {
|
||||
@@ -132,7 +134,7 @@ function isArrayOfObjects(value: unknown): boolean {
|
||||
&& value.every((item) => typeof item === 'object');
|
||||
}
|
||||
|
||||
function printNames(holders: readonly InstructionHolder[]) {
|
||||
function printNames(holders: readonly FunctionData[]) {
|
||||
return printList(holders.map((holder) => holder.name));
|
||||
}
|
||||
|
||||
@@ -144,22 +146,19 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
||||
if (functions.some((func) => !func)) {
|
||||
throw new Error('some functions are undefined');
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||
const duplicateCodes = getDuplicates(functions
|
||||
const callFunctions = functions
|
||||
.filter((func) => hasCode(func))
|
||||
.map((func) => func as CodeFunctionData);
|
||||
const duplicateCodes = getDuplicates(callFunctions
|
||||
.map((func) => func.code)
|
||||
.filter((code) => code));
|
||||
if (duplicateCodes.length > 0) {
|
||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||
}
|
||||
const duplicateRevertCodes = getDuplicates(functions
|
||||
.filter((func) => func.revertCode)
|
||||
.map((func) => func.revertCode));
|
||||
const duplicateRevertCodes = getDuplicates(callFunctions
|
||||
.map((func) => func.revertCode)
|
||||
.filter((code): code is string => Boolean(code)));
|
||||
if (duplicateRevertCodes.length > 0) {
|
||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FunctionData, ScriptData } from '@/application/collections/';
|
||||
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||
@@ -18,27 +18,24 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
private readonly functions: ISharedFunctionCollection;
|
||||
|
||||
constructor(
|
||||
functions: readonly FunctionData[] | undefined,
|
||||
functions: readonly FunctionData[],
|
||||
syntax: ILanguageSyntax,
|
||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
||||
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
||||
) {
|
||||
if (!syntax) { throw new Error('missing syntax'); }
|
||||
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
||||
}
|
||||
|
||||
public canCompile(script: ScriptData): boolean {
|
||||
if (!script) { throw new Error('missing script'); }
|
||||
if (!script.call) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return hasCall(script);
|
||||
}
|
||||
|
||||
public compile(script: ScriptData): IScriptCode {
|
||||
if (!script) { throw new Error('missing script'); }
|
||||
try {
|
||||
if (!hasCall(script)) {
|
||||
throw new Error('Script does include any calls.');
|
||||
}
|
||||
const calls = parseFunctionCalls(script.call);
|
||||
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
||||
validateCompiledCode(compiledCode, this.codeValidator);
|
||||
@@ -53,7 +50,17 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
}
|
||||
|
||||
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
|
||||
[compiledCode.code, compiledCode.revertCode].forEach(
|
||||
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
|
||||
);
|
||||
[compiledCode.code, compiledCode.revertCode]
|
||||
.filter((code): code is string => Boolean(code))
|
||||
.map((code) => code as string)
|
||||
.forEach(
|
||||
(code) => validator.throwIfInvalid(
|
||||
code,
|
||||
[new NoEmptyLines()],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function hasCall(data: ScriptData): data is ScriptData & CallInstruction {
|
||||
return (data as CallInstruction).call !== undefined;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ScriptData } from '@/application/collections/';
|
||||
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
|
||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { Script } from '@/domain/Script';
|
||||
@@ -14,7 +14,6 @@ import { ICategoryCollectionParseContext } from './ICategoryCollectionParseConte
|
||||
import { CodeValidator } from './Validation/CodeValidator';
|
||||
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
export function parseScript(
|
||||
data: ScriptData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
@@ -24,7 +23,6 @@ export function parseScript(
|
||||
): Script {
|
||||
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
||||
validateScript(data, validator);
|
||||
if (!context) { throw new Error('missing context'); }
|
||||
try {
|
||||
const script = scriptFactory(
|
||||
/* name: */ data.name,
|
||||
@@ -34,12 +32,12 @@ export function parseScript(
|
||||
);
|
||||
return script;
|
||||
} catch (err) {
|
||||
validator.throw(err.message);
|
||||
return validator.throw(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function parseLevel(
|
||||
level: string,
|
||||
level: string | undefined,
|
||||
parser: IEnumParser<RecommendationLevel>,
|
||||
): RecommendationLevel | undefined {
|
||||
if (!level) {
|
||||
@@ -56,39 +54,45 @@ function parseCode(
|
||||
if (context.compiler.canCompile(script)) {
|
||||
return context.compiler.compile(script);
|
||||
}
|
||||
const code = new ScriptCode(script.code, script.revertCode);
|
||||
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
|
||||
const code = new ScriptCode(codeScript.code, codeScript.revertCode);
|
||||
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
||||
return code;
|
||||
}
|
||||
|
||||
function validateHardcodedCodeWithoutCalls(
|
||||
scriptCode: ScriptCode,
|
||||
codeValidator: ICodeValidator,
|
||||
validator: ICodeValidator,
|
||||
syntax: ILanguageSyntax,
|
||||
) {
|
||||
[scriptCode.execute, scriptCode.revert].forEach(
|
||||
(code) => codeValidator.throwIfInvalid(
|
||||
code,
|
||||
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||
),
|
||||
);
|
||||
[scriptCode.execute, scriptCode.revert]
|
||||
.filter((code): code is string => Boolean(code))
|
||||
.forEach(
|
||||
(code) => validator.throwIfInvalid(
|
||||
code,
|
||||
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function validateScript(script: ScriptData, validator: NodeValidator) {
|
||||
function validateScript(
|
||||
script: ScriptData,
|
||||
validator: NodeValidator,
|
||||
): asserts script is NonNullable<ScriptData> {
|
||||
validator
|
||||
.assertDefined(script)
|
||||
.assertValidName(script.name)
|
||||
.assert(
|
||||
() => Boolean(script.code || script.call),
|
||||
'Must define either "call" or "code".',
|
||||
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
||||
'Neither "call" or "code" is defined.',
|
||||
)
|
||||
.assert(
|
||||
() => !(script.code && script.call),
|
||||
'Cannot define both "call" and "code".',
|
||||
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
|
||||
'Both "call" and "code" are defined.',
|
||||
)
|
||||
.assert(
|
||||
() => !(script.revertCode && script.call),
|
||||
'Cannot define "revertCode" if "call" is defined.',
|
||||
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
|
||||
'Both "call" and "revertCode" are defined.',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export class CodeValidator implements ICodeValidator {
|
||||
code: string,
|
||||
rules: readonly ICodeValidationRule[],
|
||||
): void {
|
||||
if (!rules || rules.length === 0) { throw new Error('missing rules'); }
|
||||
if (rules.length === 0) { throw new Error('missing rules'); }
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ import { ICodeLine } from '../ICodeLine';
|
||||
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||
|
||||
export class NoDuplicatedLines implements ICodeValidationRule {
|
||||
constructor(private readonly syntax: ILanguageSyntax) {
|
||||
if (!syntax) { throw new Error('missing syntax'); }
|
||||
}
|
||||
constructor(private readonly syntax: ILanguageSyntax) { }
|
||||
|
||||
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||
return lines
|
||||
|
||||
@@ -17,7 +17,6 @@ export class CodeSubstituter implements ICodeSubstituter {
|
||||
|
||||
public substitute(code: string, info: IProjectInformation): string {
|
||||
if (!code) { throw new Error('missing code'); }
|
||||
if (!info) { throw new Error('missing info'); }
|
||||
const args = new FunctionCallArgumentCollection();
|
||||
const substitute = (name: string, value: string) => args
|
||||
.addArgument(new FunctionCallArgument(name, value));
|
||||
|
||||
@@ -18,8 +18,6 @@ export class ScriptingDefinitionParser {
|
||||
definition: ScriptingDefinitionData,
|
||||
info: IProjectInformation,
|
||||
): IScriptingDefinition {
|
||||
if (!info) { throw new Error('missing info'); }
|
||||
if (!definition) { throw new Error('missing definition'); }
|
||||
const language = this.languageParser.parseEnum(definition.language, 'language');
|
||||
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
|
||||
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
|
||||
|
||||
37
src/application/collections/collection.yaml.d.ts
vendored
37
src/application/collections/collection.yaml.d.ts
vendored
@@ -12,29 +12,38 @@ declare module '@/application/collections/*' {
|
||||
}
|
||||
|
||||
export type CategoryOrScriptData = CategoryData | ScriptData;
|
||||
export type DocumentationData = ReadonlyArray<string> | string;
|
||||
export type DocumentationData = ReadonlyArray<string> | string | undefined;
|
||||
|
||||
export interface DocumentableData {
|
||||
readonly docs?: DocumentationData;
|
||||
}
|
||||
|
||||
export interface InstructionHolder {
|
||||
readonly name: string;
|
||||
|
||||
readonly code?: string;
|
||||
export interface CodeInstruction {
|
||||
readonly code: string;
|
||||
readonly revertCode?: string;
|
||||
|
||||
readonly call?: FunctionCallsData;
|
||||
}
|
||||
|
||||
export interface CallInstruction {
|
||||
readonly call: FunctionCallsData;
|
||||
}
|
||||
|
||||
export type InstructionHolder = CodeInstruction | CallInstruction;
|
||||
|
||||
export interface ParameterDefinitionData {
|
||||
readonly name: string;
|
||||
readonly optional?: boolean;
|
||||
}
|
||||
|
||||
export interface FunctionData extends InstructionHolder {
|
||||
export type FunctionDefinition = {
|
||||
readonly name: string;
|
||||
readonly parameters?: readonly ParameterDefinitionData[];
|
||||
}
|
||||
};
|
||||
|
||||
export type CodeFunctionData = FunctionDefinition & CodeInstruction;
|
||||
|
||||
export type CallFunctionData = FunctionDefinition & CallInstruction;
|
||||
|
||||
export type FunctionData = CodeFunctionData | CallFunctionData;
|
||||
|
||||
export interface FunctionCallParametersData {
|
||||
readonly [index: string]: string;
|
||||
@@ -47,10 +56,16 @@ declare module '@/application/collections/*' {
|
||||
|
||||
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
||||
|
||||
export interface ScriptData extends InstructionHolder, DocumentableData {
|
||||
export type ScriptDefinition = DocumentableData & {
|
||||
readonly name: string;
|
||||
readonly recommend?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type CodeScriptData = ScriptDefinition & CodeInstruction;
|
||||
|
||||
export type CallScriptData = ScriptDefinition & CallInstruction;
|
||||
|
||||
export type ScriptData = CodeScriptData | CallScriptData;
|
||||
|
||||
export interface ScriptingDefinitionData {
|
||||
readonly language: string;
|
||||
|
||||
@@ -8,7 +8,6 @@ export class Application implements IApplication {
|
||||
public info: IProjectInformation,
|
||||
public collections: readonly ICategoryCollection[],
|
||||
) {
|
||||
validateInformation(info);
|
||||
validateCollections(collections);
|
||||
}
|
||||
|
||||
@@ -16,19 +15,17 @@ export class Application implements IApplication {
|
||||
return this.collections.map((collection) => collection.os);
|
||||
}
|
||||
|
||||
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined {
|
||||
return this.collections.find((collection) => collection.os === operatingSystem);
|
||||
}
|
||||
}
|
||||
|
||||
function validateInformation(info: IProjectInformation) {
|
||||
if (!info) {
|
||||
throw new Error('missing project information');
|
||||
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection {
|
||||
const collection = this.collections.find((c) => c.os === operatingSystem);
|
||||
if (!collection) {
|
||||
throw new Error(`Operating system "${OperatingSystem[operatingSystem]}" is not defined in application`);
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
function validateCollections(collections: readonly ICategoryCollection[]) {
|
||||
if (!collections || !collections.length) {
|
||||
if (!collections.length) {
|
||||
throw new Error('missing collections');
|
||||
}
|
||||
if (collections.filter((c) => !c).length > 0) {
|
||||
|
||||
@@ -3,14 +3,14 @@ import { IScript } from './IScript';
|
||||
import { ICategory } from './ICategory';
|
||||
|
||||
export class Category extends BaseEntity<number> implements ICategory {
|
||||
private allSubScripts: ReadonlyArray<IScript> = undefined;
|
||||
private allSubScripts?: ReadonlyArray<IScript> = undefined;
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
public readonly name: string,
|
||||
public readonly docs: ReadonlyArray<string>,
|
||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
||||
public readonly scripts?: ReadonlyArray<IScript>,
|
||||
public readonly subCategories: ReadonlyArray<ICategory>,
|
||||
public readonly scripts: ReadonlyArray<IScript>,
|
||||
) {
|
||||
super(id);
|
||||
validateCategory(this);
|
||||
@@ -39,10 +39,7 @@ function validateCategory(category: ICategory) {
|
||||
if (!category.name) {
|
||||
throw new Error('missing name');
|
||||
}
|
||||
if (
|
||||
(!category.subCategories || category.subCategories.length === 0)
|
||||
&& (!category.scripts || category.scripts.length === 0)
|
||||
) {
|
||||
if (category.subCategories.length === 0 && category.scripts.length === 0) {
|
||||
throw new Error('A category must have at least one sub-category or script');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,6 @@ export class CategoryCollection implements ICategoryCollection {
|
||||
public readonly actions: ReadonlyArray<ICategory>,
|
||||
public readonly scripting: IScriptingDefinition,
|
||||
) {
|
||||
if (!scripting) {
|
||||
throw new Error('missing scripting definition');
|
||||
}
|
||||
this.queryable = makeQueryable(actions);
|
||||
assertInRange(os, OperatingSystem);
|
||||
ensureValid(this.queryable);
|
||||
@@ -29,17 +26,26 @@ export class CategoryCollection implements ICategoryCollection {
|
||||
ensureNoDuplicates(this.queryable.allScripts);
|
||||
}
|
||||
|
||||
public findCategory(categoryId: number): ICategory | undefined {
|
||||
return this.queryable.allCategories.find((category) => category.id === categoryId);
|
||||
public getCategory(categoryId: number): ICategory {
|
||||
const category = this.queryable.allCategories.find((c) => c.id === categoryId);
|
||||
if (!category) {
|
||||
throw new Error(`Missing category with ID: "${categoryId}"`);
|
||||
}
|
||||
return category;
|
||||
}
|
||||
|
||||
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
||||
assertInRange(level, RecommendationLevel);
|
||||
return this.queryable.scriptsByLevel.get(level);
|
||||
const scripts = this.queryable.scriptsByLevel.get(level);
|
||||
return scripts ?? [];
|
||||
}
|
||||
|
||||
public findScript(scriptId: string): IScript | undefined {
|
||||
return this.queryable.allScripts.find((script) => script.id === scriptId);
|
||||
public getScript(scriptId: string): IScript {
|
||||
const script = this.queryable.allScripts.find((s) => s.id === scriptId);
|
||||
if (!script) {
|
||||
throw new Error(`missing script: ${scriptId}`);
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
public getAllScripts(): IScript[] {
|
||||
@@ -78,13 +84,13 @@ function ensureValid(application: IQueryableCollection) {
|
||||
}
|
||||
|
||||
function ensureValidCategories(allCategories: readonly ICategory[]) {
|
||||
if (!allCategories || allCategories.length === 0) {
|
||||
if (!allCategories.length) {
|
||||
throw new Error('must consist of at least one category');
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidScripts(allScripts: readonly IScript[]) {
|
||||
if (!allScripts || allScripts.length === 0) {
|
||||
if (!allScripts.length) {
|
||||
throw new Error('must consist of at least one script');
|
||||
}
|
||||
const missingRecommendationLevels = getEnumValues(RecommendationLevel)
|
||||
|
||||
@@ -7,5 +7,5 @@ export interface IApplication {
|
||||
readonly collections: readonly ICategoryCollection[];
|
||||
|
||||
getSupportedOsList(): OperatingSystem[];
|
||||
getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined;
|
||||
getCollection(operatingSystem: OperatingSystem): ICategoryCollection;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { IDocumentable } from './IDocumentable';
|
||||
export interface ICategory extends IEntity<number>, IDocumentable {
|
||||
readonly id: number;
|
||||
readonly name: string;
|
||||
readonly subCategories?: ReadonlyArray<ICategory>;
|
||||
readonly scripts?: ReadonlyArray<IScript>;
|
||||
readonly subCategories: ReadonlyArray<ICategory>;
|
||||
readonly scripts: ReadonlyArray<IScript>;
|
||||
includes(script: IScript): boolean;
|
||||
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ export interface ICategoryCollection {
|
||||
readonly actions: ReadonlyArray<ICategory>;
|
||||
|
||||
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
|
||||
findCategory(categoryId: number): ICategory | undefined;
|
||||
findScript(scriptId: string): IScript | undefined;
|
||||
getCategory(categoryId: number): ICategory;
|
||||
getScript(scriptId: string): IScript;
|
||||
getAllScripts(): ReadonlyArray<IScript>;
|
||||
getAllCategories(): ReadonlyArray<ICategory>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface IScriptCode {
|
||||
readonly execute: string;
|
||||
readonly revert: string;
|
||||
readonly revert?: string;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,6 @@ export class ProjectInformation implements IProjectInformation {
|
||||
if (!name) {
|
||||
throw new Error('name is undefined');
|
||||
}
|
||||
if (!version) {
|
||||
throw new Error('undefined version');
|
||||
}
|
||||
if (!slogan) {
|
||||
throw new Error('undefined slogan');
|
||||
}
|
||||
|
||||
@@ -11,9 +11,6 @@ export class Script extends BaseEntity<string> implements IScript {
|
||||
public readonly level?: RecommendationLevel,
|
||||
) {
|
||||
super(name);
|
||||
if (!code) {
|
||||
throw new Error('missing code');
|
||||
}
|
||||
validateLevel(level);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ import { IScriptCode } from './IScriptCode';
|
||||
export class ScriptCode implements IScriptCode {
|
||||
constructor(
|
||||
public readonly execute: string,
|
||||
public readonly revert: string,
|
||||
public readonly revert: string | undefined,
|
||||
) {
|
||||
validateCode(execute);
|
||||
validateRevertCode(revert, execute);
|
||||
}
|
||||
}
|
||||
|
||||
function validateRevertCode(revertCode: string, execute: string) {
|
||||
function validateRevertCode(revertCode: string | undefined, execute: string) {
|
||||
if (!revertCode) {
|
||||
return;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ function validateRevertCode(revertCode: string, execute: string) {
|
||||
}
|
||||
|
||||
function validateCode(code: string): void {
|
||||
if (!code || code.length === 0) {
|
||||
if (code.length === 0) {
|
||||
throw new Error('missing code');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,13 @@ export class CodeRunner {
|
||||
constructor(
|
||||
private readonly system = getWindowInjectedSystemOperations(),
|
||||
private readonly environment = RuntimeEnvironment.CurrentEnvironment,
|
||||
) {
|
||||
if (!system) {
|
||||
throw new Error('missing system operations');
|
||||
}
|
||||
}
|
||||
) { }
|
||||
|
||||
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
||||
const { os } = this.environment;
|
||||
if (os === undefined) {
|
||||
throw new Error('Unidentified operating system');
|
||||
}
|
||||
const dir = this.system.location.combinePaths(
|
||||
this.system.operatingSystem.getTempDirectory(),
|
||||
folderName,
|
||||
|
||||
@@ -2,9 +2,6 @@ import { IEnvironmentVariables } from './IEnvironmentVariables';
|
||||
|
||||
/* Validation is externalized to keep the environment objects simple */
|
||||
export function validateEnvironmentVariables(environment: IEnvironmentVariables): void {
|
||||
if (!environment) {
|
||||
throw new Error('missing environment');
|
||||
}
|
||||
const keyValues = capturePropertyValues(environment);
|
||||
if (!Object.keys(keyValues).length) {
|
||||
throw new Error('Unable to capture key/value pairs');
|
||||
@@ -30,7 +27,7 @@ function getKeysMissingValues(keyValuePairs: Record<string, unknown>): string[]
|
||||
* Necessary because code transformations can make class getters non-enumerable during bundling.
|
||||
* This ensures that even if getters are non-enumerable, their values are still captured and used.
|
||||
*/
|
||||
function capturePropertyValues(instance: unknown): Record<string, unknown> {
|
||||
function capturePropertyValues(instance: object): Record<string, unknown> {
|
||||
const obj: Record<string, unknown> = {};
|
||||
const descriptors = Object.getOwnPropertyDescriptors(instance.constructor.prototype);
|
||||
|
||||
|
||||
@@ -9,12 +9,9 @@ export class EventSubscriptionCollection implements IEventSubscriptionCollection
|
||||
}
|
||||
|
||||
public register(subscriptions: IEventSubscription[]) {
|
||||
if (!subscriptions || subscriptions.length === 0) {
|
||||
if (subscriptions.length === 0) {
|
||||
throw new Error('missing subscriptions');
|
||||
}
|
||||
if (subscriptions.some((subscription) => !subscription)) {
|
||||
throw new Error('missing subscription in list');
|
||||
}
|
||||
this.subscriptions.push(...subscriptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
export class ConsoleLogger implements ILogger {
|
||||
constructor(private readonly globalConsole: Partial<Console> = console) {
|
||||
if (!globalConsole) {
|
||||
constructor(private readonly consoleProxy: Partial<Console> = console) {
|
||||
if (!consoleProxy) { // do not trust strictNullChecks for global objects
|
||||
throw new Error('missing console');
|
||||
}
|
||||
}
|
||||
|
||||
public info(...params: unknown[]): void {
|
||||
this.globalConsole.info(...params);
|
||||
const logFunction = this.consoleProxy?.info;
|
||||
if (!logFunction) {
|
||||
throw new Error('missing "info" function');
|
||||
}
|
||||
logFunction.call(this.consoleProxy, ...params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ export function createElectronLogger(logger: Partial<ElectronLog>): ILogger {
|
||||
throw new Error('missing logger');
|
||||
}
|
||||
return {
|
||||
info: (...params) => logger.info(...params),
|
||||
info: (...params) => {
|
||||
if (!logger.info) {
|
||||
throw new Error('missing "info" function');
|
||||
}
|
||||
logger.info(...params);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { ILogger } from './ILogger';
|
||||
export class WindowInjectedLogger implements ILogger {
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(windowVariables: WindowVariables = window) {
|
||||
if (!windowVariables) {
|
||||
constructor(windowVariables: WindowVariables | undefined | null = window) {
|
||||
if (!windowVariables) { // do not trust strict null checks for global objects
|
||||
throw new Error('missing window');
|
||||
}
|
||||
if (!windowVariables.log) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { IEntity } from '../Entity/IEntity';
|
||||
export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
|
||||
readonly length: number;
|
||||
getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
|
||||
getById(id: TKey): TEntity | undefined;
|
||||
getById(id: TKey): TEntity;
|
||||
addItem(item: TEntity): void;
|
||||
addOrUpdateItem(item: TEntity): void;
|
||||
removeItem(id: TKey): void;
|
||||
|
||||
@@ -6,7 +6,7 @@ implements IRepository<TKey, TEntity> {
|
||||
private readonly items: TEntity[];
|
||||
|
||||
constructor(items?: TEntity[]) {
|
||||
this.items = items || new Array<TEntity>();
|
||||
this.items = items ?? new Array<TEntity>();
|
||||
}
|
||||
|
||||
public get length(): number {
|
||||
@@ -17,18 +17,15 @@ implements IRepository<TKey, TEntity> {
|
||||
return predicate ? this.items.filter(predicate) : this.items;
|
||||
}
|
||||
|
||||
public getById(id: TKey): TEntity | undefined {
|
||||
public getById(id: TKey): TEntity {
|
||||
const items = this.getItems((entity) => entity.id === id);
|
||||
if (!items.length) {
|
||||
return undefined;
|
||||
throw new Error(`missing item: ${id}`);
|
||||
}
|
||||
return items[0];
|
||||
}
|
||||
|
||||
public addItem(item: TEntity): void {
|
||||
if (!item) {
|
||||
throw new Error('missing item');
|
||||
}
|
||||
if (this.exists(item.id)) {
|
||||
throw new Error(`Cannot add (id: ${item.id}) as it is already exists`);
|
||||
}
|
||||
@@ -36,9 +33,6 @@ implements IRepository<TKey, TEntity> {
|
||||
}
|
||||
|
||||
public addOrUpdateItem(item: TEntity): void {
|
||||
if (!item) {
|
||||
throw new Error('missing item');
|
||||
}
|
||||
if (this.exists(item.id)) {
|
||||
this.removeItem(item.id);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ export class DetectorBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
private detect(userAgent: string): OperatingSystem {
|
||||
private detect(userAgent: string): OperatingSystem | undefined {
|
||||
if (!userAgent) {
|
||||
throw new Error('missing userAgent');
|
||||
return undefined;
|
||||
}
|
||||
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
||||
return undefined;
|
||||
|
||||
@@ -37,7 +37,7 @@ export class RuntimeEnvironment implements IRuntimeEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
function getUserAgent(window: Partial<Window>): string {
|
||||
function getUserAgent(window: Partial<Window>): string | undefined {
|
||||
return window?.navigator?.userAgent;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,6 @@ export abstract class FactoryValidator<T> implements ISanityValidator {
|
||||
private readonly factory: FactoryFunction<T>;
|
||||
|
||||
protected constructor(factory: FactoryFunction<T>) {
|
||||
if (!factory) {
|
||||
throw new Error('missing factory');
|
||||
}
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ export function validateRuntimeSanity(
|
||||
options: ISanityCheckOptions,
|
||||
validators: readonly ISanityValidator[] = DefaultSanityValidators,
|
||||
): void {
|
||||
validateContext(options, validators);
|
||||
if (!validators.length) {
|
||||
throw new Error('missing validators');
|
||||
}
|
||||
const errorMessages = validators.reduce((errors, validator) => {
|
||||
if (validator.shouldValidate(options)) {
|
||||
const errorMessage = getErrorMessage(validator);
|
||||
@@ -26,21 +28,6 @@ export function validateRuntimeSanity(
|
||||
}
|
||||
}
|
||||
|
||||
function validateContext(
|
||||
options: ISanityCheckOptions,
|
||||
validators: readonly ISanityValidator[],
|
||||
) {
|
||||
if (!options) {
|
||||
throw new Error('missing options');
|
||||
}
|
||||
if (!validators?.length) {
|
||||
throw new Error('missing validators');
|
||||
}
|
||||
if (validators.some((validator) => !validator)) {
|
||||
throw new Error('missing validator in validators');
|
||||
}
|
||||
}
|
||||
|
||||
function getErrorMessage(validator: ISanityValidator): string | undefined {
|
||||
const errorMessages = [...validator.collectErrors()];
|
||||
if (!errorMessages.length) {
|
||||
|
||||
@@ -6,17 +6,21 @@ export enum FileType {
|
||||
}
|
||||
|
||||
export class SaveFileDialog {
|
||||
public static saveFile(text: string, fileName: string, type: FileType): void {
|
||||
const mimeType = this.mimeTypes.get(type);
|
||||
public static saveFile(
|
||||
text: string,
|
||||
fileName: string,
|
||||
type: FileType,
|
||||
): void {
|
||||
const mimeType = this.mimeTypes[type];
|
||||
this.saveBlob(text, mimeType, fileName);
|
||||
}
|
||||
|
||||
private static readonly mimeTypes = new Map<FileType, string>([
|
||||
private static readonly mimeTypes: Record<FileType, string> = {
|
||||
// Some browsers (including firefox + IE) require right mime type
|
||||
// otherwise they ignore extension and save the file as text.
|
||||
[FileType.BatchFile, 'application/bat'], // https://en.wikipedia.org/wiki/Batch_file
|
||||
[FileType.ShellScript, 'text/x-shellscript'], // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
|
||||
]);
|
||||
[FileType.BatchFile]: 'application/bat', // https://en.wikipedia.org/wiki/Batch_file
|
||||
[FileType.ShellScript]: 'text/x-shellscript', // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
|
||||
};
|
||||
|
||||
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
|
||||
try {
|
||||
|
||||
@@ -19,6 +19,6 @@ export interface ICommandOps {
|
||||
|
||||
export interface IFileSystemOps {
|
||||
setFilePermissions(filePath: string, mode: string | number): Promise<void>;
|
||||
createDirectory(directoryPath: string, isRecursive?: boolean): Promise<string>;
|
||||
createDirectory(directoryPath: string, isRecursive?: boolean): Promise<void>;
|
||||
writeToFile(filePath: string, data: string): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -17,10 +17,16 @@ export function createNodeSystemOperations(): ISystemOperations {
|
||||
filePath: string,
|
||||
mode: string | number,
|
||||
) => chmod(filePath, mode),
|
||||
createDirectory: (
|
||||
createDirectory: async (
|
||||
directoryPath: string,
|
||||
isRecursive?: boolean,
|
||||
) => mkdir(directoryPath, { recursive: isRecursive }),
|
||||
) => {
|
||||
await mkdir(directoryPath, { recursive: isRecursive });
|
||||
// Ignoring the return value from `mkdir`, which is the first directory created
|
||||
// when `recursive` is true. The function contract is to not return any value,
|
||||
// and we avoid handling this inconsistent behavior.
|
||||
// See https://github.com/nodejs/node/pull/31530
|
||||
},
|
||||
writeToFile: (
|
||||
filePath: string,
|
||||
data: string,
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { EventSource } from '../Events/EventSource';
|
||||
|
||||
export class AsyncLazy<T> {
|
||||
private valueCreated = new EventSource();
|
||||
private valueCreated = new EventSource<T>();
|
||||
|
||||
private isValueCreated = false;
|
||||
|
||||
private isCreatingValue = false;
|
||||
|
||||
private value: T | undefined;
|
||||
private state: ValueState<T> = { status: ValueStatus.NotRequested };
|
||||
|
||||
constructor(private valueFactory: () => Promise<T>) {}
|
||||
|
||||
@@ -15,23 +11,44 @@ export class AsyncLazy<T> {
|
||||
this.valueFactory = valueFactory;
|
||||
}
|
||||
|
||||
public async getValue(): Promise<T> {
|
||||
// If value is already created, return the value directly
|
||||
if (this.isValueCreated) {
|
||||
return Promise.resolve(this.value);
|
||||
public getValue(): Promise<T> {
|
||||
if (this.state.status === ValueStatus.Created) {
|
||||
return Promise.resolve(this.state.value);
|
||||
}
|
||||
// If value is being created, wait until the value is created and then return it.
|
||||
if (this.isCreatingValue) {
|
||||
return new Promise<T>((resolve) => {
|
||||
// Return/result when valueCreated event is triggered.
|
||||
this.valueCreated.on(() => resolve(this.value));
|
||||
});
|
||||
if (this.state.status === ValueStatus.BeingCreated) {
|
||||
return this.state.value;
|
||||
}
|
||||
this.isCreatingValue = true;
|
||||
this.value = await this.valueFactory();
|
||||
this.isCreatingValue = false;
|
||||
this.isValueCreated = true;
|
||||
this.valueCreated.notify(null);
|
||||
return Promise.resolve(this.value);
|
||||
const valuePromise = this.valueFactory();
|
||||
this.state = {
|
||||
status: ValueStatus.BeingCreated,
|
||||
value: valuePromise,
|
||||
};
|
||||
valuePromise.then((value) => {
|
||||
this.state = {
|
||||
status: ValueStatus.Created,
|
||||
value,
|
||||
};
|
||||
this.valueCreated.notify(value);
|
||||
});
|
||||
return valuePromise;
|
||||
}
|
||||
}
|
||||
|
||||
enum ValueStatus {
|
||||
NotRequested,
|
||||
BeingCreated,
|
||||
Created,
|
||||
}
|
||||
|
||||
type ValueState<T> =
|
||||
| {
|
||||
readonly status: ValueStatus.NotRequested;
|
||||
}
|
||||
| {
|
||||
readonly status: ValueStatus.BeingCreated;
|
||||
readonly value: Promise<T>;
|
||||
}
|
||||
| {
|
||||
readonly status: ValueStatus.Created;
|
||||
readonly value: T
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@ import { ILogger } from '@/infrastructure/Log/ILogger';
|
||||
|
||||
/* Primary entry point for platform-specific injections */
|
||||
export interface WindowVariables {
|
||||
readonly system: ISystemOperations;
|
||||
readonly isDesktop: boolean;
|
||||
readonly os: OperatingSystem;
|
||||
readonly log: ILogger;
|
||||
readonly isDesktop?: boolean;
|
||||
readonly system?: ISystemOperations;
|
||||
readonly os?: OperatingSystem;
|
||||
readonly log?: ILogger;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function validateWindowVariables(variables: Partial<WindowVariables>) {
|
||||
|
||||
function* testEveryProperty(variables: Partial<WindowVariables>): Iterable<string> {
|
||||
const tests: {
|
||||
[K in PropertyKeys<WindowVariables>]: boolean;
|
||||
[K in PropertyKeys<Required<WindowVariables>>]: boolean;
|
||||
} = {
|
||||
os: testOperatingSystem(variables.os),
|
||||
isDesktop: testIsDesktop(variables.isDesktop),
|
||||
|
||||
@@ -7,22 +7,18 @@
|
||||
<TheCodeButtons class="app__row app__code-buttons" />
|
||||
<TheFooter />
|
||||
</div>
|
||||
<OptionalDevToolkit />
|
||||
<component v-if="devToolkitComponent" :is="devToolkitComponent" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { defineAsyncComponent, defineComponent, Component } from 'vue';
|
||||
import TheHeader from '@/presentation/components/TheHeader.vue';
|
||||
import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue';
|
||||
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
|
||||
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
||||
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
||||
|
||||
const OptionalDevToolkit = process.env.NODE_ENV !== 'production'
|
||||
? defineAsyncComponent(() => import('@/presentation/components/DevToolkit/DevToolkit.vue'))
|
||||
: null;
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
TheHeader,
|
||||
@@ -30,10 +26,23 @@ export default defineComponent({
|
||||
TheScriptArea,
|
||||
TheSearchBar,
|
||||
TheFooter,
|
||||
OptionalDevToolkit,
|
||||
},
|
||||
setup() { },
|
||||
setup() {
|
||||
const devToolkitComponent = getOptionalDevToolkitComponent();
|
||||
|
||||
return {
|
||||
devToolkitComponent,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function getOptionalDevToolkitComponent(): Component | undefined {
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
if (!isDevelopment) {
|
||||
return undefined;
|
||||
}
|
||||
return defineAsyncComponent(() => import('@/presentation/components/DevToolkit/DevToolkit.vue'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -40,7 +40,7 @@ export default defineComponent({
|
||||
function getCanRunState(
|
||||
selectedOs: OperatingSystem,
|
||||
isDesktopVersion: boolean,
|
||||
hostOs: OperatingSystem,
|
||||
hostOs: OperatingSystem | undefined,
|
||||
): boolean {
|
||||
const isRunningOnSelectedOs = selectedOs === hostOs;
|
||||
return isDesktopVersion && isRunningOnSelectedOs;
|
||||
|
||||
@@ -21,11 +21,10 @@ import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue'
|
||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import IconButton from '../IconButton.vue';
|
||||
import InstructionList from './Instructions/InstructionList.vue';
|
||||
import { IInstructionListData } from './Instructions/InstructionListData';
|
||||
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
|
||||
import { getInstructions } from './Instructions/InstructionListDataFactory';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -39,7 +38,7 @@ export default defineComponent({
|
||||
|
||||
const areInstructionsVisible = ref(false);
|
||||
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
||||
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
||||
const instructions = computed<IInstructionListData | undefined>(() => getInstructions(
|
||||
currentState.value.collection.os,
|
||||
fileName.value,
|
||||
));
|
||||
@@ -59,16 +58,6 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
|
||||
function getDownloadInstructions(
|
||||
os: OperatingSystem,
|
||||
fileName: string,
|
||||
): IInstructionListData | undefined {
|
||||
if (!hasInstructions(os)) {
|
||||
return undefined;
|
||||
}
|
||||
return getInstructions(os, fileName);
|
||||
}
|
||||
|
||||
function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) {
|
||||
const content = state.code.current;
|
||||
const type = getType(state.collection.scripting.language);
|
||||
|
||||
@@ -15,13 +15,11 @@ export class InstructionsBuilder {
|
||||
}
|
||||
|
||||
public withStep(stepBuilder: InstructionStepBuilderType) {
|
||||
if (!stepBuilder) { throw new Error('missing stepBuilder'); }
|
||||
this.stepBuilders.push(stepBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(data: IInstructionsBuilderData): IInstructionListData {
|
||||
if (!data) { throw new Error('missing data'); }
|
||||
return {
|
||||
operatingSystem: this.os,
|
||||
steps: this.stepBuilders.map((stepBuilder) => stepBuilder(data)),
|
||||
|
||||
@@ -86,12 +86,9 @@ export default defineComponent({
|
||||
() => info.getDownloadUrl(OperatingSystem.macOS),
|
||||
);
|
||||
|
||||
const osName = computed<string>(() => {
|
||||
if (!props.data) {
|
||||
throw new Error('missing data');
|
||||
}
|
||||
return renderOsName(props.data.operatingSystem);
|
||||
});
|
||||
const osName = computed<string>(
|
||||
() => renderOsName(props.data.operatingSystem),
|
||||
);
|
||||
|
||||
return {
|
||||
appName,
|
||||
|
||||
@@ -9,15 +9,11 @@ const builders = new Map<OperatingSystem, InstructionsBuilder>([
|
||||
[OperatingSystem.Linux, new LinuxInstructionsBuilder()],
|
||||
]);
|
||||
|
||||
export function hasInstructions(os: OperatingSystem) {
|
||||
return builders.has(os);
|
||||
}
|
||||
|
||||
export function getInstructions(
|
||||
os: OperatingSystem,
|
||||
fileName: string,
|
||||
): IInstructionListData {
|
||||
): IInstructionListData | undefined {
|
||||
return builders
|
||||
.get(os)
|
||||
.build({ fileName });
|
||||
?.build({ fileName });
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export default defineComponent({
|
||||
|
||||
function updateCode(code: string, language: ScriptingLanguage) {
|
||||
const innerCode = code || getDefaultCode(language);
|
||||
editor.setValue(innerCode, 1);
|
||||
editor?.setValue(innerCode, 1);
|
||||
}
|
||||
|
||||
function handleCodeChange(event: ICodeChangedEvent) {
|
||||
@@ -96,7 +96,7 @@ export default defineComponent({
|
||||
if (!currentMarkerId) {
|
||||
return;
|
||||
}
|
||||
editor.session.removeMarker(currentMarkerId);
|
||||
editor?.session.removeMarker(currentMarkerId);
|
||||
currentMarkerId = undefined;
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ export default defineComponent({
|
||||
|
||||
function highlight(startRow: number, endRow: number) {
|
||||
const AceRange = ace.require('ace/range').Range;
|
||||
currentMarkerId = editor.session.addMarker(
|
||||
currentMarkerId = editor?.session.addMarker(
|
||||
new AceRange(startRow, 0, endRow, 0),
|
||||
'code-area__highlight',
|
||||
'fullLine',
|
||||
@@ -123,8 +123,11 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function scrollToLine(row: number) {
|
||||
const column = editor.session.getLine(row).length;
|
||||
editor.gotoLine(row, column, true);
|
||||
const column = editor?.session.getLine(row).length;
|
||||
if (column === undefined) {
|
||||
return;
|
||||
}
|
||||
editor?.gotoLine(row, column, true);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -15,9 +15,12 @@ export enum SelectionType {
|
||||
|
||||
export function setCurrentSelectionType(type: SelectionType, context: SelectionMutationContext) {
|
||||
if (type === SelectionType.Custom) {
|
||||
throw new Error('cannot select custom type');
|
||||
throw new Error('Cannot select custom type.');
|
||||
}
|
||||
const selector = selectors.get(type);
|
||||
if (!selector) {
|
||||
throw new Error(`Cannot handle the type: ${SelectionType[type]}`);
|
||||
}
|
||||
selector.select(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,14 +31,14 @@ export default defineComponent({
|
||||
const { modifyCurrentContext, currentState } = injectKey((keys) => keys.useCollectionState);
|
||||
const { application } = injectKey((keys) => keys.useApplication);
|
||||
|
||||
const allOses = computed<ReadonlyArray<IOsViewModel>>(() => (
|
||||
application.getSupportedOsList() ?? [])
|
||||
.map((os) : IOsViewModel => (
|
||||
{
|
||||
const allOses = computed<ReadonlyArray<IOsViewModel>>(
|
||||
() => application
|
||||
.getSupportedOsList()
|
||||
.map((os) : IOsViewModel => ({
|
||||
os,
|
||||
name: renderOsName(os),
|
||||
}
|
||||
)));
|
||||
})),
|
||||
);
|
||||
|
||||
const currentOs = computed<OperatingSystem>(() => {
|
||||
return currentState.value.os;
|
||||
|
||||
@@ -48,8 +48,12 @@ export default defineComponent({
|
||||
const firstElement = shallowRef<HTMLElement>();
|
||||
|
||||
function onResize(displacementX: number): void {
|
||||
const leftWidth = firstElement.value.offsetWidth + displacementX;
|
||||
firstElement.value.style.width = `${leftWidth}px`;
|
||||
const element = firstElement.value;
|
||||
if (!element) {
|
||||
throw new Error('The element reference ref is not correctly assigned to a DOM element.');
|
||||
}
|
||||
const leftWidth = element.offsetWidth + displacementX;
|
||||
element.style.width = `${leftWidth}px`;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -29,7 +29,10 @@ export default defineComponent({
|
||||
const cursorCssValue = 'ew-resize';
|
||||
let initialX: number | undefined;
|
||||
|
||||
const resize = (event) => {
|
||||
const resize = (event: MouseEvent) => {
|
||||
if (initialX === undefined) {
|
||||
throw new Error('Resize action started without an initial X coordinate.');
|
||||
}
|
||||
const displacementX = event.clientX - initialX;
|
||||
emit('resized', displacementX);
|
||||
initialX = event.clientX;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
-->
|
||||
<div
|
||||
v-if="categoryIds != null && categoryIds.length > 0"
|
||||
v-if="categoryIds.length > 0"
|
||||
class="cards"
|
||||
>
|
||||
<CardListItem
|
||||
@@ -50,8 +50,9 @@ export default defineComponent({
|
||||
const { currentState, onStateChange } = injectKey((keys) => keys.useCollectionState);
|
||||
|
||||
const width = ref<number>(0);
|
||||
const categoryIds = computed<ReadonlyArray<number>>(() => currentState
|
||||
.value.collection.actions.map((category) => category.id));
|
||||
const categoryIds = computed<readonly number[]>(
|
||||
() => currentState.value.collection.actions.map((category) => category.id),
|
||||
);
|
||||
const activeCategoryId = ref<number | undefined>(undefined);
|
||||
|
||||
function onSelected(categoryId: number, isExpanded: boolean) {
|
||||
|
||||
@@ -89,12 +89,9 @@ export default defineComponent({
|
||||
|
||||
const cardElement = shallowRef<HTMLElement>();
|
||||
|
||||
const cardTitle = computed<string | undefined>(() => {
|
||||
if (!props.categoryId || !currentState.value) {
|
||||
return undefined;
|
||||
}
|
||||
const category = currentState.value.collection.findCategory(props.categoryId);
|
||||
return category?.name;
|
||||
const cardTitle = computed<string>(() => {
|
||||
const category = currentState.value.collection.getCategory(props.categoryId);
|
||||
return category.name;
|
||||
});
|
||||
|
||||
function collapse() {
|
||||
@@ -102,8 +99,12 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
async function scrollToCard() {
|
||||
const card = cardElement.value;
|
||||
if (!card) {
|
||||
throw new Error('Card is not found');
|
||||
}
|
||||
await sleep(400); // wait a bit to allow GUI to render the expanded card
|
||||
cardElement.value.scrollIntoView({ behavior: 'smooth' });
|
||||
card.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -34,7 +34,7 @@ export default defineComponent({
|
||||
const currentCollection = computed<ICategoryCollection>(() => currentState.value.collection);
|
||||
|
||||
const currentCategory = computed<ICategory>(
|
||||
() => currentCollection.value.findCategory(props.categoryId),
|
||||
() => currentCollection.value.getCategory(props.categoryId),
|
||||
);
|
||||
|
||||
const isAnyChildSelected = computed<boolean>(
|
||||
|
||||
@@ -71,6 +71,9 @@ export default defineComponent({
|
||||
const searchHasMatches = ref(false);
|
||||
const trimmedSearchQuery = computed(() => {
|
||||
const query = searchQuery.value;
|
||||
if (!query) {
|
||||
return '';
|
||||
}
|
||||
const threshold = 30;
|
||||
if (query.length <= threshold - 3) {
|
||||
return query;
|
||||
@@ -94,7 +97,7 @@ export default defineComponent({
|
||||
|
||||
function updateFromInitialFilter(filter?: IFilterResult) {
|
||||
searchQuery.value = filter?.query;
|
||||
searchHasMatches.value = filter?.hasAnyMatches();
|
||||
searchHasMatches.value = filter?.hasAnyMatches() ?? false;
|
||||
}
|
||||
|
||||
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {
|
||||
|
||||
@@ -42,7 +42,7 @@ function renderText(docs: readonly string[] | undefined): string {
|
||||
}
|
||||
|
||||
function renderAsMarkdownListItem(content: string): string {
|
||||
if (!content || content.length === 0) {
|
||||
if (content.length === 0) {
|
||||
throw new Error('missing content');
|
||||
}
|
||||
const lines = content.split(/\r\n|\r|\n/);
|
||||
|
||||
@@ -85,7 +85,8 @@ function removeTrailingExtension(value: string): string {
|
||||
if (parts.length === 1) {
|
||||
return value;
|
||||
}
|
||||
if (parts.at(-1).length > 9) {
|
||||
const extension = parts[parts.length - 1];
|
||||
if (extension.length > 9) {
|
||||
return value; // Heuristically web file extension is no longer than 9 chars (e.g. "html")
|
||||
}
|
||||
return parts.slice(0, -1).join('.');
|
||||
@@ -115,9 +116,8 @@ function selectMostDescriptiveName(parts: readonly string[]): string | undefined
|
||||
}
|
||||
|
||||
function isGoodPathPart(part: string): boolean {
|
||||
return part
|
||||
return part.length > 2 // This is often non-human categories like T5 etc.
|
||||
&& !isDigit(part) // E.g. article numbers, issue numbers
|
||||
&& part.length > 2 // This is often non-human categories like T5 etc.
|
||||
&& !/^index(?:\.\S{0,10}$|$)/.test(part) // E.g. index.html
|
||||
&& !/^[A-Za-z]{2,4}([_-][A-Za-z]{4})?([_-]([A-Za-z]{2}|[0-9]{3}))$/.test(part) // Locale string e.g. fr-FR
|
||||
&& !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(part) // GUID
|
||||
|
||||
@@ -8,6 +8,6 @@ export interface NodeMetadata {
|
||||
readonly text: string;
|
||||
readonly isReversible: boolean;
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
readonly children?: ReadonlyArray<NodeMetadata>;
|
||||
readonly children: ReadonlyArray<NodeMetadata>;
|
||||
readonly type: NodeType;
|
||||
}
|
||||
|
||||
@@ -25,10 +25,7 @@ export class CategoryReverter implements IReverter {
|
||||
}
|
||||
|
||||
function getAllSubScriptReverters(categoryId: number, collection: ICategoryCollection) {
|
||||
const category = collection.findCategory(categoryId);
|
||||
if (!category) {
|
||||
throw new Error(`Category with id "${categoryId}" does not exist`);
|
||||
}
|
||||
const category = collection.getCategory(categoryId);
|
||||
const scripts = category.getAllScriptsRecursively();
|
||||
return scripts.map((script) => new ScriptReverter(script.id));
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import { useSelectedScriptNodeIds } from './TreeViewAdapter/UseSelectedScriptNod
|
||||
export default defineComponent({
|
||||
props: {
|
||||
categoryId: {
|
||||
type: [Number, undefined],
|
||||
type: [Number],
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import type { ReadOnlyTreeNode } from '../Node/TreeNode';
|
||||
|
||||
export interface TreeViewFilterEvent {
|
||||
readonly action: TreeViewFilterAction;
|
||||
readonly predicate?: TreeViewFilterPredicate;
|
||||
}
|
||||
type TreeViewFilterTriggeredEvent = {
|
||||
readonly action: TreeViewFilterAction.Triggered;
|
||||
readonly predicate: TreeViewFilterPredicate;
|
||||
};
|
||||
|
||||
type TreeViewFilterRemovedEvent = {
|
||||
readonly action: TreeViewFilterAction.Removed;
|
||||
};
|
||||
|
||||
export type TreeViewFilterEvent = TreeViewFilterTriggeredEvent | TreeViewFilterRemovedEvent;
|
||||
|
||||
export enum TreeViewFilterAction {
|
||||
Triggered,
|
||||
@@ -14,14 +20,14 @@ export type TreeViewFilterPredicate = (node: ReadOnlyTreeNode) => boolean;
|
||||
|
||||
export function createFilterTriggeredEvent(
|
||||
predicate: TreeViewFilterPredicate,
|
||||
): TreeViewFilterEvent {
|
||||
): TreeViewFilterTriggeredEvent {
|
||||
return {
|
||||
action: TreeViewFilterAction.Triggered,
|
||||
predicate,
|
||||
};
|
||||
}
|
||||
|
||||
export function createFilterRemovedEvent(): TreeViewFilterEvent {
|
||||
export function createFilterRemovedEvent(): TreeViewFilterRemovedEvent {
|
||||
return {
|
||||
action: TreeViewFilterAction.Removed,
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export class TreeNodeHierarchy implements HierarchyAccess {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
public setParent(parent: TreeNode): void {
|
||||
public setParent(parent: TreeNode | undefined): void {
|
||||
this.parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user