diff --git a/packages/apps/deno-runtime/acorn-walk.d.ts b/packages/apps/deno-runtime/acorn-walk.d.ts deleted file mode 100644 index 49f3d6e33df3a..0000000000000 --- a/packages/apps/deno-runtime/acorn-walk.d.ts +++ /dev/null @@ -1,175 +0,0 @@ -import type acorn from './acorn.d'; - -export type FullWalkerCallback = ( - node: acorn.AnyNode, - state: TState, - type: string, -) => void; - -export type FullAncestorWalkerCallback = ( - node: acorn.AnyNode, - state: TState, - ancestors: acorn.AnyNode[], - type: string, -) => void; - -type AggregateType = { - Expression: acorn.Expression; - Statement: acorn.Statement; - Pattern: acorn.Pattern; - ForInit: acorn.VariableDeclaration | acorn.Expression; -}; - -export type SimpleVisitors = - & { - [type in acorn.AnyNode['type']]?: (node: Extract, state: TState) => void; - } - & { - [type in keyof AggregateType]?: (node: AggregateType[type], state: TState) => void; - }; - -export type AncestorVisitors = - & { - [type in acorn.AnyNode['type']]?: (node: Extract, state: TState, ancestors: acorn.Node[]) => void; - } - & { - [type in keyof AggregateType]?: (node: AggregateType[type], state: TState, ancestors: acorn.Node[]) => void; - }; - -export type WalkerCallback = (node: acorn.Node, state: TState) => void; - -export type RecursiveVisitors = - & { - [type in acorn.AnyNode['type']]?: (node: Extract, state: TState, callback: WalkerCallback) => void; - } - & { - [type in keyof AggregateType]?: (node: AggregateType[type], state: TState, callback: WalkerCallback) => void; - }; - -export type FindPredicate = (type: string, node: acorn.Node) => boolean; - -export interface Found { - node: acorn.Node; - state: TState; -} - -/** - * does a 'simple' walk over a tree - * @param node the AST node to walk - * @param visitors an object with properties whose names correspond to node types in the {@link https://github.com/estree/estree | ESTree spec}. The properties should contain functions that will be called with the node object and, if applicable the state at that point. - * @param base a walker algorithm - * @param state a start state. The default walker will simply visit all statements and expressions and not produce a meaningful state. (An example of a use of state is to track scope at each point in the tree.) - */ -export function simple( - node: acorn.Node, - visitors: SimpleVisitors, - base?: RecursiveVisitors, - state?: TState, -): void; - -/** - * does a 'simple' walk over a tree, building up an array of ancestor nodes (including the current node) and passing the array to the callbacks as a third parameter. - * @param node - * @param visitors - * @param base - * @param state - */ -export function ancestor( - node: acorn.Node, - visitors: AncestorVisitors, - base?: RecursiveVisitors, - state?: TState, -): void; - -/** - * does a 'recursive' walk, where the walker functions are responsible for continuing the walk on the child nodes of their target node. - * @param node - * @param state the start state - * @param functions contain an object that maps node types to walker functions - * @param base provides the fallback walker functions for node types that aren't handled in the {@link functions} object. If not given, the default walkers will be used. - */ -export function recursive( - node: acorn.Node, - state: TState, - functions: RecursiveVisitors, - base?: RecursiveVisitors, -): void; - -/** - * does a 'full' walk over a tree, calling the {@link callback} with the arguments (node, state, type) for each node - * @param node - * @param callback - * @param base - * @param state - */ -export function full( - node: acorn.Node, - callback: FullWalkerCallback, - base?: RecursiveVisitors, - state?: TState, -): void; - -/** - * does a 'full' walk over a tree, building up an array of ancestor nodes (including the current node) and passing the array to the callbacks as a third parameter. - * @param node - * @param callback - * @param base - * @param state - */ -export function fullAncestor( - node: acorn.AnyNode, - callback: FullAncestorWalkerCallback, - base?: RecursiveVisitors, - state?: TState, -): void; - -/** - * builds a new walker object by using the walker functions in {@link functions} and filling in the missing ones by taking defaults from {@link base}. - * @param functions - * @param base - */ -export function make( - functions: RecursiveVisitors, - base?: RecursiveVisitors, -): RecursiveVisitors; - -/** - * tries to locate a node in a tree at the given start and/or end offsets, which satisfies the predicate test. {@link start} and {@link end} can be either `null` (as wildcard) or a `number`. {@link test} may be a string (indicating a node type) or a function that takes (nodeType, node) arguments and returns a boolean indicating whether this node is interesting. {@link base} and {@link state} are optional, and can be used to specify a custom walker. Nodes are tested from inner to outer, so if two nodes match the boundaries, the inner one will be preferred. - * @param node - * @param start - * @param end - * @param type - * @param base - * @param state - */ -export function findNodeAt( - node: acorn.AnyNode, - start: number | undefined, - end?: number | undefined, - type?: FindPredicate | string, - base?: RecursiveVisitors, - state?: TState, -): Found | undefined; - -/** - * like {@link findNodeAt}, but will match any node that exists 'around' (spanning) the given position. - * @param node - * @param start - * @param type - * @param base - * @param state - */ -export function findNodeAround( - node: acorn.AnyNode, - start: number | undefined, - type?: FindPredicate | string, - base?: RecursiveVisitors, - state?: TState, -): Found | undefined; - -/** - * similar to {@link findNodeAround}, but will match all nodes after the given position (testing outer nodes before inner nodes). - */ -export const findNodeAfter: typeof findNodeAround; - -export const base: RecursiveVisitors; diff --git a/packages/apps/deno-runtime/acorn.d.ts b/packages/apps/deno-runtime/acorn.d.ts deleted file mode 100644 index 7ba007d999b11..0000000000000 --- a/packages/apps/deno-runtime/acorn.d.ts +++ /dev/null @@ -1,915 +0,0 @@ -export interface Node { - start?: number; - end?: number; - type: string; - range?: [number, number]; - loc?: SourceLocation | null; -} - -export interface SourceLocation { - source?: string | null; - start: Position; - end: Position; -} - -export interface Position { - /** 1-based */ - line: number; - /** 0-based */ - column: number; -} - -export interface Identifier extends Node { - type: 'Identifier'; - name: string; -} - -export interface Literal extends Node { - type: 'Literal'; - value?: string | boolean | null | number | RegExp | bigint; - raw?: string; - regex?: { - pattern: string; - flags: string; - }; - bigint?: string; -} - -export interface Program extends Node { - type: 'Program'; - body: Array; - sourceType: 'script' | 'module'; -} - -export interface Function extends Node { - id?: Identifier | null; - params: Array; - body: BlockStatement | Expression; - generator: boolean; - expression: boolean; - async: boolean; -} - -export interface ExpressionStatement extends Node { - type: 'ExpressionStatement'; - expression: Expression | Literal; - directive?: string; -} - -export interface BlockStatement extends Node { - type: 'BlockStatement'; - body: Array; -} - -export interface EmptyStatement extends Node { - type: 'EmptyStatement'; -} - -export interface DebuggerStatement extends Node { - type: 'DebuggerStatement'; -} - -export interface WithStatement extends Node { - type: 'WithStatement'; - object: Expression; - body: Statement; -} - -export interface ReturnStatement extends Node { - type: 'ReturnStatement'; - argument?: Expression | null; -} - -export interface LabeledStatement extends Node { - type: 'LabeledStatement'; - label: Identifier; - body: Statement; -} - -export interface BreakStatement extends Node { - type: 'BreakStatement'; - label?: Identifier | null; -} - -export interface ContinueStatement extends Node { - type: 'ContinueStatement'; - label?: Identifier | null; -} - -export interface IfStatement extends Node { - type: 'IfStatement'; - test: Expression; - consequent: Statement; - alternate?: Statement | null; -} - -export interface SwitchStatement extends Node { - type: 'SwitchStatement'; - discriminant: Expression; - cases: Array; -} - -export interface SwitchCase extends Node { - type: 'SwitchCase'; - test?: Expression | null; - consequent: Array; -} - -export interface ThrowStatement extends Node { - type: 'ThrowStatement'; - argument: Expression; -} - -export interface TryStatement extends Node { - type: 'TryStatement'; - block: BlockStatement; - handler?: CatchClause | null; - finalizer?: BlockStatement | null; -} - -export interface CatchClause extends Node { - type: 'CatchClause'; - param?: Pattern | null; - body: BlockStatement; -} - -export interface WhileStatement extends Node { - type: 'WhileStatement'; - test: Expression; - body: Statement; -} - -export interface DoWhileStatement extends Node { - type: 'DoWhileStatement'; - body: Statement; - test: Expression; -} - -export interface ForStatement extends Node { - type: 'ForStatement'; - init?: VariableDeclaration | Expression | null; - test?: Expression | null; - update?: Expression | null; - body: Statement; -} - -export interface ForInStatement extends Node { - type: 'ForInStatement'; - left: VariableDeclaration | Pattern; - right: Expression; - body: Statement; -} - -export interface FunctionDeclaration extends Function { - type: 'FunctionDeclaration'; - id: Identifier; - body: BlockStatement; -} - -export interface VariableDeclaration extends Node { - type: 'VariableDeclaration'; - declarations: Array; - kind: 'var' | 'let' | 'const'; -} - -export interface VariableDeclarator extends Node { - type: 'VariableDeclarator'; - id: Pattern; - init?: Expression | null; -} - -export interface ThisExpression extends Node { - type: 'ThisExpression'; -} - -export interface ArrayExpression extends Node { - type: 'ArrayExpression'; - elements: Array; -} - -export interface ObjectExpression extends Node { - type: 'ObjectExpression'; - properties: Array; -} - -export interface Property extends Node { - type: 'Property'; - key: Expression; - value: Expression; - kind: 'init' | 'get' | 'set'; - method: boolean; - shorthand: boolean; - computed: boolean; -} - -export interface FunctionExpression extends Function { - type: 'FunctionExpression'; - body: BlockStatement; -} - -export interface UnaryExpression extends Node { - type: 'UnaryExpression'; - operator: UnaryOperator; - prefix: boolean; - argument: Expression; -} - -export type UnaryOperator = '-' | '+' | '!' | '~' | 'typeof' | 'void' | 'delete'; - -export interface UpdateExpression extends Node { - type: 'UpdateExpression'; - operator: UpdateOperator; - argument: Expression; - prefix: boolean; -} - -export type UpdateOperator = '++' | '--'; - -export interface BinaryExpression extends Node { - type: 'BinaryExpression'; - operator: BinaryOperator; - left: Expression | PrivateIdentifier; - right: Expression; -} - -export type BinaryOperator = - | '==' - | '!=' - | '===' - | '!==' - | '<' - | '<=' - | '>' - | '>=' - | '<<' - | '>>' - | '>>>' - | '+' - | '-' - | '*' - | '/' - | '%' - | '|' - | '^' - | '&' - | 'in' - | 'instanceof' - | '**'; - -export interface AssignmentExpression extends Node { - type: 'AssignmentExpression'; - operator: AssignmentOperator; - left: Pattern; - right: Expression; -} - -export type AssignmentOperator = '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '<<=' | '>>=' | '>>>=' | '|=' | '^=' | '&=' | '**=' | '||=' | '&&=' | '??='; - -export interface LogicalExpression extends Node { - type: 'LogicalExpression'; - operator: LogicalOperator; - left: Expression; - right: Expression; -} - -export type LogicalOperator = '||' | '&&' | '??'; - -export interface MemberExpression extends Node { - type: 'MemberExpression'; - object: Expression | Super; - property: Expression | PrivateIdentifier; - computed: boolean; - optional: boolean; -} - -export interface ConditionalExpression extends Node { - type: 'ConditionalExpression'; - test: Expression; - alternate: Expression; - consequent: Expression; -} - -export interface CallExpression extends Node { - type: 'CallExpression'; - callee: Expression | Super; - arguments: Array; - optional: boolean; -} - -export interface NewExpression extends Node { - type: 'NewExpression'; - callee: Expression; - arguments: Array; -} - -export interface SequenceExpression extends Node { - type: 'SequenceExpression'; - expressions: Array; -} - -export interface ForOfStatement extends Node { - type: 'ForOfStatement'; - left: VariableDeclaration | Pattern; - right: Expression; - body: Statement; - await: boolean; -} - -export interface Super extends Node { - type: 'Super'; -} - -export interface SpreadElement extends Node { - type: 'SpreadElement'; - argument: Expression; -} - -export interface ArrowFunctionExpression extends Function { - type: 'ArrowFunctionExpression'; -} - -export interface YieldExpression extends Node { - type: 'YieldExpression'; - argument?: Expression | null; - delegate: boolean; -} - -export interface TemplateLiteral extends Node { - type: 'TemplateLiteral'; - quasis: Array; - expressions: Array; -} - -export interface TaggedTemplateExpression extends Node { - type: 'TaggedTemplateExpression'; - tag: Expression; - quasi: TemplateLiteral; -} - -export interface TemplateElement extends Node { - type: 'TemplateElement'; - tail: boolean; - value: { - cooked?: string | null; - raw: string; - }; -} - -export interface AssignmentProperty extends Node { - type: 'Property'; - key: Expression; - value: Pattern; - kind: 'init'; - method: false; - shorthand: boolean; - computed: boolean; -} - -export interface ObjectPattern extends Node { - type: 'ObjectPattern'; - properties: Array; -} - -export interface ArrayPattern extends Node { - type: 'ArrayPattern'; - elements: Array; -} - -export interface RestElement extends Node { - type: 'RestElement'; - argument: Pattern; -} - -export interface AssignmentPattern extends Node { - type: 'AssignmentPattern'; - left: Pattern; - right: Expression; -} - -export interface Class extends Node { - id?: Identifier | null; - superClass?: Expression | null; - body: ClassBody; -} - -export interface ClassBody extends Node { - type: 'ClassBody'; - body: Array; -} - -export interface MethodDefinition extends Node { - type: 'MethodDefinition'; - key: Expression | PrivateIdentifier; - value: FunctionExpression; - kind: 'constructor' | 'method' | 'get' | 'set'; - computed: boolean; - static: boolean; -} - -export interface ClassDeclaration extends Class { - type: 'ClassDeclaration'; - id: Identifier; -} - -export interface ClassExpression extends Class { - type: 'ClassExpression'; -} - -export interface MetaProperty extends Node { - type: 'MetaProperty'; - meta: Identifier; - property: Identifier; -} - -export interface ImportDeclaration extends Node { - type: 'ImportDeclaration'; - specifiers: Array; - source: Literal; -} - -export interface ImportSpecifier extends Node { - type: 'ImportSpecifier'; - imported: Identifier | Literal; - local: Identifier; -} - -export interface ImportDefaultSpecifier extends Node { - type: 'ImportDefaultSpecifier'; - local: Identifier; -} - -export interface ImportNamespaceSpecifier extends Node { - type: 'ImportNamespaceSpecifier'; - local: Identifier; -} - -export interface ExportNamedDeclaration extends Node { - type: 'ExportNamedDeclaration'; - declaration?: Declaration | null; - specifiers: Array; - source?: Literal | null; -} - -export interface ExportSpecifier extends Node { - type: 'ExportSpecifier'; - exported: Identifier | Literal; - local: Identifier | Literal; -} - -export interface AnonymousFunctionDeclaration extends Function { - type: 'FunctionDeclaration'; - id: null; - body: BlockStatement; -} - -export interface AnonymousClassDeclaration extends Class { - type: 'ClassDeclaration'; - id: null; -} - -export interface ExportDefaultDeclaration extends Node { - type: 'ExportDefaultDeclaration'; - declaration: AnonymousFunctionDeclaration | FunctionDeclaration | AnonymousClassDeclaration | ClassDeclaration | Expression; -} - -export interface ExportAllDeclaration extends Node { - type: 'ExportAllDeclaration'; - source: Literal; - exported?: Identifier | Literal | null; -} - -export interface AwaitExpression extends Node { - type: 'AwaitExpression'; - argument: Expression; -} - -export interface ChainExpression extends Node { - type: 'ChainExpression'; - expression: MemberExpression | CallExpression; -} - -export interface ImportExpression extends Node { - type: 'ImportExpression'; - source: Expression; -} - -export interface ParenthesizedExpression extends Node { - type: 'ParenthesizedExpression'; - expression: Expression; -} - -export interface PropertyDefinition extends Node { - type: 'PropertyDefinition'; - key: Expression | PrivateIdentifier; - value?: Expression | null; - computed: boolean; - static: boolean; -} - -export interface PrivateIdentifier extends Node { - type: 'PrivateIdentifier'; - name: string; -} - -export interface StaticBlock extends Node { - type: 'StaticBlock'; - body: Array; -} - -export type Statement = - | ExpressionStatement - | BlockStatement - | EmptyStatement - | DebuggerStatement - | WithStatement - | ReturnStatement - | LabeledStatement - | BreakStatement - | ContinueStatement - | IfStatement - | SwitchStatement - | ThrowStatement - | TryStatement - | WhileStatement - | DoWhileStatement - | ForStatement - | ForInStatement - | ForOfStatement - | Declaration; - -export type Declaration = - | FunctionDeclaration - | VariableDeclaration - | ClassDeclaration; - -export type Expression = - | Identifier - | Literal - | ThisExpression - | ArrayExpression - | ObjectExpression - | FunctionExpression - | UnaryExpression - | UpdateExpression - | BinaryExpression - | AssignmentExpression - | LogicalExpression - | MemberExpression - | ConditionalExpression - | CallExpression - | NewExpression - | SequenceExpression - | ArrowFunctionExpression - | YieldExpression - | TemplateLiteral - | TaggedTemplateExpression - | ClassExpression - | MetaProperty - | AwaitExpression - | ChainExpression - | ImportExpression - | ParenthesizedExpression; - -export type Pattern = - | Identifier - | MemberExpression - | ObjectPattern - | ArrayPattern - | RestElement - | AssignmentPattern; - -export type ModuleDeclaration = - | ImportDeclaration - | ExportNamedDeclaration - | ExportDefaultDeclaration - | ExportAllDeclaration; - -export type AnyNode = - | Statement - | Expression - | Declaration - | ModuleDeclaration - | Literal - | Program - | SwitchCase - | CatchClause - | Property - | Super - | SpreadElement - | TemplateElement - | AssignmentProperty - | ObjectPattern - | ArrayPattern - | RestElement - | AssignmentPattern - | ClassBody - | MethodDefinition - | MetaProperty - | ImportSpecifier - | ImportDefaultSpecifier - | ImportNamespaceSpecifier - | ExportSpecifier - | AnonymousFunctionDeclaration - | AnonymousClassDeclaration - | PropertyDefinition - | PrivateIdentifier - | StaticBlock - | VariableDeclaration - | VariableDeclarator; - -export function parse(input: string, options: Options): Program; - -export function parseExpressionAt(input: string, pos: number, options: Options): Expression; - -export function tokenizer(input: string, options: Options): { - getToken(): Token; - [Symbol.iterator](): Iterator; -}; - -export type ecmaVersion = 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 'latest'; - -export interface Options { - /** - * `ecmaVersion` indicates the ECMAScript version to parse. Must be - * either 3, 5, 6 (or 2015), 7 (2016), 8 (2017), 9 (2018), 10 - * (2019), 11 (2020), 12 (2021), 13 (2022), 14 (2023), or `"latest"` - * (the latest version the library supports). This influences - * support for strict mode, the set of reserved words, and support - * for new syntax features. - */ - ecmaVersion: ecmaVersion; - - /** - * `sourceType` indicates the mode the code should be parsed in. - * Can be either `"script"` or `"module"`. This influences global - * strict mode and parsing of `import` and `export` declarations. - */ - sourceType?: 'script' | 'module'; - - /** - * a callback that will be called when a semicolon is automatically inserted. - * @param lastTokEnd the position of the comma as an offset - * @param lastTokEndLoc location if {@link locations} is enabled - */ - onInsertedSemicolon?: (lastTokEnd: number, lastTokEndLoc?: Position) => void; - - /** - * similar to `onInsertedSemicolon`, but for trailing commas - * @param lastTokEnd the position of the comma as an offset - * @param lastTokEndLoc location if `locations` is enabled - */ - onTrailingComma?: (lastTokEnd: number, lastTokEndLoc?: Position) => void; - - /** - * By default, reserved words are only enforced if ecmaVersion >= 5. - * Set `allowReserved` to a boolean value to explicitly turn this on - * an off. When this option has the value "never", reserved words - * and keywords can also not be used as property names. - */ - allowReserved?: boolean | 'never'; - - /** - * When enabled, a return at the top level is not considered an error. - */ - allowReturnOutsideFunction?: boolean; - - /** - * When enabled, import/export statements are not constrained to - * appearing at the top of the program, and an import.meta expression - * in a script isn't considered an error. - */ - allowImportExportEverywhere?: boolean; - - /** - * By default, `await` identifiers are allowed to appear at the top-level scope only if {@link ecmaVersion} >= 2022. - * When enabled, await identifiers are allowed to appear at the top-level scope, - * but they are still not allowed in non-async functions. - */ - allowAwaitOutsideFunction?: boolean; - - /** - * When enabled, super identifiers are not constrained to - * appearing in methods and do not raise an error when they appear elsewhere. - */ - allowSuperOutsideMethod?: boolean; - - /** - * When enabled, hashbang directive in the beginning of file is - * allowed and treated as a line comment. Enabled by default when - * {@link ecmaVersion} >= 2023. - */ - allowHashBang?: boolean; - - /** - * By default, the parser will verify that private properties are - * only used in places where they are valid and have been declared. - * Set this to false to turn such checks off. - */ - checkPrivateFields?: boolean; - - /** - * When `locations` is on, `loc` properties holding objects with - * `start` and `end` properties as {@link Position} objects will be attached to the - * nodes. - */ - locations?: boolean; - - /** - * a callback that will cause Acorn to call that export function with object in the same - * format as tokens returned from `tokenizer().getToken()`. Note - * that you are not allowed to call the parser from the - * callback—that will corrupt its internal state. - */ - onToken?: ((token: Token) => void) | Token[]; - - /** - * This takes a export function or an array. - * - * When a export function is passed, Acorn will call that export function with `(block, text, start, - * end)` parameters whenever a comment is skipped. `block` is a - * boolean indicating whether this is a block (`/* *\/`) comment, - * `text` is the content of the comment, and `start` and `end` are - * character offsets that denote the start and end of the comment. - * When the {@link locations} option is on, two more parameters are - * passed, the full locations of {@link Position} export type of the start and - * end of the comments. - * - * When a array is passed, each found comment of {@link Comment} export type is pushed to the array. - * - * Note that you are not allowed to call the - * parser from the callback—that will corrupt its internal state. - */ - onComment?: - | (( - isBlock: boolean, - text: string, - start: number, - end: number, - startLoc?: Position, - endLoc?: Position, - ) => void) - | Comment[]; - - /** - * Nodes have their start and end characters offsets recorded in - * `start` and `end` properties (directly on the node, rather than - * the `loc` object, which holds line/column data. To also add a - * [semi-standardized][range] `range` property holding a `[start, - * end]` array with the same numbers, set the `ranges` option to - * `true`. - */ - ranges?: boolean; - - /** - * It is possible to parse multiple files into a single AST by - * passing the tree produced by parsing the first file as - * `program` option in subsequent parses. This will add the - * toplevel forms of the parsed file to the `Program` (top) node - * of an existing parse tree. - */ - program?: Node; - - /** - * When {@link locations} is on, you can pass this to record the source - * file in every node's `loc` object. - */ - sourceFile?: string; - - /** - * This value, if given, is stored in every node, whether {@link locations} is on or off. - */ - directSourceFile?: string; - - /** - * When enabled, parenthesized expressions are represented by - * (non-standard) ParenthesizedExpression nodes - */ - preserveParens?: boolean; -} - -export class Parser { - options: Options; - input: string; - - constructor(options: Options, input: string, startPos?: number); - parse(): Program; - - static parse(input: string, options: Options): Program; - static parseExpressionAt(input: string, pos: number, options: Options): Expression; - static tokenizer(input: string, options: Options): { - getToken(): Token; - [Symbol.iterator](): Iterator; - }; - static extend(...plugins: ((BaseParser: typeof Parser) => typeof Parser)[]): typeof Parser; -} - -export const defaultOptions: Options; - -export function getLineInfo(input: string, offset: number): Position; - -export class TokenType { - label: string; - keyword: string | undefined; -} - -export const tokTypes: { - num: TokenType; - regexp: TokenType; - string: TokenType; - name: TokenType; - privateId: TokenType; - eof: TokenType; - - bracketL: TokenType; - bracketR: TokenType; - braceL: TokenType; - braceR: TokenType; - parenL: TokenType; - parenR: TokenType; - comma: TokenType; - semi: TokenType; - colon: TokenType; - dot: TokenType; - question: TokenType; - questionDot: TokenType; - arrow: TokenType; - template: TokenType; - invalidTemplate: TokenType; - ellipsis: TokenType; - backQuote: TokenType; - dollarBraceL: TokenType; - - eq: TokenType; - assign: TokenType; - incDec: TokenType; - prefix: TokenType; - logicalOR: TokenType; - logicalAND: TokenType; - bitwiseOR: TokenType; - bitwiseXOR: TokenType; - bitwiseAND: TokenType; - equality: TokenType; - relational: TokenType; - bitShift: TokenType; - plusMin: TokenType; - modulo: TokenType; - star: TokenType; - slash: TokenType; - starstar: TokenType; - coalesce: TokenType; - - _break: TokenType; - _case: TokenType; - _catch: TokenType; - _continue: TokenType; - _debugger: TokenType; - _default: TokenType; - _do: TokenType; - _else: TokenType; - _finally: TokenType; - _for: TokenType; - _function: TokenType; - _if: TokenType; - _return: TokenType; - _switch: TokenType; - _throw: TokenType; - _try: TokenType; - _var: TokenType; - _const: TokenType; - _while: TokenType; - _with: TokenType; - _new: TokenType; - _this: TokenType; - _super: TokenType; - _class: TokenType; - _extends: TokenType; - _export: TokenType; - _import: TokenType; - _null: TokenType; - _true: TokenType; - _false: TokenType; - _in: TokenType; - _instanceof: TokenType; - _typeof: TokenType; - _void: TokenType; - _delete: TokenType; -}; - -export interface Comment { - type: 'Line' | 'Block'; - value: string; - start: number; - end: number; - loc?: SourceLocation; - range?: [number, number]; -} - -export class Token { - type: TokenType; - start: number; - end: number; - loc?: SourceLocation; - range?: [number, number]; -} - -export const version: string; diff --git a/packages/apps/deno-runtime/deno.jsonc b/packages/apps/deno-runtime/deno.jsonc index da7d145b4d1ed..09a5d66f54459 100644 --- a/packages/apps/deno-runtime/deno.jsonc +++ b/packages/apps/deno-runtime/deno.jsonc @@ -4,19 +4,20 @@ "@rocket.chat/ui-kit": "npm:@rocket.chat/ui-kit@^0.31.22", "@rocket.chat/apps-engine/": "../../../packages/apps-engine/", "@rocket.chat/apps/": "../", - "@std/cli": "jsr:@std/cli@^1.0.9", "@std/io": "jsr:@std/io@^0.225.3", - "@std/streams": "jsr:@std/streams@^1.0.16", - "acorn": "npm:acorn@8.10.0", - "acorn-walk": "npm:acorn-walk@8.2.0", + "acorn": "npm:acorn@8.17.0", + "acorn-walk": "npm:acorn-walk@8.3.5", "astring": "npm:astring@1.8.6", "jsonrpc-lite": "npm:jsonrpc-lite@2.2.0", "stack-trace": "npm:stack-trace@0.0.10", "uuid": "npm:uuid@8.3.2" }, - "unstable": ["detect-cjs","sloppy-imports"], + "unstable": ["detect-cjs", "sloppy-imports"], + "compilerOptions": { + "strict": false + }, "tasks": { - "test": "deno test --no-check --allow-read --allow-write" + "test": "deno test --no-check --allow-read --allow-write --allow-env" }, "fmt": { "lineWidth": 160, diff --git a/packages/apps/deno-runtime/deno.lock b/packages/apps/deno-runtime/deno.lock index 4ef784109019f..dc49f2a27de85 100644 --- a/packages/apps/deno-runtime/deno.lock +++ b/packages/apps/deno-runtime/deno.lock @@ -2,14 +2,12 @@ "version": "5", "specifiers": { "jsr:@std/bytes@^1.0.6": "1.0.6", - "jsr:@std/cli@^1.0.9": "1.0.13", "jsr:@std/io@~0.225.3": "0.225.3", - "jsr:@std/streams@^1.0.16": "1.0.16", "npm:@msgpack/msgpack@3.0.0-beta2": "3.0.0-beta2", "npm:@rocket.chat/ui-kit@~0.31.22": "0.31.25_@rocket.chat+icons@0.32.0", "npm:@types/node@*": "22.12.0", - "npm:acorn-walk@8.2.0": "8.2.0", - "npm:acorn@8.10.0": "8.10.0", + "npm:acorn-walk@8.3.5": "8.3.5", + "npm:acorn@8.17.0": "8.17.0", "npm:astring@1.8.6": "1.8.6", "npm:jsonrpc-lite@2.2.0": "2.2.0", "npm:stack-trace@*": "0.0.10", @@ -20,20 +18,11 @@ "@std/bytes@1.0.6": { "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" }, - "@std/cli@1.0.13": { - "integrity": "5db2d95ab2dca3bca9fb6ad3c19908c314e93d6391c8b026725e4892d4615a69" - }, "@std/io@0.225.3": { "integrity": "27b07b591384d12d7b568f39e61dff966b8230559122df1e9fd11cc068f7ddd1", "dependencies": [ "jsr:@std/bytes" ] - }, - "@std/streams@1.0.16": { - "integrity": "85030627befb1767c60d4f65cb30fa2f94af1d6ee6e5b2515b76157a542e89c4", - "dependencies": [ - "jsr:@std/bytes" - ] } }, "npm": { @@ -56,11 +45,14 @@ "undici-types" ] }, - "acorn-walk@8.2.0": { - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" + "acorn-walk@8.3.5": { + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dependencies": [ + "acorn" + ] }, - "acorn@8.10.0": { - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "acorn@8.17.0": { + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "bin": true }, "astring@1.8.6": { @@ -123,13 +115,11 @@ }, "workspace": { "dependencies": [ - "jsr:@std/cli@^1.0.9", "jsr:@std/io@~0.225.3", - "jsr:@std/streams@^1.0.16", "npm:@msgpack/msgpack@3.0.0-beta2", "npm:@rocket.chat/ui-kit@~0.31.22", - "npm:acorn-walk@8.2.0", - "npm:acorn@8.10.0", + "npm:acorn-walk@8.3.5", + "npm:acorn@8.17.0", "npm:astring@1.8.6", "npm:jsonrpc-lite@2.2.0", "npm:stack-trace@0.0.10", diff --git a/packages/apps/deno-runtime/error-handlers.ts b/packages/apps/deno-runtime/error-handlers.ts index 5ce6542cd679e..25df23a58df90 100644 --- a/packages/apps/deno-runtime/error-handlers.ts +++ b/packages/apps/deno-runtime/error-handlers.ts @@ -3,27 +3,22 @@ import * as Messenger from './lib/messenger'; export function unhandledRejectionListener(event: PromiseRejectionEvent) { event.preventDefault(); - const { type, reason } = event; + const error = event.reason; Messenger.sendNotification({ method: 'unhandledRejection', - params: [ - { - type, - reason: reason instanceof Error ? reason.message : reason, - timestamp: new Date(), - }, - ], + params: [error?.stack || error], }); } export function unhandledExceptionListener(event: ErrorEvent) { event.preventDefault(); - const { type, message, filename, lineno, colno } = event; + const error = event.error; + Messenger.sendNotification({ method: 'uncaughtException', - params: [{ type, message, filename, lineno, colno }], + params: [error?.stack || error], }); } diff --git a/packages/apps/deno-runtime/handlers/app/construct.ts b/packages/apps/deno-runtime/handlers/app/construct.ts index 19a2743169efb..a359de051aaa8 100644 --- a/packages/apps/deno-runtime/handlers/app/construct.ts +++ b/packages/apps/deno-runtime/handlers/app/construct.ts @@ -1,9 +1,6 @@ -import { Socket } from 'node:net'; - import type { IParseAppPackageResult } from '@rocket.chat/apps/dist/server/compiler/IParseAppPackageResult'; import { AppObjectRegistry } from '../../AppObjectRegistry'; -import { require } from '../../lib/require'; import { sanitizeDeprecatedUsage } from '../../lib/sanitizeDeprecatedUsage'; import { AppAccessorsInstance } from '../../lib/accessors/mod'; import { RequestContext } from '../../lib/requestContext'; @@ -11,17 +8,37 @@ import { RequestContext } from '../../lib/requestContext'; const ALLOWED_NATIVE_MODULES = ['path', 'url', 'crypto', 'buffer', 'stream', 'net', 'http', 'https', 'zlib', 'util', 'punycode', 'os', 'querystring', 'fs']; const ALLOWED_EXTERNAL_MODULES = ['uuid']; -function prepareEnvironment() { - // Deno does not behave equally to Node when it comes to piping content to a socket - // So we intervene here - const originalFinal = Socket.prototype._final; - // deno-lint-ignore no-explicit-any - Socket.prototype._final = function _final(cb: any) { - // Deno closes the readable stream in the Socket earlier than Node - // The exact reason for that is yet unknown, so we'll need to simply delay the execution - // which allows data to be read in a response - setTimeout(() => originalFinal.call(this, cb), 1); - }; +/** + * A platform-dependent `require` used to resolve the modules an app is allowed + * to load (native `node:` modules, a small allow-list of npm packages, and + * apps-engine files). Each runtime injects its own via {@link setSandboxRequire} + * — Node hands over its global `require`, Deno hands over its `createRequire` + * shim that knows how to resolve compiled apps-engine paths. + */ +type SandboxRequire = (module: string) => unknown; + +function defaultSandboxRequire(): never { + throw new Error('No sandbox require has been injected; the runtime adapter must call setSandboxRequire() during bootstrap'); +} + +let sandboxRequire: SandboxRequire = defaultSandboxRequire; + +export function setSandboxRequire(newRequire: SandboxRequire): void { + sandboxRequire = newRequire; +} + +/** + * Extra globals bound into the app's eval shell on top of the common ones + * (`exports`, `module`, `require`, `console`, `globalThis`). Node needs none; + * Deno injects a `Buffer` and shadows `Deno` with `undefined`. Injecting them + * as data keeps the eval-shell skeleton single-source. + */ +type SandboxGlobals = Record; + +let sandboxGlobals: SandboxGlobals = {}; + +export function setSandboxGlobals(globals: SandboxGlobals): void { + sandboxGlobals = globals; } // As the apps are bundled, the only times they will call require are @@ -34,27 +51,36 @@ function buildRequire(): (module: string) => unknown { const normalized = module.replace('node:', ''); if (ALLOWED_NATIVE_MODULES.includes(normalized)) { - return require(`node:${normalized}`); + return sandboxRequire(`node:${normalized}`); } if (ALLOWED_EXTERNAL_MODULES.includes(module)) { - return require(`npm:${module}`); + return sandboxRequire(`npm:${module}`); } if (module.startsWith('@rocket.chat/apps-engine')) { // Our `require` function knows how to handle these - return require(module); + return sandboxRequire(module); } throw new Error(`Module ${module} is not allowed`); }; } -function wrapAppCode(code: string): (require: (module: string) => unknown) => Promise> { - return new Function( +function wrapAppCode(code: string): (require: SandboxRequire) => Promise> { + const globals = sandboxGlobals; + // The common globals are bound by name; any platform-specific extras are + // spread in by name from the injected `sandboxGlobals`, so the shell + // skeleton stays identical across runtimes. + const extraNames = Object.keys(globals); + const extraParams = extraNames.length ? `,${extraNames.join(',')}` : ''; + const extraArgs = extraNames.map((name) => `,__globals[${JSON.stringify(name)}]`).join(''); + + // eslint-disable-next-line @typescript-eslint/no-implied-eval -- This is the reason we run in a separate process + const fn = new Function( 'require', + '__globals', ` - const { Buffer } = require('buffer'); const exports = {}; const module = { exports }; const _error = console.error.bind(console); @@ -66,12 +92,14 @@ function wrapAppCode(code: string): (require: (module: string) => unknown) => Pr warn: _error, }; - const result = (async (exports,module,require,Buffer,console,globalThis,Deno) => { + const result = (async (exports,module,require,console,globalThis${extraParams}) => { ${code}; - })(exports,module,require,Buffer,_console,undefined,undefined); + })(exports,module,require,_console,undefined${extraArgs}); return result.then(() => module.exports);`, - ) as (require: (module: string) => unknown) => Promise>; + ) as (require: SandboxRequire, globals: SandboxGlobals) => Promise>; + + return (require: SandboxRequire) => fn(require, globals); } export default async function handleConstructApp(request: RequestContext): Promise { @@ -87,8 +115,6 @@ export default async function handleConstructApp(request: RequestContext): Promi throw new Error('Invalid params', { cause: 'invalid_param_type' }); } - prepareEnvironment(); - AppObjectRegistry.set('id', appPackage.info.id); const source = sanitizeDeprecatedUsage(appPackage.files[appPackage.info.classFile]); diff --git a/packages/apps/deno-runtime/handlers/app/handleUploadEvents.ts b/packages/apps/deno-runtime/handlers/app/handleUploadEvents.ts index dc4b286970575..69b698532da2a 100644 --- a/packages/apps/deno-runtime/handlers/app/handleUploadEvents.ts +++ b/packages/apps/deno-runtime/handlers/app/handleUploadEvents.ts @@ -1,17 +1,18 @@ import { Buffer } from 'node:buffer'; +import { readFile } from 'node:fs/promises'; import type { App } from '@rocket.chat/apps-engine/definition/App'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException'; -import type { IFileUploadContext } from '@rocket.chat/apps-engine/definition/uploads/IFileUploadContext' -import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails' -import { toArrayBuffer } from '@std/streams'; -import { Defined, JsonRpcError } from 'jsonrpc-lite'; +import type { IFileUploadContext } from '@rocket.chat/apps-engine/definition/uploads/IFileUploadContext'; +import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails'; +import type { Defined } from 'jsonrpc-lite'; +import { JsonRpcError } from 'jsonrpc-lite'; import { AppObjectRegistry } from '../../AppObjectRegistry'; -import { assertAppAvailable, assertHandlerFunction, isPlainObject } from '../lib/assertions'; import { AppAccessorsInstance } from '../../lib/accessors/mod'; import { RequestContext } from '../../lib/requestContext'; import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; +import { assertAppAvailable, assertHandlerFunction, isPlainObject } from '../lib/assertions'; export const uploadEvents = ['executePreFileUpload'] as const; @@ -28,8 +29,11 @@ function assertString(v: unknown): asserts v is string { } export default async function handleUploadEvents(request: RequestContext): Promise { - const { method: rawMethod, params } = request as { method: `app:${typeof uploadEvents[number]}`; params: [{ file?: IUploadDetails, path?: string }]}; - const [, method] = rawMethod.split(':') as ['app', typeof uploadEvents[number]]; + const { method: rawMethod, params } = request as { + method: `app:${(typeof uploadEvents)[number]}`; + params: [{ file?: IUploadDetails; path?: string }]; + }; + const [, method] = rawMethod.split(':') as ['app', (typeof uploadEvents)[number]]; try { const [{ file, path }] = params; @@ -42,12 +46,11 @@ export default async function handleUploadEvents(request: RequestContext): Promi assertIsUpload(file); assertString(path); - using tempFile = await Deno.open(path, { read: true, create: false }); let context: IFileUploadContext; switch (method) { case 'executePreFileUpload': { - const fileContents = await toArrayBuffer(tempFile.readable); + const fileContents = await readFile(path); context = { file, content: Buffer.from(fileContents) }; break; } @@ -61,7 +64,7 @@ export default async function handleUploadEvents(request: RequestContext): Promi AppAccessorsInstance.getPersistence(), AppAccessorsInstance.getModifier(), ); - } catch(e) { + } catch (e) { if (e?.name === AppsEngineException.name) { return new JsonRpcError(e.message, AppsEngineException.JSONRPC_ERROR_CODE, { name: e.name }); } diff --git a/packages/apps/deno-runtime/handlers/tests/upload-event-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/upload-event-handler.test.ts index 8813d77892403..b0647d30536dd 100644 --- a/packages/apps/deno-runtime/handlers/tests/upload-event-handler.test.ts +++ b/packages/apps/deno-runtime/handlers/tests/upload-event-handler.test.ts @@ -1,5 +1,8 @@ // deno-lint-ignore-file no-explicit-any import { Buffer } from 'node:buffer'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import type { App } from '@rocket.chat/apps-engine/definition/App'; import type { IPreFileUpload } from '@rocket.chat/apps-engine/definition/uploads/IPreFileUpload'; @@ -16,13 +19,15 @@ import { AppObjectRegistry } from '../../AppObjectRegistry'; describe('handlers > upload', () => { let app: App & IPreFileUpload; + let tempDir: string; let path: string; let file: IUploadDetails; beforeEach(async () => { AppObjectRegistry.clear(); - path = await Deno.makeTempFile(); + tempDir = await mkdtemp(join(tmpdir(), 'rc-apps-upload-')); + path = join(tempDir, 'tempfile'); app = { extendConfiguration: () => {}, @@ -33,7 +38,7 @@ describe('handlers > upload', () => { const content = 'Temp file for testing'; - await Deno.writeTextFile(path, content); + await writeFile(path, content); file = { name: 'TempFile.txt', @@ -45,7 +50,7 @@ describe('handlers > upload', () => { }); afterEach(async () => { - await Deno.remove(path).catch((e) => e?.code !== 'ENOENT' && console.warn(`Failed to remove temp file at ${path}`, e)); + await rm(tempDir, { recursive: true, force: true }).catch((e) => console.warn(`Failed to remove temp dir at ${tempDir}`, e)); }); it('correctly handles valid parameters', async () => { @@ -97,7 +102,7 @@ describe('handlers > upload', () => { }); it('fails when "path" is not a readable file path', async () => { - await Deno.remove(path); + await rm(path); const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); diff --git a/packages/apps/deno-runtime/lib/accessors/builders/BlockBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/BlockBuilder.ts index 9460b2c789858..69798dd311d7c 100644 --- a/packages/apps/deno-runtime/lib/accessors/builders/BlockBuilder.ts +++ b/packages/apps/deno-runtime/lib/accessors/builders/BlockBuilder.ts @@ -1,210 +1,16 @@ -import { v1 as uuid } from 'uuid'; - -import type { - IActionsBlock, - IBlock, - IConditionalBlock, - IConditionalBlockFilters, - IContextBlock, - IImageBlock, - IInputBlock, - ISectionBlock, -} from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks'; -import { BlockType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks'; -import type { - IBlockElement, - IButtonElement, - IImageElement, - IInputElement, - IInteractiveElement, - IMultiStaticSelectElement, - IOverflowMenuElement, - IPlainTextInputElement, - ISelectElement, - IStaticSelectElement, -} from '@rocket.chat/apps-engine/definition/uikit/blocks/Elements'; -import { BlockElementType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Elements'; -import { TextObjectType, type ITextObject } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects'; +import { BlockBuilder as AppsEngineBlockBuilder } from '@rocket.chat/apps-engine/definition/uikit/blocks/BlockBuilder'; import { AppObjectRegistry } from '../../../AppObjectRegistry'; -type BlockFunctionParameter = Omit; -type ElementFunctionParameter = T extends IInteractiveElement ? Omit | Partial> - : Omit; - -type SectionBlockParam = BlockFunctionParameter; -type ImageBlockParam = BlockFunctionParameter; -type ActionsBlockParam = BlockFunctionParameter; -type ContextBlockParam = BlockFunctionParameter; -type InputBlockParam = BlockFunctionParameter; - -type ButtonElementParam = ElementFunctionParameter; -type ImageElementParam = ElementFunctionParameter; -type OverflowMenuElementParam = ElementFunctionParameter; -type PlainTextInputElementParam = ElementFunctionParameter; -type StaticSelectElementParam = ElementFunctionParameter; -type MultiStaticSelectElementParam = ElementFunctionParameter; - /** + * Local BlockBuilder that extends the apps-engine BlockBuilder. + * It overrides the constructor to source the appId from the registry + * instead of requiring it as a constructor argument. + * * @deprecated please prefer the rocket.chat/ui-kit components */ -export class BlockBuilder { - private readonly blocks: Array; - private readonly appId: string; - +export class BlockBuilder extends AppsEngineBlockBuilder { constructor() { - this.blocks = []; - this.appId = String(AppObjectRegistry.get('id')); - } - - public addSectionBlock(block: SectionBlockParam): BlockBuilder { - this.addBlock({ type: BlockType.SECTION, ...block } as ISectionBlock); - - return this; - } - - public addImageBlock(block: ImageBlockParam): BlockBuilder { - this.addBlock({ type: BlockType.IMAGE, ...block } as IImageBlock); - - return this; - } - - public addDividerBlock(): BlockBuilder { - this.addBlock({ type: BlockType.DIVIDER }); - - return this; - } - - public addActionsBlock(block: ActionsBlockParam): BlockBuilder { - this.addBlock({ type: BlockType.ACTIONS, ...block } as IActionsBlock); - - return this; - } - - public addContextBlock(block: ContextBlockParam): BlockBuilder { - this.addBlock({ type: BlockType.CONTEXT, ...block } as IContextBlock); - - return this; - } - - public addInputBlock(block: InputBlockParam): BlockBuilder { - this.addBlock({ type: BlockType.INPUT, ...block } as IInputBlock); - - return this; - } - - public addConditionalBlock(innerBlocks: BlockBuilder | Array, condition?: IConditionalBlockFilters): BlockBuilder { - const render = innerBlocks instanceof BlockBuilder ? innerBlocks.getBlocks() : innerBlocks; - - this.addBlock({ - type: BlockType.CONDITIONAL, - render, - when: condition, - } as IConditionalBlock); - - return this; - } - - public getBlocks() { - return this.blocks; - } - - public newPlainTextObject(text: string, emoji = false): ITextObject { - return { - type: TextObjectType.PLAINTEXT, - text, - emoji, - }; - } - - public newMarkdownTextObject(text: string): ITextObject { - return { - type: TextObjectType.MARKDOWN, - text, - }; - } - - public newButtonElement(info: ButtonElementParam): IButtonElement { - return this.newInteractiveElement({ - type: BlockElementType.BUTTON, - ...info, - } as IButtonElement); - } - - public newImageElement(info: ImageElementParam): IImageElement { - return { - type: BlockElementType.IMAGE, - ...info, - }; - } - - public newOverflowMenuElement(info: OverflowMenuElementParam): IOverflowMenuElement { - return this.newInteractiveElement({ - type: BlockElementType.OVERFLOW_MENU, - ...info, - } as IOverflowMenuElement); - } - - public newPlainTextInputElement(info: PlainTextInputElementParam): IPlainTextInputElement { - return this.newInputElement({ - type: BlockElementType.PLAIN_TEXT_INPUT, - ...info, - } as IPlainTextInputElement); - } - - public newStaticSelectElement(info: StaticSelectElementParam): IStaticSelectElement { - return this.newSelectElement({ - type: BlockElementType.STATIC_SELECT, - ...info, - } as IStaticSelectElement); - } - - public newMultiStaticElement(info: MultiStaticSelectElementParam): IMultiStaticSelectElement { - return this.newSelectElement({ - type: BlockElementType.MULTI_STATIC_SELECT, - ...info, - } as IMultiStaticSelectElement); - } - - private newInteractiveElement(element: T): T { - if (!element.actionId) { - element.actionId = this.generateActionId(); - } - - return element; - } - - private newInputElement(element: T): T { - if (!element.actionId) { - element.actionId = this.generateActionId(); - } - - return element; - } - - private newSelectElement(element: T): T { - if (!element.actionId) { - element.actionId = this.generateActionId(); - } - - return element; - } - - private addBlock(block: IBlock): void { - if (!block.blockId) { - block.blockId = this.generateBlockId(); - } - - block.appId = this.appId; - - this.blocks.push(block); - } - - private generateBlockId(): string { - return uuid(); - } - - private generateActionId(): string { - return uuid(); + super(String(AppObjectRegistry.get('id') ?? '')); } } diff --git a/packages/apps/deno-runtime/lib/accessors/mod.ts b/packages/apps/deno-runtime/lib/accessors/mod.ts index 17073545edeb0..c0f13d725b7d8 100644 --- a/packages/apps/deno-runtime/lib/accessors/mod.ts +++ b/packages/apps/deno-runtime/lib/accessors/mod.ts @@ -1,32 +1,41 @@ +import type { IApiExtend } from '@rocket.chat/apps-engine/definition/accessors/IApiExtend'; import type { IAppAccessors } from '@rocket.chat/apps-engine/definition/accessors/IAppAccessors'; -import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api/IApiEndpointMetadata'; -import type { IEnvironmentWrite } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentWrite'; -import type { IEnvironmentRead } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentRead'; +import type { IConfigurationExtend } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationExtend'; import type { IConfigurationModify } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationModify'; -import type { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead'; +import type { IEnvironmentRead } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentRead'; +import type { IEnvironmentWrite } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentWrite'; +import type { IHttp, IHttpExtend } from '@rocket.chat/apps-engine/definition/accessors/IHttp'; import type { IModify } from '@rocket.chat/apps-engine/definition/accessors/IModify'; import type { INotifier } from '@rocket.chat/apps-engine/definition/accessors/INotifier'; +import type { IOutboundCommunicationProviderExtend } from '@rocket.chat/apps-engine/definition/accessors/IOutboundCommunicationProviderExtend'; import type { IPersistence } from '@rocket.chat/apps-engine/definition/accessors/IPersistence'; -import type { IHttp, IHttpExtend } from '@rocket.chat/apps-engine/definition/accessors/IHttp'; -import type { IConfigurationExtend } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationExtend'; -import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand'; -import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor'; +import type { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead'; +import type { ISchedulerExtend } from '@rocket.chat/apps-engine/definition/accessors/ISchedulerExtend'; +import type { ISlashCommandsExtend } from '@rocket.chat/apps-engine/definition/accessors/ISlashCommandsExtend'; +import type { ISlashCommandsModify } from '@rocket.chat/apps-engine/definition/accessors/ISlashCommandsModify'; +import type { IVideoConfProvidersExtend } from '@rocket.chat/apps-engine/definition/accessors/IVideoConfProvidersExtend'; import type { IApi } from '@rocket.chat/apps-engine/definition/api/IApi'; -import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider'; +import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api/IApiEndpointMetadata'; import type { - IOutboundPhoneMessageProvider, IOutboundEmailMessageProvider, + IOutboundPhoneMessageProvider, } from '@rocket.chat/apps-engine/definition/outboundCommunication/IOutboundCommsProvider'; +import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand'; +import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider'; -import { Http } from './http'; import { HttpExtend } from './extenders/HttpExtender'; -import * as Messenger from '../messenger'; +import { formatErrorResponse } from './formatResponseErrorHandler'; +import { Http } from './http'; import { AppObjectRegistry } from '../../AppObjectRegistry'; +import * as Messenger from '../messenger'; import { ModifyCreator } from './modify/ModifyCreator'; -import { ModifyUpdater } from './modify/ModifyUpdater'; import { ModifyExtender } from './modify/ModifyExtender'; +import { ModifyUpdater } from './modify/ModifyUpdater'; import { Notifier } from './notifier'; -import { formatErrorResponse } from './formatResponseErrorHandler'; + +/** Helper: extends T with an internal _proxy property used for delegation. */ +type WithProxy = T & { _proxy: T }; const httpMethods = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'] as const; @@ -37,18 +46,31 @@ if (!AppObjectRegistry.has('apiEndpoints')) { export class AppAccessors { private defaultAppAccessors?: IAppAccessors; + private environmentRead?: IEnvironmentRead; + private environmentWriter?: IEnvironmentWrite; + private configModifier?: IConfigurationModify; + private configExtender?: IConfigurationExtend; + private reader?: IRead; + private modifier?: IModify; + private persistence?: IPersistence; + private creator?: ModifyCreator; + private updater?: ModifyUpdater; + private extender?: ModifyExtender; + private httpExtend: IHttpExtend = new HttpExtend(); + private http?: IHttp; + private notifier?: INotifier; private proxify: (namespace: string, overrides?: Record unknown>) => T; @@ -58,28 +80,26 @@ export class AppAccessors { new Proxy( { __kind: `accessor:${namespace}` }, { - get: - (_target: unknown, prop: string) => - (...params: unknown[]) => { - // We don't want to send a request for this prop - if (prop === 'toJSON') { - return {}; - } - - // If the prop is inteded to be overriden by the caller - if (prop in overrides) { - return overrides[prop].apply(undefined, params); - } - - return senderFn({ - method: `accessor:${namespace}:${prop}`, - params, - }) - .then((response) => response.result) - .catch((err) => { - throw formatErrorResponse(err); - }); - }, + get: (_target: unknown, prop: string) => (...params: unknown[]) => { + // We don't want to send a request for this prop + if (prop === 'toJSON') { + return {}; + } + + // If the prop is inteded to be overriden by the caller + if (prop in overrides) { + return overrides[prop].apply(undefined, params); + } + + return senderFn({ + method: `accessor:${namespace}:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }); + }, }, ) as T; @@ -116,23 +136,25 @@ export class AppAccessors { public getConfigurationModify() { if (!this.configModifier) { - this.configModifier = { - scheduler: this.proxify('getConfigurationModify:scheduler'), - slashCommands: { - _proxy: this.proxify('getConfigurationModify:slashCommands'), - modifySlashCommand(slashcommand: ISlashCommand) { - // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand - AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); + const slashCommandsModify: WithProxy = { + _proxy: this.proxify('getConfigurationModify:slashCommands'), + modifySlashCommand(slashcommand: ISlashCommand) { + // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand + AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); - return this._proxy.modifySlashCommand(slashcommand); - }, - disableSlashCommand(command: string) { - return this._proxy.disableSlashCommand(command); - }, - enableSlashCommand(command: string) { - return this._proxy.enableSlashCommand(command); - }, + return this._proxy.modifySlashCommand(slashcommand); + }, + disableSlashCommand(command: string) { + return this._proxy.disableSlashCommand(command); + }, + enableSlashCommand(command: string) { + return this._proxy.enableSlashCommand(command); }, + }; + + this.configModifier = { + scheduler: this.proxify('getConfigurationModify:scheduler'), + slashCommands: slashCommandsModify, serverSettings: this.proxify('getConfigurationModify:serverSettings'), }; } @@ -142,77 +164,87 @@ export class AppAccessors { public getConfigurationExtend() { if (!this.configExtender) { - const senderFn = this.senderFn; + const { senderFn } = this; - this.configExtender = { - ui: this.proxify('getConfigurationExtend:ui'), - http: this.httpExtend, - settings: this.proxify('getConfigurationExtend:settings'), - externalComponents: this.proxify('getConfigurationExtend:externalComponents'), - api: { - _proxy: this.proxify('getConfigurationExtend:api'), - async provideApi(api: IApi) { - const apiEndpoints = AppObjectRegistry.get('apiEndpoints')!; + const apiExtend: WithProxy = { + _proxy: this.proxify('getConfigurationExtend:api'), + async provideApi(api: IApi) { + const apiEndpoints = AppObjectRegistry.get('apiEndpoints')!; - api.endpoints.forEach((endpoint) => { - endpoint._availableMethods = httpMethods.filter((method) => typeof endpoint[method] === 'function'); + api.endpoints.forEach((endpoint) => { + endpoint._availableMethods = httpMethods.filter((method) => typeof endpoint[method] === 'function'); - // We need to keep a reference to the endpoint around for us to call the executor later - AppObjectRegistry.set(`api:${endpoint.path}`, endpoint); - }); + // We need to keep a reference to the endpoint around for us to call the executor later + AppObjectRegistry.set(`api:${endpoint.path}`, endpoint); + }); - const result = await this._proxy.provideApi(api); + const result = await this._proxy.provideApi(api); - // Let's call the listApis method to cache the info from the endpoints - // Also, since this is a side-effect, we do it async so we can return to the caller - senderFn({ method: 'accessor:api:listApis' }) - .then((response) => apiEndpoints.push(...(response.result as IApiEndpointMetadata[]))) - .catch((err) => err.error); + // Let's call the listApis method to cache the info from the endpoints + // Also, since this is a side-effect, we do it async so we can return to the caller + senderFn({ method: 'accessor:api:listApis' }) + .then((response) => apiEndpoints.push(...(response.result as IApiEndpointMetadata[]))) + .catch((err) => err.error); - return result; - }, + return result; }, - scheduler: { - _proxy: this.proxify('getConfigurationExtend:scheduler'), - registerProcessors(processors: IProcessor[]) { - // Store the processor instance to use when the Apps-Engine calls the processor - processors.forEach((processor) => { - AppObjectRegistry.set(`scheduler:${processor.id}`, processor); - }); - - return this._proxy.registerProcessors(processors); - }, + }; + + const schedulerExtend: WithProxy = { + _proxy: this.proxify('getConfigurationExtend:scheduler'), + registerProcessors(processors: IProcessor[]) { + // Store the processor instance to use when the Apps-Engine calls the processor + processors.forEach((processor) => { + AppObjectRegistry.set(`scheduler:${processor.id}`, processor); + }); + + return this._proxy.registerProcessors(processors); }, - videoConfProviders: { - _proxy: this.proxify('getConfigurationExtend:videoConfProviders'), - provideVideoConfProvider(provider: IVideoConfProvider) { - // Store the videoConfProvider instance to use when the Apps-Engine calls the videoConfProvider - AppObjectRegistry.set(`videoConfProvider:${provider.name}`, provider); + }; - return this._proxy.provideVideoConfProvider(provider); - }, + const videoConfProviders: WithProxy = { + _proxy: this.proxify('getConfigurationExtend:videoConfProviders'), + provideVideoConfProvider(provider: IVideoConfProvider) { + // Store the videoConfProvider instance to use when the Apps-Engine calls the videoConfProvider + AppObjectRegistry.set(`videoConfProvider:${provider.name}`, provider); + + return this._proxy.provideVideoConfProvider(provider); }, - outboundCommunication: { - _proxy: this.proxify('getConfigurationExtend:outboundCommunication'), - registerEmailProvider(provider: IOutboundEmailMessageProvider) { - AppObjectRegistry.set(`outboundCommunication:${provider.name}-${provider.type}`, provider); - return this._proxy.registerEmailProvider(provider); - }, - registerPhoneProvider(provider: IOutboundPhoneMessageProvider) { - AppObjectRegistry.set(`outboundCommunication:${provider.name}-${provider.type}`, provider); - return this._proxy.registerPhoneProvider(provider); - }, + }; + + const outboundCommunication: WithProxy = { + _proxy: this.proxify('getConfigurationExtend:outboundCommunication'), + registerEmailProvider(provider: IOutboundEmailMessageProvider) { + AppObjectRegistry.set(`outboundCommunication:${provider.name}-${provider.type}`, provider); + return this._proxy.registerEmailProvider(provider); + }, + registerPhoneProvider(provider: IOutboundPhoneMessageProvider) { + AppObjectRegistry.set(`outboundCommunication:${provider.name}-${provider.type}`, provider); + return this._proxy.registerPhoneProvider(provider); }, - slashCommands: { - _proxy: this.proxify('getConfigurationExtend:slashCommands'), - provideSlashCommand(slashcommand: ISlashCommand) { - // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand - AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); + }; - return this._proxy.provideSlashCommand(slashcommand); - }, + const slashCommandsExtend: WithProxy = { + _proxy: this.proxify('getConfigurationExtend:slashCommands'), + provideSlashCommand(slashcommand: ISlashCommand) { + // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand + AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); + + return this._proxy.provideSlashCommand(slashcommand); }, }; + + this.configExtender = { + ui: this.proxify('getConfigurationExtend:ui'), + http: this.httpExtend, + settings: this.proxify('getConfigurationExtend:settings'), + externalComponents: this.proxify('getConfigurationExtend:externalComponents'), + api: apiExtend, + scheduler: schedulerExtend, + videoConfProviders, + outboundCommunication, + slashCommands: slashCommandsExtend, + }; } return this.configExtender; diff --git a/packages/apps/deno-runtime/lib/ast/mod.ts b/packages/apps/deno-runtime/lib/ast/mod.ts index f18d3de4ffc9d..234cfd3e784b2 100644 --- a/packages/apps/deno-runtime/lib/ast/mod.ts +++ b/packages/apps/deno-runtime/lib/ast/mod.ts @@ -1,13 +1,12 @@ -import { generate } from 'astring'; -// @deno-types="../../acorn.d.ts" -import { parse, Program } from 'acorn'; -// @deno-types="../../acorn-walk.d.ts" +import type { AnyNode, Node } from 'acorn'; +import { parse } from 'acorn'; import { fullAncestor } from 'acorn-walk'; +import { generate } from 'astring'; import * as operations from './operations'; import type { WalkerState } from './operations'; -function fixAst(ast: Program): boolean { +function fixAst(ast: Node): boolean { const pendingOperations = [ operations.fixLivechatIsOnlineCalls, operations.checkReassignmentOfModifiedIdentifiers, @@ -27,7 +26,7 @@ function fixAst(ast: Program): boolean { fullAncestor( ast, (node, state, ancestors, type) => { - ops.forEach((operation) => operation(node, state, ancestors, type)); + ops.forEach((operation) => operation(node as AnyNode, state as WalkerState, ancestors as AnyNode[], type)); }, undefined, state, @@ -51,7 +50,7 @@ function fixAst(ast: Program): boolean { export function fixBrokenSynchronousAPICalls(appSource: string): string { const astRootNode = parse(appSource, { // Latest ecma version supported by this version of acorn. - ecmaVersion: "latest", + ecmaVersion: 'latest', // Allow everything, we don't want to complain if code is badly written // Also, since the code itself has been transpiled, the chance of getting // shenanigans is lower diff --git a/packages/apps/deno-runtime/lib/ast/operations.ts b/packages/apps/deno-runtime/lib/ast/operations.ts index 7a5a4993ad297..4d74cc5efa059 100644 --- a/packages/apps/deno-runtime/lib/ast/operations.ts +++ b/packages/apps/deno-runtime/lib/ast/operations.ts @@ -1,7 +1,5 @@ -// @deno-types="../../acorn.d.ts" -import { AnyNode, AssignmentExpression, AwaitExpression, Expression, Function, Identifier, MethodDefinition, Property } from 'acorn'; -// @deno-types="../../acorn-walk.d.ts" -import { FullAncestorWalkerCallback } from 'acorn-walk'; +import type { AnyNode, AssignmentExpression, AwaitExpression, Expression, Function, Identifier, MethodDefinition, Property } from 'acorn'; +import type { FullAncestorWalkerCallback } from 'acorn-walk'; export type WalkerState = { isModified: boolean; @@ -104,10 +102,8 @@ export function buildFixModifiedFunctionsOperation(functionIdentifiers: Set = (node, { functionIdentifiers }, _ancestors) => { +export const checkReassignmentOfModifiedIdentifiers: FullAncestorWalkerCallback = ( + node, + { functionIdentifiers }, + _ancestors, +) => { if (node.type === 'AssignmentExpression') { if (node.operator !== '=') return; @@ -176,8 +176,6 @@ export const checkReassignmentOfModifiedIdentifiers: FullAncestorWalkerCallback< if (node.value?.type !== 'Identifier' || !functionIdentifiers.has(node.value.name)) return; functionIdentifiers.add(node.key.name); - - return; } }; diff --git a/packages/apps/deno-runtime/lib/ast/tests/data/ast_blocks.ts b/packages/apps/deno-runtime/lib/ast/tests/data/ast_blocks.ts index 8e750e6eaf587..8a283c6903a67 100644 --- a/packages/apps/deno-runtime/lib/ast/tests/data/ast_blocks.ts +++ b/packages/apps/deno-runtime/lib/ast/tests/data/ast_blocks.ts @@ -1,5 +1,4 @@ -// @deno-types="../../../../acorn.d.ts" -import { AnyNode, ClassDeclaration, ExpressionStatement, FunctionDeclaration, VariableDeclaration } from 'acorn'; +import type { AnyNode, ClassDeclaration, ExpressionStatement, FunctionDeclaration, VariableDeclaration } from 'acorn'; /** * Partial AST blocks to support testing. @@ -11,6 +10,8 @@ type TestNodeExcerpt = { node: N; }; +const startEnd = { start: 0, end: 0 }; + export const FunctionDeclarationFoo: TestNodeExcerpt = { code: 'function foo() {}', node: { @@ -18,6 +19,7 @@ export const FunctionDeclarationFoo: TestNodeExcerpt = { id: { type: 'Identifier', name: 'foo', + ...startEnd, }, expression: false, generator: false, @@ -26,7 +28,9 @@ export const FunctionDeclarationFoo: TestNodeExcerpt = { body: { type: 'BlockStatement', body: [], + ...startEnd, }, + ...startEnd, }, }; @@ -41,6 +45,7 @@ export const ConstFooAssignedFunctionExpression: TestNodeExcerpt id: { type: 'Identifier', name: 'Bar', + ...startEnd, }, superClass: null, body: { @@ -139,6 +161,7 @@ export const MethodDefinitionOfFooInClassBar: TestNodeExcerpt key: { type: 'Identifier', name: 'foo', + ...startEnd, }, value: { type: 'FunctionExpression', @@ -150,14 +173,19 @@ export const MethodDefinitionOfFooInClassBar: TestNodeExcerpt body: { type: 'BlockStatement', body: [], + ...startEnd, }, + ...startEnd, }, kind: 'method', computed: false, static: false, + ...startEnd, }, ], + ...startEnd, }, + ...startEnd, }, }; @@ -170,10 +198,13 @@ export const SimpleCallExpressionOfFoo: TestNodeExcerpt = { callee: { type: 'Identifier', name: 'foo', + ...startEnd, }, arguments: [], optional: false, + ...startEnd, }, + ...startEnd, }, }; @@ -187,6 +218,7 @@ export const SyncFunctionDeclarationWithAsyncCallExpression: TestNodeExcerpt = { left: { type: 'Identifier', name: 'bar', + ...startEnd, }, right: { type: 'Identifier', name: 'foo', + ...startEnd, }, + ...startEnd, }, + ...startEnd, }, }; @@ -256,17 +299,23 @@ export const AssignmentOfFooToBarMemberExpression: TestNodeExcerpt = { id: { type: 'Identifier', name: 'bar', + ...startEnd, }, expression: false, generator: false, @@ -352,28 +412,37 @@ export const FixSimpleCallExpression: TestNodeExcerpt = { id: { type: 'Identifier', name: 'a', + ...startEnd, }, init: { type: 'CallExpression', callee: { type: 'Identifier', name: 'foo', + ...startEnd, }, arguments: [], optional: false, + ...startEnd, }, + ...startEnd, }, ], + ...startEnd, }, { type: 'ReturnStatement', argument: { type: 'Identifier', name: 'a', + ...startEnd, }, + ...startEnd, }, ], + ...startEnd, }, + ...startEnd, }, }; @@ -394,6 +463,7 @@ export const ArrowFunctionDerefCallExpression: TestNodeExcerpt { diff --git a/packages/apps/deno-runtime/lib/logger.ts b/packages/apps/deno-runtime/lib/logger.ts index 59b6206471518..b7d6ba1c2a705 100644 --- a/packages/apps/deno-runtime/lib/logger.ts +++ b/packages/apps/deno-runtime/lib/logger.ts @@ -1,7 +1,11 @@ +import type { ILogEntry } from '@rocket.chat/apps-engine/definition/accessors/ILogEntry'; +import type { ILogger } from '@rocket.chat/apps-engine/definition/accessors/ILogger'; +import type { AppMethod } from '@rocket.chat/apps-engine/definition/metadata/AppMethod'; import stackTrace from 'stack-trace'; + import { AppObjectRegistry } from '../AppObjectRegistry'; -export interface StackFrame { +export interface IStackFrame { getTypeName(): string; getFunctionName(): string; getMethodName(): string; @@ -39,13 +43,15 @@ interface ILoggerStorageEntry { _createdAt: Date; } -export class Logger { +export class Logger implements ILogger { + public method: `${AppMethod}`; + private entries: Array; + private start: Date; - private method: string; constructor(method: string) { - this.method = method; + this.method = method as `${AppMethod}`; this.entries = []; this.start = new Date(); } @@ -98,7 +104,7 @@ export class Logger { }); } - private getStack(stack: Array): string { + private getStack(stack: Array): string { let func = 'anonymous'; if (stack.length === 1) { @@ -120,10 +126,26 @@ export class Logger { return func; } - private getTotalTime(): number { + public getTotalTime(): number { return new Date().getTime() - this.start.getTime(); } + public getEntries(): Array { + return this.entries as Array; + } + + public getMethod(): `${AppMethod}` { + return this.method; + } + + public getStartTime(): Date { + return this.start; + } + + public getEndTime(): Date { + return new Date(); + } + public hasEntries(): boolean { return this.entries.length > 0; } diff --git a/packages/apps/deno-runtime/lib/messenger.ts b/packages/apps/deno-runtime/lib/messenger.ts index 4b938459e9715..7998f1b654a92 100644 --- a/packages/apps/deno-runtime/lib/messenger.ts +++ b/packages/apps/deno-runtime/lib/messenger.ts @@ -1,4 +1,4 @@ -import { writeAll } from '@std/io'; +import EventEmitter from 'node:events'; import * as jsonrpc from 'jsonrpc-lite'; @@ -30,7 +30,7 @@ export function isErrorResponse(message: jsonrpc.JsonRpc): message is jsonrpc.Er const COMMAND_PONG = '_zPONG'; -export const RPCResponseObserver = new EventTarget(); +export const RPCResponseObserver = new EventEmitter(); export const Queue = new (class Queue { private queue: Uint8Array[] = []; @@ -47,7 +47,7 @@ export const Queue = new (class Queue { const message = this.queue.shift(); if (message) { - await Transport.send(message); + await transport.send(message); } } @@ -64,34 +64,36 @@ export const Queue = new (class Queue { } })(); -export const Transport = new (class Transporter { - private selectedTransport: Transporter['stdoutTransport'] | Transporter['noopTransport']; - - constructor() { - this.selectedTransport = this.stdoutTransport.bind(this); - } - - private async stdoutTransport(message: Uint8Array): Promise { - await writeAll(Deno.stdout, message); - } - - private async noopTransport(_message: Uint8Array): Promise {} - - public selectTransport(transport: 'stdout' | 'noop'): void { - switch (transport) { - case 'stdout': - this.selectedTransport = this.stdoutTransport.bind(this); - break; - case 'noop': - this.selectedTransport = this.noopTransport.bind(this); - break; - } - } - - public send(message: Uint8Array): Promise { - return this.selectedTransport(message); - } -})(); +/** + * A platform-dependent component responsible for delivering encoded messages to + * the host that controls this runtime. + * + * Each runtime platform is expected to provide its own implementation and + * inject it via {@link setTransport}. + */ +export type Transport = { + send(message: Uint8Array): Promise; +}; + +/** + * The default transport. It discards every message, and is used until a + * platform injects its own transport via {@link setTransport}. + */ +export const noopTransport: Transport = { + send: () => Promise.resolve(), +}; + +let transport: Transport = noopTransport; + +/** + * Injects the transport implementation to be used when sending messages. + * + * Platforms must call this during bootstrap to wire up the appropriate + * transport. Until then, messages are discarded by the default no-op transport. + */ +export function setTransport(newTransport: Transport): void { + transport = newTransport; +} export function parseMessage(message: string | Record) { let parsed: jsonrpc.IParsedObject | jsonrpc.IParsedObject[]; @@ -171,19 +173,15 @@ export async function sendRequest(requestDescriptor: RequestDescriptor): Promise // TODO: add timeout to this const responsePromise = new Promise((resolve, reject) => { - const handler = (event: Event) => { - if (event instanceof ErrorEvent) { - reject(event.error); - } - - if (event instanceof CustomEvent) { - resolve(event.detail); + const handler = (payload: { error: Error } | { detail: jsonrpc.SuccessObject }) => { + if ('error' in payload) { + return reject(payload.error); } - RPCResponseObserver.removeEventListener(`response:${request.id}`, handler); + return resolve(payload.detail); }; - RPCResponseObserver.addEventListener(`response:${request.id}`, handler); + RPCResponseObserver.once(`response:${request.id}`, handler); }); await Queue.enqueue(request); diff --git a/packages/apps/deno-runtime/lib/metricsCollector.ts b/packages/apps/deno-runtime/lib/metricsCollector.ts index ac968caccace3..9737c9e8e4eaf 100644 --- a/packages/apps/deno-runtime/lib/metricsCollector.ts +++ b/packages/apps/deno-runtime/lib/metricsCollector.ts @@ -1,9 +1,10 @@ -import { writeAll } from '@std/io'; +import process from 'node:process'; + import { Queue } from './messenger'; export function collectMetrics() { return { - pid: Deno.pid, + pid: process.pid, queueSize: Queue.getCurrentSize(), }; } @@ -16,5 +17,7 @@ const encoder = new TextEncoder(); export async function sendMetrics() { const metrics = collectMetrics(); - await writeAll(Deno.stderr, encoder.encode(JSON.stringify(metrics))); + await new Promise((resolve, reject) => { + process.stderr.write(encoder.encode(JSON.stringify(metrics)), (error) => (error ? reject(error) : resolve())); + }); } diff --git a/packages/apps/deno-runtime/lib/parseArgs.ts b/packages/apps/deno-runtime/lib/parseArgs.ts index a9c4844154990..ec1efd80a9ff2 100644 --- a/packages/apps/deno-runtime/lib/parseArgs.ts +++ b/packages/apps/deno-runtime/lib/parseArgs.ts @@ -1,4 +1,4 @@ -import { parseArgs as $parseArgs } from '@std/cli/parse-args'; +import { parseArgs as $parseArgs } from 'node:util'; export type ParsedArgs = { subprocess: string; @@ -7,5 +7,22 @@ export type ParsedArgs = { }; export function parseArgs(args: string[]): ParsedArgs { - return $parseArgs(args); + const { values } = $parseArgs({ + args, + options: { + subprocess: { type: 'string' }, + spawnId: { type: 'string' }, + metricsReportFrequencyInMs: { type: 'string' }, + }, + strict: false, + }); + + return { + subprocess: (values.subprocess as string) ?? '', + spawnId: Number(values.spawnId ?? 0), + metricsReportFrequencyInMs: + typeof values.metricsReportFrequencyInMs === 'string' && Number.isFinite(values.metricsReportFrequencyInMs) + ? Number(values.metricsReportFrequencyInMs) + : undefined, + }; } diff --git a/packages/apps/deno-runtime/lib/require.ts b/packages/apps/deno-runtime/lib/require.ts index 7d842d829e598..a2c17036237a5 100644 --- a/packages/apps/deno-runtime/lib/require.ts +++ b/packages/apps/deno-runtime/lib/require.ts @@ -7,8 +7,7 @@ export const require = (mod: string) => { // However, the import maps are configured to look at the source folder for typescript files, but during // runtime those files are not available if (mod.startsWith('@rocket.chat/apps-engine')) { - // Only remove "src/" substring when it comes after "apps-engine/" - mod = import.meta.resolve(mod).replace('file://', '').replace('apps-engine/src/', 'apps-engine/'); + mod = import.meta.resolve(mod).replace('file://', ''); } return _require(mod); diff --git a/packages/apps/deno-runtime/lib/tests/messenger.test.ts b/packages/apps/deno-runtime/lib/tests/messenger.test.ts index d278c4ccbd545..9c6057726bca5 100644 --- a/packages/apps/deno-runtime/lib/tests/messenger.test.ts +++ b/packages/apps/deno-runtime/lib/tests/messenger.test.ts @@ -3,6 +3,7 @@ import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203. import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; import * as Messenger from '../messenger'; +import { stdoutTransport } from '../transports/stdoutTransport'; import { AppObjectRegistry } from '../../AppObjectRegistry'; import { createMockRequest } from '../../handlers/tests/helpers/mod'; import { RequestContext } from '../requestContext'; @@ -14,14 +15,14 @@ describe('Messenger', () => { beforeEach(() => { AppObjectRegistry.clear(); AppObjectRegistry.set('id', 'test'); - Messenger.Transport.selectTransport('noop'); + Messenger.setTransport(Messenger.noopTransport); context = createMockRequest({ method: 'test', params: [] }); }); afterAll(() => { AppObjectRegistry.clear(); - Messenger.Transport.selectTransport('stdout'); + Messenger.setTransport(stdoutTransport); }); it('should add logs to success responses', async () => { diff --git a/packages/apps/deno-runtime/lib/transports/stdoutTransport.ts b/packages/apps/deno-runtime/lib/transports/stdoutTransport.ts new file mode 100644 index 0000000000000..dce9283b6fa1b --- /dev/null +++ b/packages/apps/deno-runtime/lib/transports/stdoutTransport.ts @@ -0,0 +1,16 @@ +import { writeAll } from '@std/io'; + +import type { Transport } from '../messenger'; + +/** + * Transport that writes messages to the process' standard output. + * + * This is the transport used when the runtime is executed as a subprocess by + * the Apps-Engine framework, and it is specific to platforms that expose a + * Node-compatible `process.stdout`. + */ +export const stdoutTransport: Transport = { + send(message: Uint8Array): Promise { + return writeAll(Deno.stdout, message); + }, +}; diff --git a/packages/apps/deno-runtime/main.ts b/packages/apps/deno-runtime/main.ts index 13ce43302923c..711f7abda07ad 100644 --- a/packages/apps/deno-runtime/main.ts +++ b/packages/apps/deno-runtime/main.ts @@ -1,135 +1,54 @@ -if (!Deno.args.includes('--subprocess')) { - Deno.stderr.writeSync( - new TextEncoder().encode(` - This is a Deno wrapper for Rocket.Chat Apps. It is not meant to be executed stand-alone; - It is instead meant to be executed as a subprocess by the Apps-Engine framework. - `), - ); - Deno.exit(1001); -} - -import { JsonRpcError } from 'jsonrpc-lite'; - -import * as Messenger from './lib/messenger'; -import { decoder } from './lib/codec'; -import { Logger } from './lib/logger'; - -import slashcommandHandler from './handlers/slashcommand-handler'; -import videoConferenceHandler from './handlers/videoconference-handler'; -import apiHandler from './handlers/api-handler'; -import handleApp from './handlers/app/handler'; -import handleScheduler from './handlers/scheduler-handler'; +import { Buffer } from 'node:buffer'; +import { Socket } from 'node:net'; +import process from 'node:process'; + +// Deno consumes the base as TypeScript source through the import map: ESM +// imports honor the map (`@rocket.chat/apps/` → `../`), so bare specifiers and +// `@rocket.chat/apps/*` resolve to allowed paths. Importing the compiled `dist` +// instead would run the base as CommonJS, whose `require()` bypasses the import +// map and falls back to node_modules — outside the subprocess read allowlist. +import { stdoutTransport } from './lib/transports/stdoutTransport'; +import { setTransport } from './lib/messenger'; + +import { setSandboxGlobals, setSandboxRequire } from './handlers/app/construct'; import registerErrorListeners from './error-handlers'; -import { sendMetrics } from './lib/metricsCollector'; -import outboundMessageHandler from './handlers/outboundcomms-handler'; -import { RequestContext } from './lib/requestContext'; - -type Handlers = { - app: typeof handleApp; - api: typeof apiHandler; - slashcommand: typeof slashcommandHandler; - videoconference: typeof videoConferenceHandler; - outboundCommunication: typeof outboundMessageHandler; - scheduler: typeof handleScheduler; - ping: (request: RequestContext) => 'pong'; -}; - -const COMMAND_PING = '_zPING'; - -async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promise { - const methodHandlers: Handlers = { - app: handleApp, - api: apiHandler, - slashcommand: slashcommandHandler, - videoconference: videoConferenceHandler, - outboundCommunication: outboundMessageHandler, - scheduler: handleScheduler, - ping: (_request) => 'pong', - }; - - // We're not handling notifications at the moment - if (type === 'notification') { - return Messenger.sendInvalidRequestError(); - } - - const { id, method } = payload; - - const logger = new Logger(method); - - const context: RequestContext = Object.assign(payload, { - context: { logger }, - }); +import { require } from './lib/require'; +import { startMainLoop } from './mainLoop'; - const [methodPrefix] = method.split(':') as [keyof Handlers]; - const handler = methodHandlers[methodPrefix]; - - if (!handler) { - return Messenger.errorResponse( - { - error: { message: 'Method not found', code: -32601 }, - id, - }, - context, - ); - } - - const result = await handler(context); - - if (result instanceof JsonRpcError) { - return Messenger.errorResponse({ id, error: result }, context); - } +if (!process.argv.includes('--subprocess')) { + console.error(` + This is a Deno wrapper for Rocket.Chat Apps. It is not meant to be executed stand-alone; + It is instead meant to be executed as a subprocess by the Apps-Engine framework. + `); - return Messenger.successResponse({ id, result }, context); + process.exit(1); } -function handleResponse(response: Messenger.JsonRpcResponse): void { - let event: Event; - - if (response.type === 'error') { - event = new ErrorEvent(`response:${response.payload.id}`, { - error: response.payload, - }); - } else { - event = new CustomEvent(`response:${response.payload.id}`, { - detail: response.payload, - }); - } - - Messenger.RPCResponseObserver.dispatchEvent(event); +function prepareEnvironment() { + // Deno does not behave equally to Node when it comes to piping content to a socket + // So we intervene here + const originalFinal = Socket.prototype._final; + // deno-lint-ignore no-explicit-any + Socket.prototype._final = function _final(cb: any) { + // Deno closes the readable stream in the Socket earlier than Node + // The exact reason for that is yet unknown, so we'll need to simply delay the execution + // which allows data to be read in a response + setTimeout(() => originalFinal.call(this, cb), 1); + }; } -async function main() { - Messenger.sendNotification({ method: 'ready' }); - - for await (const message of decoder.decodeStream(Deno.stdin.readable)) { - try { - // Process PING command first as it is not JSON RPC - if (message === COMMAND_PING) { - void Messenger.pongResponse(); - void sendMetrics(); - continue; - } - - const JSONRPCMessage = Messenger.parseMessage(message as Record); +// This runtime communicates with the Apps-Engine host through stdout +setTransport(stdoutTransport); - if (Messenger.isRequest(JSONRPCMessage)) { - void requestRouter(JSONRPCMessage); - continue; - } - - if (Messenger.isResponse(JSONRPCMessage)) { - handleResponse(JSONRPCMessage); - } - } catch (error) { - if (Messenger.isErrorResponse(error)) { - await Messenger.errorResponse(error); - } else { - await Messenger.sendParseError(); - } - } - } -} +// Deno's sandbox `require` is a createRequire shim that resolves compiled +// apps-engine paths; the eval shell additionally needs a `Buffer` and a +// shadowed `Deno` (→ undefined) bound by name. +setSandboxRequire(require); +setSandboxGlobals({ Buffer, Deno: undefined }); registerErrorListeners(); -main(); +// Process-global side effect; doing it once at startup is cleaner than inside construct +prepareEnvironment(); + +startMainLoop(); diff --git a/packages/apps/deno-runtime/mainLoop.ts b/packages/apps/deno-runtime/mainLoop.ts new file mode 100644 index 0000000000000..d18fbd9ab1af6 --- /dev/null +++ b/packages/apps/deno-runtime/mainLoop.ts @@ -0,0 +1,127 @@ +import process from 'node:process'; + +import jsonrpc, { JsonRpcError, type SuccessObject } from 'jsonrpc-lite'; + +import apiHandler from './handlers/api-handler'; +import handleApp from './handlers/app/handler'; +import outboundMessageHandler from './handlers/outboundcomms-handler'; +import handleScheduler from './handlers/scheduler-handler'; +import slashcommandHandler from './handlers/slashcommand-handler'; +import videoConferenceHandler from './handlers/videoconference-handler'; +import { decoder } from './lib/codec'; +import { Logger } from './lib/logger'; +import * as Messenger from './lib/messenger'; +import { sendMetrics } from './lib/metricsCollector'; +import type { RequestContext } from './lib/requestContext'; + +type Handlers = { + app: typeof handleApp; + api: typeof apiHandler; + slashcommand: typeof slashcommandHandler; + videoconference: typeof videoConferenceHandler; + outboundCommunication: typeof outboundMessageHandler; + scheduler: typeof handleScheduler; + ping: (request: RequestContext) => Promise<'pong'>; +}; + +const COMMAND_PING = '_zPING'; + +async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promise { + const methodHandlers: Handlers = { + app: handleApp, + api: apiHandler, + slashcommand: slashcommandHandler, + videoconference: videoConferenceHandler, + outboundCommunication: outboundMessageHandler, + scheduler: handleScheduler, + ping: (_request) => Promise.resolve('pong'), + }; + + // We're not handling notifications at the moment + if (type === 'notification') { + return Messenger.sendInvalidRequestError(); + } + + const { id, method } = payload; + + const logger = new Logger(method); + + const context: RequestContext = Object.assign(payload, { + context: { logger }, + }); + + const [methodPrefix] = method.split(':') as [keyof Handlers]; + const handler = methodHandlers[methodPrefix]; + + if (!handler) { + return Messenger.errorResponse( + { + error: { message: 'Method not found', code: -32601 }, + id, + }, + context, + ); + } + + const result = await handler(context).catch((reason) => + JsonRpcError.internalError({ cause: reason instanceof Error ? reason.toString() : reason }), + ); + + if (result instanceof JsonRpcError) { + return Messenger.errorResponse({ id, error: result }, context); + } + + return Messenger.successResponse({ id, result }, context); +} + +function handleResponse(response: Messenger.JsonRpcResponse): void { + let payload: { error: Error } | { detail: SuccessObject }; + + if (Messenger.isErrorResponse(response.payload)) { + payload = { error: new Error(response.payload.error.message) }; + } else { + payload = { detail: response.payload }; + } + + Messenger.RPCResponseObserver.emit(`response:${response.payload.id}`, payload); +} + +/** + * The platform-agnostic message loop shared by every runtime. + * + * Adapters are expected to wire up their platform seams — transport, sandbox + * `require`/globals, error listeners — during bootstrap and only then invoke + * this loop. It reads messages from `process.stdin` (a `node:` API available on + * every supported platform) and dispatches them to the shared handlers. + */ +export async function startMainLoop(): Promise { + Messenger.sendNotification({ method: 'ready', params: [] }); + + for await (const message of decoder.decodeStream(process.stdin)) { + try { + // Process PING command first as it is not JSON RPC + if (message === COMMAND_PING) { + void Messenger.pongResponse(); + void sendMetrics(); + continue; + } + + const JSONRPCMessage = Messenger.parseMessage(message as Record); + + if (Messenger.isRequest(JSONRPCMessage)) { + void requestRouter(JSONRPCMessage); + continue; + } + + if (Messenger.isResponse(JSONRPCMessage)) { + handleResponse(JSONRPCMessage); + } + } catch (error) { + if (Messenger.isErrorResponse(error)) { + await Messenger.errorResponse(error); + } else { + await Messenger.sendParseError(); + } + } + } +} diff --git a/packages/apps/deno-runtime/tests/error-handlers.test.ts b/packages/apps/deno-runtime/tests/error-handlers.test.ts new file mode 100644 index 0000000000000..c9301ae7e726e --- /dev/null +++ b/packages/apps/deno-runtime/tests/error-handlers.test.ts @@ -0,0 +1,44 @@ +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; + +import * as Messenger from '../lib/messenger'; + +import { unhandledExceptionListener, unhandledRejectionListener } from '../error-handlers'; + +// the Deno seam suite: exercises the platform-specific error-handler +// registration and, in doing so, confirms the Deno adapter resolves the base +// source through the import map. +describe('error-handlers (Deno seam)', () => { + it('forwards unhandled rejections to the base Messenger as a notification', () => { + const enqueue = spy(Messenger.Queue, 'enqueue'); + + try { + const error = new Error('rejected'); + unhandledRejectionListener({ preventDefault() {}, reason: error } as unknown as PromiseRejectionEvent); + + assertEquals(enqueue.calls.length, 1); + const notification = enqueue.calls[0].args[0] as { method: string; params: unknown[] }; + assertEquals(notification.method, 'unhandledRejection'); + assertEquals(notification.params, [error.stack]); + } finally { + enqueue.restore(); + } + }); + + it('forwards uncaught exceptions to the base Messenger as a notification', () => { + const enqueue = spy(Messenger.Queue, 'enqueue'); + + try { + const error = new Error('thrown'); + unhandledExceptionListener({ preventDefault() {}, error } as unknown as ErrorEvent); + + assertEquals(enqueue.calls.length, 1); + const notification = enqueue.calls[0].args[0] as { method: string; params: unknown[] }; + assertEquals(notification.method, 'uncaughtException'); + assertEquals(notification.params, [error.stack]); + } finally { + enqueue.restore(); + } + }); +});