Preload

VextJS provides the Preload mechanism, allowing the following two types of sources to execute scripts before the Node.js module is loaded:

  1. Dependency package declaration: npm package declares vext.preload in package.json
  2. Project-level directory: preload/ in the root directory of the application project

vext start / vext dev will automatically discover these declarations and inject them into the child process via the --import parameter.

Why do we need to preload?

Some tools, such as the OpenTelemetry SDK, must be initialized before the application code is loaded in order to correctly patch Node.js built-in modules (http, net, dns) and third-party libraries (MongoDB, pg, Redis, etc.).

Node.js's --import parameter is designed for exactly this: it ensures that the specified script is run before any user code is executed.

Manually adding --import requires modifying the startup script, which increases the configuration burden. VextJS’s preload mechanism automates this step:

  • The plug-in package only needs to declare vext.preload in package.json
  • The application project only needs to create the preload/ directory in the project root

The CLI will automatically complete the injection.

Starting from v0.3.6, application projects no longer need to package a local npm package for preload. Just create the preload/ directory in the project root.

Working principle

vext start / vext dev

Scan the project root preload/ directory

Read dependencies + devDependencies of project package.json

Traverse the package.json of installed dependencies and look for the "vext.preload" field

Project-level preload and package-level preload are merged, deduplicated and converted into file:/// URLs

Inject into the child process execArgv with the --import <url> parameter

When the child process starts, the preload script is executed first (earlier than all application code)

Timing diagram

sequenceDiagram
    participant CLI as vext CLI (parent process)
    participant PR as resolvePreloads()
    participant Child as child process
    participant Script as preload script
    participant App as application code

    CLI->>PR: Scan project root preload/ + direct dependencies
    PR-->>CLI: [file:///...preload.js]
    CLI->>Child: fork({ execArgv: ["--import", "file:///..."] })
    Child->>Script: executed first (--import mechanism)
    Script->>Script: SDK initialization/environment bridging/monkey-patch, etc.
    Child->>App: Load application code
    Note over App: preload is ready at this time

Statement preload

Method A: Project-level preload/ directory

Create in the application project root directory:

preload/
├── 01-bootstrap-port.ts
├── 02-bootstrap-verbose.mjs
└── 03-polyfill.js

First term rules:

RulesDescription
Directory locationFixed to project root preload/
Scan scopeNon-recursive, only scan first-level files in the current directory
File orderInject in ascending order of file names
Project level vs package levelProject level preload is executed first, and package level vext.preload is executed later
DeduplicationDeduplication by absolute path

Supported file types

TypeProcessingRecommendation
.mjsDirect injection✅ Recommended
.jsInject directly under ESM project✅ Available
.tsCompile to .vext/preload/*.mjs before starting and then inject✅ Available
.mtsCompile to .vext/preload/*.mjs before starting and then inject✅ Recommended

It is recommended to use .mjs / .mts first, which has the clearest semantics.

How TypeScript preload works

If the project-level preload/ directory contains .ts / .mts files, the CLI will use esbuild to compile them to:

.vext/preload/*.mjs

For example:

preload/01-bootstrap-port.ts
→ .vext/preload/01-bootstrap-port.__compiled__.mjs
→ --import file:///.../.vext/preload/01-bootstrap-port.__compiled__.mjs

The purpose of this is to ensure that: without modifying the vext build main compilation chain:

  • vext dev
  • vext start -Cluster worker

The preload behavior of the three links is consistent.

Behavior under vext dev

Project-level preload belongs to execution logic before startup. Therefore, when files in preload/ are added/modified/deleted:

  • vext dev will listen to this directory
  • and trigger cold restart uniformly

This ensures that the results are consistent with manual restarts and avoids "the preload has changed but the development server still uses the old injection results".

Method B: Dependency package vext.preload

Add the vext.preload field in the package.json of the npm package:

{
  "name": "my-vext-plugin",
  "vext": {
    "preload": "./dist/instrumentation.js"
  }
}

Field format

FormatExampleDescription
String"./dist/init.js"Single preload script
Array["./dist/a.js", "./dist/b.js"]Multiple scripts, injected in array order

Paths are relative to the package root (node_modules/<package>/) and are automatically resolved to absolute paths by the CLI.

Real example

vextjs-opentelemetry has this declaration built in:

{
  "name": "vextjs-opentelemetry",
  "vext": {
    "preload": "./dist/instrumentation.js"
  }
}

After installation, vext start / vext dev automatically injects --import, OpenTelemetry SDK completes initialization before the application starts, and automatic patches such as MongoDB / pg / Redis take effect.

Applicable scenarios

SceneDescription
OpenTelemetry SDKMust be initialized before module loading to monkey-patch HTTP/DB client
APM ToolsDatadog, New Relic and other APM agents are the same
Global polyfillA global patch that needs to be injected before all code is executed
Process-level configuration bridgingFor example, setting environment variables for the bootstrap provider to read during the configuration phase

The boundary between preload and bootstrap config providerpreload and src/config/bootstrap.ts both occur before the application is fully started, but have different responsibilities:

capabilitiespreloadbootstrap config provider
Execution timingBefore Node.js module is loaded (--import)Before configuring merge / validate / freeze
Main responsibilitiesSDK initialization, environment bridging, monkey patch, global polyfillReturn structured configuration patch
Whether to participate in the configuration priority chain
Is it suitable as the main path for remote database configuration

Recommended practices:

  • APM/OpenTelemetry/monkey patch → use preload
  • Bridge environment variables to bootstrap provider before starting → You can also use preload
  • Remote configuration center / database configuration main chain during startup → Use bootstrap config provider
  • The two can cooperate: preload first prepares the SDK, token cache or environment variables, and the provider then reads these statuses and outputs the patch.

Three startup modes

modepreload takes effect?Description
vext start / vext devCLI automatically discovers and injects --import
node --import <path> dist/server.jsManually add --import, the effect is the same
node dist/server.js (without --import)preload script will not execute

It is recommended to use vext start / vext dev to enjoy the convenience of automatic injection.

Cluster mode

In Cluster mode, the preload script also takes effect. The CLI passes the --import argument to all Worker processes via cluster.setupPrimary({ execArgv }):

VEXT_CLUSTER=1 vext start # Each Worker automatically loads the preload script

Notes

Safe Behavior

  • The project-level directory is a controlled single directory: Only the project root preload/ is recognized, and any directory is not scanned recursively.
  • Only scan direct dependencies: CLI only reads the dependencies + devDependencies of the project package.json, and does not recursively scan sub-dependencies
  • Skip when the file does not exist: When the file pointed to by vext.preload does not exist, the CLI will output a warning and skip it, without blocking startup.
  • Downgrade when parsing fails: The dependent package package.json is silently skipped when parsing fails.
  • fail-fast when project-level TS preload compilation fails: avoid bringing obviously unexecutable TS preload into the running phase
  • No impact when there is no preload declaration: When there is no project-level directory or package-level preload declaration, the CLI behavior is exactly the same as before.

Coexists with manual --import

CLI-injected --import does not conflict with --import manually added by the user. If the same script is injected twice, there is usually global registration protection inside the SDK and it will not be initialized repeatedly.

Suggestions for developing preload scripts

  • Scripts should be executed quickly to avoid blocking application startup
  • If it is .js / .ts, please make sure the project adopts ESM semantics ("type": "module")
  • Errors should be handled by yourself; in the case of TS preload, syntax compilation errors will directly interrupt the startup

Deployment boundaries

If you are using project-level preload/:

  • vext build will compile the project root preload/ to dist/preload/
  • .ts / .mts / .js / .mjs will all be uniformly output into .mjs files that can be directly --import
  • Therefore, when deploying in production, you usually only need to carry:
    • Project root package.json
    • dist/ (which already contains dist/preload/, if used)

vext start will give priority to reading the project root preload/; if the directory does not exist in the root directory, it will automatically fall back to reading dist/preload/.

Write custom preload

Write project-level preload

// preload/01-bootstrap-port.ts
process.env.APP_BOOTSTRAP_PORT = "3011";
// preload/02-sdk.mjs
try {
  const { init } = await import("../src/sdk.js");
  await init();
} catch (err) {
  console.warn("[app preload] init failed:", err.message);
}

Write package-level preload

If you are developing a vext plugin package that requires preload:

// src/instrumentation.ts — preload entry
try {
  console.log("[my-plugin] preload script executed");

  const { init } = await import("./sdk.js");
  await init();
} catch (err) {
  console.warn("[my-plugin] preload failed:", (err as Error).message);
}

export {};

Declare in package.json:

{
  "name": "my-vext-plugin",
  "vext": {
    "preload": "./dist/instrumentation.js"
  }
}

After building, any project using vext start / vext dev will have the preload script automatically executed after installing this package.

Next step