Skip to content

Commit 69b7fa4

Browse files
committed
feat(apps): introduce node-runtime
refactor(apps): reduce node-runtime to a thin base adapter node-runtime now keeps only its platform seam: main.ts is a bootstrap that wires the stdout transport, the sandbox require (Node's global require) and empty sandbox globals, registers error listeners, then invokes the base message loop. error-handlers and stdoutTransport import the base messenger via the compiled specifier @rocket.chat/apps/base-runtime/dist/..., resolved by the existing loader-hook. Drop the stray debug console.error from loader-hook now that real runtime imports flow through it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> node-runtime main.ts comment
1 parent e1bd350 commit 69b7fa4

7 files changed

Lines changed: 97 additions & 2 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Messenger from '@rocket.chat/apps/base-runtime/dist/lib/messenger';
2+
3+
export default function registerErrorListeners() {
4+
process.on('uncaughtException', (error: Error, origin: 'uncaughtException' | 'unhandledRejection') => {
5+
Messenger.sendNotification({
6+
method: origin,
7+
params: [error.stack || error],
8+
});
9+
});
10+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { registerHooks } from 'node:module';
2+
import path from 'node:path';
3+
4+
// This file compiles to dist/lib/loader-hook.js.
5+
// Three levels up from dist/lib/ lands on packages/apps/ — the @rocket.chat/apps package root.
6+
const appsPackageDir = path.resolve(__dirname, '../../..');
7+
8+
const PACKAGE_PREFIX = '@rocket.chat/apps';
9+
10+
registerHooks({
11+
resolve(specifier, context, nextResolve) {
12+
if (specifier === PACKAGE_PREFIX || specifier.startsWith(`${PACKAGE_PREFIX}/`)) {
13+
const subpath = specifier.slice(PACKAGE_PREFIX.length).replace(/^\//, '');
14+
const localPath = subpath ? path.join(appsPackageDir, subpath) : appsPackageDir;
15+
return nextResolve(localPath, context);
16+
}
17+
return nextResolve(specifier, context);
18+
},
19+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Transport } from '@rocket.chat/apps/base-runtime/dist/lib/messenger';
2+
3+
/**
4+
* Transport that writes messages to the process' standard output.
5+
*
6+
* This is the transport used when the runtime is executed as a subprocess by
7+
* the Apps-Engine framework, and it is specific to platforms that expose a
8+
* Node-compatible `process.stdout`.
9+
*/
10+
export const stdoutTransport: Transport = {
11+
async send(message: Uint8Array): Promise<void> {
12+
await new Promise<void>((resolve, reject) => {
13+
process.stdout.write(message, (err) => (err ? reject(err) : resolve()));
14+
});
15+
},
16+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import './lib/loader-hook';
2+
3+
import { setSandboxGlobals, setSandboxRequire } from '@rocket.chat/apps/base-runtime/dist/handlers/app/construct';
4+
import { setTransport } from '@rocket.chat/apps/base-runtime/dist/lib/messenger';
5+
import { startMainLoop } from '@rocket.chat/apps/base-runtime/dist/mainLoop';
6+
7+
import registerErrorListeners from './error-handlers';
8+
import { stdoutTransport } from './lib/transports/stdoutTransport';
9+
10+
if (!process.argv.includes('--subprocess')) {
11+
process.stderr.write(
12+
new TextEncoder().encode(`
13+
This is a Deno wrapper for Rocket.Chat Apps. It is not meant to be executed stand-alone;
14+
It is instead meant to be executed as a subprocess by the Apps-Engine framework.
15+
`),
16+
);
17+
process.exit(1001);
18+
}
19+
20+
// This runtime communicates with the Apps-Engine host through stdout
21+
setTransport(stdoutTransport);
22+
23+
// The sandbox `require` handed to the app is Node's own global `require`; it
24+
// needs no extra globals beyond the common ones the base eval shell binds.
25+
setSandboxRequire(require);
26+
setSandboxGlobals({});
27+
28+
registerErrorListeners();
29+
30+
void startMainLoop();
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"compilerOptions": {
4+
"rootDir": "./src",
5+
"outDir": "./dist",
6+
"baseUrl": ".",
7+
"types": ["node"],
8+
"lib": ["es2023"],
9+
"module": "nodenext",
10+
"moduleResolution": "nodenext",
11+
"target": "es2023",
12+
"declaration": true,
13+
"paths": {
14+
"@rocket.chat/apps/*": ["../*"]
15+
}
16+
},
17+
"include": ["./src/**/*"]
18+
}

packages/apps/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
"files": [
88
"dist/",
99
"base-runtime/",
10+
"node-runtime/",
1011
"deno-runtime/",
1112
".deno-cache/"
1213
],
1314
"scripts": {
14-
"build": "run-s build:clean build:default build:base-runtime build:deno-cache",
15+
"build": "run-s build:clean build:default build:base-runtime build:node-runtime build:deno-cache",
1516
"build:clean": "rimraf dist base-runtime/dist",
1617
"build:default": "tsc -p tsconfig.json",
1718
"build:base-runtime": "tsc -p base-runtime/tsconfig.json",
19+
"build:node-runtime": "tsc -p node-runtime/tsconfig.json",
1820
"build:deno-cache": "node scripts/deno-cache.js",
1921
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
2022
"lint": "eslint .",

packages/apps/turbo.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"build": {
55
"dependsOn": ["^build"],
66
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.tool-versions"],
7-
"outputs": ["base-runtime/**", "deno-runtime/**", "scripts/**", ".deno-cache/**", "dist/**"]
7+
"outputs": ["base-runtime/**", "node-runtime/**", "deno-runtime/**", "scripts/**", ".deno-cache/**", "dist/**"]
88
}
99
}
1010
}

0 commit comments

Comments
 (0)