Create your own
Lesson illustration

Configuring TypeScript Builds for Node.js and Frida Agents

Hello. This module begins the point where the TypeScript CLI becomes a dynamic-instrumentation tool: it will have a Node.js host running on Windows and a separate Frida agent that runs inside an authorized target process.

Those are different runtime environments, not merely different folders. The host needs Node.js APIs, the Frida Node bindings, terminal I/O, and file access. The agent needs Frida’s in-process JavaScript APIs such as Process, Module, Interceptor, and send. Treating them as separate build targets prevents a large class of mistakes before anything is injected.

In this lesson, you will configure that separation: a strict ESM TypeScript build for the host, a Frida-typed TypeScript check for the agent, and frida-compile to bundle the agent into the single JavaScript file Frida will later inject. This is a foundation for attach, tracing, recording, and the shared CLI/REPL features that follow.


Two programs, two runtimes, one deliverable

The eventual tool has two cooperating programs:

ConcernNode.js hostFrida agent
Runs inYour Windows terminal processThe selected target process
Primary responsibilitiesCLI/REPL, process selection, session lifecycle, persistence, renderingIn-process observation and instrumentation
APIs availableNode.js standard library, frida Node bindingsFrida Gum globals such as Process, Module, and Interceptor
Build outputESM files under dist/host/One bundled script, dist/agent.js
Must not assumeIt can access a target process’s memory directlyIt has Node.js, a terminal, filesystem access, or your host application objects

The most important operational fact is this:

Frida injects JavaScript, not TypeScript.

TypeScript is valuable because it validates the agent source before runtime and gives you accurate editor support. But before injection, the TypeScript agent must be transpiled and bundled into a JavaScript artifact.

The diagram contrasts direct injection of a JavaScript agent with a TypeScript workflow, where `frida-compile` transpiles and bundles TypeScript project files into one JavaScript agent before it is injected into the target process.

A bundler is especially important once the agent has multiple modules. Native Frida injection expects script source, whereas your development source will eventually contain imports for argument decoders, event serializers, hook installers, and protocol definitions. frida-compile turns that module graph into an injectable script.

JavaScript API | Frida • A world-class dynamic instrumentation toolkit

Read the short “Getting started” section of Frida’s JavaScript API documentation. It explains why Frida recommends TypeScript bindings for agent development.

In the “Getting started” section, read the complete introductory paragraph beginning with Frida’s recommendation to use TypeScript bindings. Focus on the tooling benefits: the declarations are a development-time safety net, not code that is injected into the target.


A source layout that makes the boundary visible

Start with a deliberately small layout:

src/
  main.ts
  agent/
    index.ts

dist/
  host/
    main.js
  agent.js

tsconfig.base.json
tsconfig.host.json
tsconfig.agent.json
package.json

src/main.ts is the initial host entry point. As the application gains vertical slices, host-side code can be organized underneath src/ by feature: process discovery, sessions, probes, recording, and terminal adapters.

src/agent/index.ts is the injected agent entry point. Agent-only hook logic and Frida-specific helpers should remain under src/agent/.

This physical separation is useful, but it is not security or architectural enforcement by itself. TypeScript starts compilation from include patterns, but imported files can still enter a compilation even when they are excluded from the initial file search. Therefore, maintain this dependency rule:

  • Host code may read the generated agent artifact as data.
  • Agent code must not import host code.
  • Agent code must not import Node built-ins such as node:fs, node:path, or node:readline.
  • Any future shared message-contract code must be pure TypeScript: no Node APIs and no Frida runtime APIs.

The next lesson will sharpen the host-versus-agent responsibility boundary. For now, the build configuration makes accidental mixing much harder.


Install the runtime and build dependencies

From the project root, install the Frida Node binding as a production dependency. The host will use it at runtime to enumerate processes, attach, create scripts, and manage sessions.

pnpm add frida

Install the compiler, Node typings, Frida Gum typings, and agent bundler as development dependencies:

pnpm add -D typescript @types/node @types/frida-gum frida-compile

The key packages have distinct roles:

  • frida is the host-side Node.js binding used at runtime.
  • @types/frida-gum declares the globals and APIs available within a Frida agent.
  • frida-compile transpiles and bundles the agent for injection.
  • typescript performs strict static checks.
  • @types/node provides the host’s Node.js types.

Keep ESLint, Prettier, and Vitest from the engineering baseline configured as before. They apply across the repository, but the TypeScript targets below control which runtime-specific global APIs each program can see.


Build from a strict shared baseline

Create tsconfig.base.json. This contains rules that should apply to both runtime targets.

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "useUnknownInCatchVariables": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  }
}

A few choices deserve attention:

  • strict is the core correctness setting. It makes nullability, implicit any, and several unsafe assumptions visible early.
  • noUncheckedIndexedAccess is useful for CLI arguments, decoded native values, and later event payloads, where an indexed value may be absent.
  • exactOptionalPropertyTypes prevents treating “not provided” and “provided as undefined” as interchangeable.
  • useUnknownInCatchVariables supports the later design in which unexpected exceptions are translated at an application boundary instead of leaking through the system.
  • isolatedModules keeps every source file compatible with independent transpilation, which is appropriate when an agent bundler processes modules.
  • verbatimModuleSyntax makes import behavior more explicit. This reduces surprises when maintaining ESM host code.

The shared file deliberately does not choose a module system or a type environment. Those differ between the host and the agent.

TSConfig Reference - Docs on every TSConfig option

Use the relevant TSConfig reference sections to understand why separate target configurations should inherit common strictness while defining distinct module resolution, global types, and outputs.

First, in the “Extends” section, read from the explanation that a base configuration is loaded first through the warning about circular inheritance. Focus on inheritance behavior, especially the fact that each inheriting configuration replaces include and exclude. Then read the “Module” and “Module Resolution” sections. Find the guidance that modern Node projects typically use nodenext while bundled code typically uses preserve or esnext; read the module guidance and the subsequent explanation of bundler resolution. In the “Types” section, read the whole section, including the difference from typeRoots. Use the distinction to confirm why types is appropriate when each target should receive only its intended global declarations. Finally, in “Out Dir”, read the explanation of how TypeScript preserves source structure under its output folder, from output layout.


Configure the Node.js host target

Create tsconfig.host.json:

{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "./src",
    "outDir": "./dist/host",
    "types": ["node"],
    "sourceMap": true,
    "noEmitOnError": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["src/agent/**/*.ts"]
}

This target emits ESM JavaScript intended for a current Node.js runtime. It will compile:

src/main.ts

into:

dist/host/main.js

The "type": "module" field in package.json, shown shortly, complements NodeNext. In host TypeScript files, use Node-compatible ESM imports. Once you introduce relative host imports, write the emitted extension in the TypeScript import:

import { createApplication } from "./composition-root.js";

Although the source file is .ts, Node will execute the emitted .js file. NodeNext makes TypeScript model that runtime behavior accurately.

The host target deliberately specifies:

"types": ["node"]

This brings in Node global types such as process, Buffer, and timer APIs. It does not add Frida Gum’s in-process globals. A host should access Frida through explicit imports from the frida package, which keeps the dependency obvious:

import frida from "frida";

Later, the host will use that imported binding to attach and load the contents of dist/agent.js. It should never attempt to import the agent source as executable Node code.


Configure the Frida agent target

Create tsconfig.agent.json:

{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "types": ["frida-gum"],
    "noEmit": true
  },
  "include": ["src/agent/**/*.ts"]
}

This configuration has a different purpose from the host configuration:

  1. It type-checks only agent source.
  2. It makes Frida Gum declarations available through @types/frida-gum.
  3. It intentionally does not expose Node globals.
  4. It asks TypeScript to reason about code that a bundler will process.
  5. It emits no JavaScript itself, because frida-compile owns agent bundling.

The "types": ["frida-gum"] setting is a useful boundary. In src/agent/index.ts, Frida-provided globals should be known to TypeScript:

const pid = Process.id;

send({
  type: "agent-ready",
  pid
});

Conversely, this should fail during agent type-checking:

process.cwd();

That failure is valuable. The injected runtime is not Node.js, so process.cwd() would not be a legitimate agent operation even if a developer could somehow suppress the compiler error.

Create this minimal agent entry point:

// src/agent/index.ts

export {};

send({
  type: "agent-ready",
  pid: Process.id
});

export {} makes the file an explicit module. send() and Process.id are Frida agent APIs, and their definitions come from @types/frida-gum.

This message is only an initial smoke signal. Do not treat the object shape as a durable cross-process protocol yet. A later lesson will define, validate, and version typed host-agent messages before they reach application code or recording storage.


Bundle the agent and build the host

Add or merge the following fields in package.json:

{
  "private": true,
  "type": "module",
  "scripts": {
    "typecheck:host": "tsc -p tsconfig.host.json --noEmit",
    "typecheck:agent": "tsc -p tsconfig.agent.json",
    "build:host": "tsc -p tsconfig.host.json",
    "build:agent": "frida-compile src/agent/index.ts -o dist/agent.js",
    "build": "pnpm run typecheck:host && pnpm run typecheck:agent && pnpm run build:host && pnpm run build:agent"
  }
}

The build has two intentional validation stages:

  1. Type-check the host against Node’s runtime model.
  2. Type-check the agent against Frida Gum’s runtime model.
  3. Emit host JavaScript using TypeScript.
  4. Bundle the agent using frida-compile.

Use the separate scripts while developing:

pnpm run typecheck:host
pnpm run typecheck:agent
pnpm run build:host
pnpm run build:agent

Use the complete build before committing or packaging:

pnpm run build

Add generated artifacts to .gitignore:

dist/

The expected output after a successful build is:

dist/
  host/
    main.js
    main.js.map
  agent.js

Depending on the bundler version and source-map settings, there may also be a source map for agent.js.

The agent output should be treated as an opaque deployment artifact. It is valid JavaScript intended for Frida’s injected runtime, not a Node command you should run with node dist/agent.js.

For a minimal host smoke test, create src/main.ts:

console.log("Host build succeeded.");

Then verify the host artifact separately:

pnpm run build:host
pnpm exec node dist/host/main.js

You should see:

Host build succeeded.

At this stage, building the agent proves that your agent source is typed and bundleable. Actual attachment and injection come after a controlled Windows fixture is introduced.


Diagnose the common boundary mistakes

When this setup fails, the error often tells you exactly which boundary has been crossed.

SymptomLikely causeCorrective action
Cannot find name 'Process' in src/agent/Frida Gum types are absent or the wrong TS config is being usedConfirm @types/frida-gum is installed and run tsc -p tsconfig.agent.json
Cannot find name 'process' in agent codeNode-specific code entered the agentMove the operation to the host, then expose only the required command/message boundary
Node cannot resolve a relative import in compiled host outputHost source omitted the .js extension in an ESM relative importWrite ./module.js in the TypeScript source import
dist/agent.js contains unresolved importsThe agent was transpiled without bundling, or the wrong command was runUse frida-compile for the final injectable asset
Agent files appear in dist/host/The host include or exclude patterns are wrongKeep src/agent/**/*.ts excluded from the host’s root file set
An excluded agent file is still compiled by the hostA host file imported itRemove the cross-runtime import; exclude cannot block files imported into the module graph

That last case is particularly important. Configuration supports the architectural boundary, but it cannot replace design discipline. Keep a host import graph and an agent import graph. Later in the course, automated dependency checks will make this rule enforceable.


Build-completion checklist

Before proceeding, confirm the following:

  • pnpm run typecheck:host succeeds.
  • pnpm run typecheck:agent succeeds.
  • pnpm run build creates both host and agent artifacts.
  • src/agent/index.ts recognizes Process and send.
  • Node-specific globals are rejected from the agent target.
  • pnpm exec node dist/host/main.js runs the host smoke test.
  • You do not run dist/agent.js with Node; it is an injection artifact.

You now have a clean dual-target foundation: strict ESM JavaScript for the Node host and a separately type-checked, bundled JavaScript agent for Frida injection. The Frida declarations improve agent correctness without making the agent a Node program, and frida-compile produces exactly the single artifact the host will later load.

Next, you will make the runtime boundary explicit: which responsibilities belong in the host, which must happen inside the target process, and how that distinction informs the application’s ports and adapters.

Can't find a good explanation? Sign up and we'll make it for you

Sign up