skip to content

Local Import Paths

How a script pulls in a helper file it wrote itself or a library hosted elsewhere, and why an import that works fine under Node is refused here with no obvious explanation.

on this pageshow

explore

questions

5

Why does a k6 script's import of './helpers' fail when './helpers.js' works?

level: juniorimportance: must knowfreq 72%

answer

  1. browser-like, not Node-like
  2. the loader guesses nothing
  3. no index.js, no node_modules walk
  4. write the suffix yourself

basics

~20 s

k6 resolves import specifiers the way a browser does, not the way Node does: it never appends .js, never falls back to index.js and never searches node_modules. The extension-less path names a file that does not exist, so the load fails.

solid answer

~40 s

k6 has its own loader and does not implement the Node.js module resolution algorithm. A specifier starting with `.` or `/` is joined onto the importing file's URL and read from disk **exactly as written** — no extension probing, no `index.js` fallback for a directory, no walk up through `node_modules`. So `./helpers` resolves to a path with no extension, nothing is there, and k6 aborts before the test starts with `The moduleSpecifier "./helpers" couldn't be found on local disk.` The k6 docs call this browser-like module resolution and state that file names for imports must be fully specified. The same applies to TypeScript: k6 transpiles with esbuild purely on the `.ts` filename suffix, so that extension has to be written out too.

code

javascript · 6 lines
javascript
// tests/smoke.js -- helpers.js sits in the same directory
import { login } from './helpers.js';

export default function () {
  login();
}

go deeper

for a junior

Remember the one-line rule: in k6 you always write the file extension, and relative paths start with ./ or ../. If an import fails, read whether the message says the file was not found or the specifier was not recognised.

for a middle

Be able to explain the loader's branches: k6-prefixed names go to the built-in registry, dot or slash means a literal file path, :// means a URL, and everything else is refused. Say plainly that there is no node_modules lookup.

for a senior

Show that you diagnose from the exact error text, and that you know the fix for npm dependencies is a bundler that emits one self-contained file. Mention that this failure happens at load time, before any virtual user starts.

for a principal

The tradeoff is whether the team accepts a bundling step at all. Vendoring plain files keeps k6 runs dependency-free and debuggable; a bundler unlocks the npm ecosystem but adds a build artefact that must be versioned and kept in step with the tests.

## What k6 does with a module specifier Every `import` and every `require()` in a k6 script hands a string — the **module specifier** — to k6's own resolution step, whose only job is to turn that string into a module or a URL. In k6 v2 that step has exactly four outcomes, and only one of them ever touches your filesystem: - a specifier that is `k6` or begins with `k6/` goes to the built-in and extension module registry and never reaches disk at all; - a specifier whose first character is `.` or `/` (or a Windows drive letter such as `C:`) is treated as a **file path**; - a specifier containing `://` is treated as a **URL**, and only the `file` and `https` schemes are accepted; - anything else is refused outright with `The moduleSpecifier "lodash" couldn't be recognised as something k6 supports.` The file branch is deliberately unclever. k6 makes sure the importing module's URL ends in a slash, joins your specifier onto it as a URL, and reads that resulting path byte-for-byte off disk. There is no candidate list, no probing loop, no directory scan. That is what the k6 documentation means when it says k6 adopts **browser-like module resolution** and that file names for imports must be fully specified, such as `./helpers.js`. ## Why the missing suffix is fatal `./helpers` is a perfectly valid specifier, so the first stage succeeds: it resolves to something like `file:///srv/k6/tests/helpers`. The second stage then tries to read that exact path, finds nothing, and returns k6's local-disk error: ``` The moduleSpecifier "./helpers" couldn't be found on local disk. Make sure that you've specified the right path to the file. ``` This happens while k6 is loading the main module, before a single virtual user or iteration exists, so the run never starts. The message continues with a hint about mounting volumes when running in the Docker image, which is the other common cause of the same error. ## What Node does that k6 does not | Node.js resolution step | k6 v2 | |---|---| | Append `.js`, `.json`, `.node` to a path with no suffix | Never | | Fall back to `<dir>/index.js` when the path is a directory | Never | | Read `main` or `exports` from a nearby `package.json` | Never | | Walk up parent directories looking inside `node_modules` | Never | | Resolve a bare name such as `lodash` to an installed package | Never | There is no `node_modules` code path anywhere in k6, and no `package.json` is ever consulted. That is why installing a package with npm next to your script changes nothing: k6 cannot see it. The supported way to use an npm dependency is to run a bundler such as webpack or rollup and import the single self-contained file it emits. ## Two different failures that look alike Beginners often conflate two errors that come from different branches of the loader: 1. **`./helpers`** reaches the file branch and fails at read time, reporting `couldn't be found on local disk`. The fix is to write the suffix. 2. **`helpers`** or **`lodash`** never reaches the file branch, because the specifier does not start with a dot or a slash. It fails at resolve time, reporting `couldn't be recognised as something k6 supports`. The fix is to write a relative path, an absolute path, or an `https://` URL. Reading which of the two messages you got tells you immediately whether the problem is a typo in a path or a mistaken belief that k6 installs packages. ## Consequences for a shared helper file Say `smoke.js`, `load.js` and `soak.js` all pull in one `helpers.js`. Every one of the three has to spell the suffix out: - `import { login } from './helpers.js';` - `import { login } from '../lib/helpers.js';` - `import { login } from '/srv/k6/lib/helpers.js';` All three forms are legal. An absolute path works but ties the script to one machine's layout; k6 warns on Windows that an absolute import is not cross-platform and will not survive being moved or run with `k6 cloud`, and recommends the `file://` form if you insist on one. ## TypeScript does not relax the rule k6 transpiles with esbuild for files whose name ends in `.ts`, and that decision is made from the filename alone. A TypeScript helper is therefore imported as `./helpers.ts`. k6 will not try `.ts` and then `.js` for a bare `./helpers`, because there is still no probing anywhere in the loader — only the literal path you supplied.

  • Does k6 resolve a directory import such as './lib' to './lib/index.js'?
    No. k6 has no directory or index resolution at all, so `./lib` is treated as a filename and the read fails. Point the specifier at the file itself, for example `./lib/helpers.js`. The only specifiers k6 special-cases are `k6` and anything under `k6/`, which go to the built-in and extension registry instead of the filesystem.
  • What error does a bare specifier such as 'lodash' produce instead?
    A different one: `The moduleSpecifier "lodash" couldn't be recognised as something k6 supports.` The file branch is never entered, because k6 only treats a specifier as a path when it begins with a dot, a slash or a drive letter, and only treats it as remote when it contains `://`.
  • If npm installed the package next to my k6 script, why can k6 still not import it?
    Because k6 never looks in `node_modules` — there is no such lookup in the binary. To use an npm dependency you run a bundler such as webpack or rollup, which inlines the dependency into one self-contained file, and then import that emitted file by a relative path with its suffix written out.

A k6 import behaves like the src attribute on an HTML image tag: the exact path you typed is fetched, and nothing decides that you probably meant logo.png when you wrote logo. Node's resolver is a search; k6's is a literal fetch.

saying these in an interview costs you the question

  • k6 will find helpers.js if I just write ./helpers
  • npm install the package and k6 picks it up
  • an index.js in the folder makes ./lib importable
  • k6 runs on Node, so Node's resolver applies
  • a package.json main field tells k6 the entry file
open as a page

How does k6's own require() differ from an ES import statement in a test script?

level: middleimportance: must knowfreq 51%

basics

~20 s

k6 accepts both, but require() is k6's own implementation, not Node's: it loads built-in k6 modules, local files and remote https scripts only, it exists solely in the init stage, and it takes a specifier computed at runtime.

open as a page

When a k6 helper module imports './config.js', which file's directory is that relative to?

level: middleimportance: should knowfreq 40%

basics

~20 s

The directory of the file the specifier is written in. k6 resolves a relative import against the importing module's own URL, never against the entry script, the command line argument, or the shell's working directory.

open as a page

In k6, which network imports are accepted, and how is a fetched module handled?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Only https is accepted; k6 refuses http and every other scheme for imports. It downloads the module at load time, executes it as your test's own code, resolves that module's relative imports against its https origin, and forbids it from reaching local files.

open as a page

How would you distribute one shared k6 helpers.js across several test scripts and repositories?

level: principalimportance: nice to knowfreq 27%

basics

~20 s

k6 offers exactly three routes: a relative path inside one repository, a version-pinned https URL, or a bundler artefact imported as a local file. Since k6 has no node_modules lookup and no lockfile, the choice is really about how the version is pinned.

open as a page