Skip to main content

Dependency Resolution

OpenCore builds each server resource as its own runtime boundary. When a server bundle uses build.server.external, those packages are not included in server.js by default, so the built resource must still be able to load them at runtime.

FiveM and RedM servers running FXServer Node.js 22 use a filesystem sandbox. If a dependency resolves outside the mounted resource folder, Node can fail with errors like:

ERR_ACCESS_DENIED
permission: FileSystemRead
no device found

build.dependencyResolution controls how OpenCore makes server.external packages available to the runtime.

Quick Recommendation

Use isolated for production FiveM and RedM resources.

import { defineConfig } from '@open-core/cli'

export default defineConfig({
build: {
dependencyResolution: {
mode: 'isolated',
packageManager: 'auto',
verifySandboxPaths: true,
allowInstallScripts: false,
cache: true,
},
},
})

Configure server.external on the resources that actually need runtime packages, for example a database resource that imports pg or typeorm.

auto currently resolves to isolated for FiveM and RedM.

Configuration

type DependencyResolutionMode =
| 'auto'
| 'isolated'
| 'shared-resource'
| 'bundle'
| 'symlink'

interface DependencyResolutionConfig {
mode?: DependencyResolutionMode
packageManager?: 'auto' | 'npm' | 'pnpm' | 'yarn'
sharedResourceName?: string
verifySandboxPaths?: boolean
allowInstallScripts?: boolean
cache?: boolean
}

Options:

OptionDefaultMeaning
mode'auto'Selects the dependency strategy.
packageManager'auto'Package manager used for physical installs in isolated and shared-resource.
sharedResourceName'__opencore_deps'Resource name generated by shared-resource.
verifySandboxPathstrueScans generated resources and rejects symlinks that resolve outside the resource folder.
allowInstallScriptsfalseAllows dependency lifecycle scripts during install. Keep disabled unless a dependency truly requires it.
cachetrueReuses OpenCore's generated physical dependency trees from node_modules/.cache/opencore/dependencies.

Package Normalization

OpenCore normalizes external imports to package names before installing or checking dependencies.

External importNormalized package
typeormtypeorm
typeorm/browsertypeorm
pg/lib/clientpg
@prisma/adapter-pg@prisma/adapter-pg
@scope/package/subpath@scope/package

Node built-ins are ignored:

fs
path
node:fs
node:path

Filesystem paths are rejected:

../path
/absolute/path
file:...

Version Resolution

OpenCore never installs latest silently. Versions are resolved in this order:

  1. Resource package.json
  2. Root package.json
  3. Installed package metadata
  4. Clear build error

Local filesystem specs such as file:, link:, and workspace: are not accepted for deployed runtime dependencies. Use registry versions for resources that must run inside FXServer.

Strategies

auto

auto chooses the stable default for the target runtime.

For FiveM and RedM, auto currently means isolated.

Pros:

  • Minimal config for most projects.
  • Tracks OpenCore's safest runtime default.
  • Avoids legacy symlink behavior under the FXServer Node.js 22 sandbox.

Cons:

  • Behavior can evolve as OpenCore gains more runtime knowledge.
  • Use an explicit mode if you need a locked strategy for CI or release builds.

Use when:

  • You want the recommended OpenCore default.
  • You do not need experimental shared dependency behavior.

isolated

isolated installs runtime dependencies into each built resource that has server.external packages.

Output shape:

dist/
└── database/
├── fxmanifest.lua
├── server.js
├── package.json
└── node_modules/

What it does:

  • Writes a minimal package.json into the built resource.
  • Installs only normalized runtime dependencies required by server.external.
  • Creates a physical local node_modules in the resource.
  • Uses pnpm's hoisted node linker with copied packages when pnpm is selected, so transitive runtime imports resolve inside the resource.
  • Rejects symlinks that resolve outside the resource when verifySandboxPaths is enabled.

Pros:

  • Stable default for FiveM and RedM.
  • Best fit for FXServer Node.js 22 sandbox rules.
  • Works with packages that expect runtime files in node_modules.
  • Keeps resource boundaries explicit.
  • Easier to inspect and deploy because each resource is self-contained.

Cons:

  • Duplicates dependencies across resources.
  • Slower builds than symlink mode because packages may be installed per resource.
  • Larger output if several resources externalize the same packages.

Use when:

  • You use database drivers, ORMs, Prisma adapters, or packages with runtime assets.
  • You deploy to FiveM or RedM production servers.
  • You want the least surprising runtime behavior.

For example, typeorm requires tslib at runtime. With isolated pnpm installs, OpenCore generates node_modules/tslib inside the resource so FXServer does not climb to the workspace root and fail the sandbox check.

Example:

export default defineConfig({
build: {
dependencyResolution: {
mode: 'isolated',
verifySandboxPaths: true,
allowInstallScripts: false,
},
server: {
external: ['typeorm', 'pg', '@prisma/adapter-pg'],
},
},
})

shared-resource

shared-resource generates one dependency resource and routes external imports through that resource.

Output shape:

dist/
├── database/
│ ├── fxmanifest.lua
│ └── server.js
└── __opencore_deps/
├── fxmanifest.lua
├── noop.js
├── package.json
└── node_modules/

What it does:

  • Aggregates server.external packages from all resources using shared-resource mode.
  • Installs those dependencies once into __opencore_deps.
  • Fails on dependency version conflicts instead of choosing one silently.
  • Uses an esbuild virtual module to load dependencies with GetResourcePath('__opencore_deps').
  • Does not use regex replacement on generated JavaScript.

Pros:

  • Avoids duplicated dependency installs across resources.
  • Smaller deployment when many resources share the same runtime packages.
  • Keeps each game resource as a separate resource instead of making one monolithic bundle.

Cons:

  • Experimental.
  • FXServer Node.js 22 may still restrict cross-resource filesystem reads.
  • All participating resources must agree on dependency versions.
  • Startup order and resource availability must be considered in real server testing.

Use when:

  • Multiple resources share the same large runtime dependencies.
  • You can validate the result on a real FXServer Node.js 22 environment.
  • You accept that sandbox behavior may still change or block cross-resource access.

Example:

export default defineConfig({
build: {
dependencyResolution: {
mode: 'shared-resource',
sharedResourceName: '__opencore_deps',
verifySandboxPaths: true,
},
server: {
external: ['typeorm', 'pg'],
},
},
})
warning

Do not mark shared-resource as stable for your project until you test it on the exact FXServer version and Node.js runtime you deploy.

bundle

bundle treats server.external as a list of packages that should be compatibility-checked and then bundled into each resource.

Output shape:

dist/
└── utility/
├── fxmanifest.lua
└── server.js

No generated node_modules is required for the bundled packages.

What it does:

  • Checks each configured package before bundling.
  • Rejects native packages with binding.gyp, .node files, prebuilds, or native package metadata.
  • Warns about dynamic require() because bundlers cannot always statically resolve it.
  • Bundles compatible packages into the resource's server.js.

Pros:

  • Smallest deployment shape for pure JavaScript packages.
  • No runtime dependency install needed.
  • No cross-resource reads.
  • Good for small utilities and libraries with static imports.

Cons:

  • Experimental.
  • Not reliable for packages that load files at runtime.
  • Not reliable for packages with dynamic require() or dynamic imports.
  • Not suitable for native modules.
  • Can increase bundle size per resource if the same package is used in many resources.

Use when:

  • The dependency is a pure JavaScript library.
  • The package uses static imports or static require() calls.
  • The package does not load runtime assets from disk.
  • You want a self-contained server.js for small utility dependencies.

Avoid for:

  • Prisma engines or Prisma clients.
  • Native modules such as better-sqlite3, sqlite3, bcrypt, sharp, or canvas.
  • Packages with .node binaries.
  • Packages that read templates, migrations, schemas, certificates, or other assets at runtime.
  • Packages that construct import paths dynamically.

Example:

export default defineConfig({
build: {
dependencyResolution: {
mode: 'bundle',
},
server: {
external: ['nanoid'],
},
},
})

Pure Libraries For Bundle Mode

A pure library is a package that can be copied into the JavaScript bundle without needing files or binaries next to it at runtime.

Good signs:

  • Written in JavaScript or TypeScript and published as JavaScript.
  • No binding.gyp.
  • No .node files.
  • No prebuilds/ folder.
  • Imports are static and visible to the bundler.
  • Does not call fs.readFileSync() to load package assets at runtime.
  • Does not depend on native binaries, external executables, generated engines, or platform-specific downloads.

Usually good candidates:

  • Small ID, math, date, validation, formatting, parsing, and utility libraries.
  • Libraries like nanoid, zod, date-fns, or similar pure-JS utilities, depending on their exact version and transitive dependencies.

Usually bad candidates:

  • Database drivers with native bindings.
  • ORMs that load runtime metadata or assets dynamically.
  • Prisma-related packages.
  • Image processing packages.
  • Password hashing packages backed by native modules.
  • Packages that scan directories or load arbitrary files from their package folder at runtime.

If you are unsure, use isolated first. Move to bundle only after testing startup and the code paths that use the package.

symlink preserves the old behavior and links node_modules from the source resource into the built resource.

Pros:

  • Fast.
  • Useful for older runtimes or local experiments.
  • Avoids repeated package installs.

Cons:

  • Legacy opt-in only.
  • May fail under the FXServer Node.js 22 filesystem sandbox.
  • Can resolve packages outside the mounted resource folder.
  • Less representative of a production deployment.

Use when:

  • You are targeting an older runtime that is known to allow this behavior.
  • You are debugging locally and explicitly accept sandbox risk.

Example:

export default defineConfig({
build: {
dependencyResolution: {
mode: 'symlink',
},
server: {
external: ['typeorm', 'pg'],
},
},
})

OpenCore prints a warning when symlink mode is used:

Warning: symlink mode may fail under the FXServer Node.js 22 filesystem sandbox.
Use mode "isolated" for production.

Strategy Comparison

ModeStabilityRuntime shapeBest forMain risk
autoStableUses runtime defaultMost projectsDefault may evolve over time
isolatedStablePer-resource node_modulesProduction FiveM/RedM externalsLarger output
shared-resourceExperimentalOne generated dependency resourceShared large dependenciesCross-resource sandbox restrictions
bundleExperimentalDependencies inside server.jsPure JavaScript librariesDynamic loading and runtime assets
symlinkLegacyLinked node_modulesOlder runtimes and local testingFXServer Node.js 22 sandbox failures

Use isolated for:

  • typeorm
  • pg
  • @prisma/adapter-pg
  • Prisma-related packages
  • packages with runtime assets
  • packages where correctness matters more than output size

Use shared-resource only when:

  • many resources share the same dependencies
  • duplicated installs are too expensive
  • you can run an FXServer Node.js 22 integration test

Use bundle only when:

  • the package is pure JavaScript
  • the package has no native bindings
  • the package has no runtime asset loading
  • all important code paths work after bundling

Use symlink only when:

  • you explicitly need legacy behavior
  • you understand the sandbox risk

Troubleshooting

ERR_ACCESS_DENIED or no device found

Use isolated and keep verifySandboxPaths: true.

This usually means a runtime dependency resolved outside the mounted FiveM resource path.

Build fails because a version cannot be resolved

Add the dependency to the resource package.json or root package.json with an explicit version.

OpenCore will not install latest silently.

Bundle mode rejects a package

Use isolated unless you can remove the native dependency or runtime asset dependency.

Bundle mode is intentionally strict because a successful build does not always mean the package can run inside FXServer.

Shared-resource mode works in build but fails in server

Switch to isolated.

The build can generate valid files, but FXServer may still restrict cross-resource filesystem reads under Node.js 22.