How do you pin which version of a k6/x extension a k6 script requires, and verify it?
answer
- a bare import pins nothing
- directive string, like use strict
- top of the file or never read
- k6 deps shows the resolved constraint
basics
~20 sPut a "use k6 with k6/x/faker >=v0.4.0" directive at the very top of the script file, or pin at build time with xk6 build --with module@version. Confirm with k6 deps, which prints each resolved constraint.
solid answer
~40 sA bare `import` of a `k6/x/` module records it as a dependency with no constraint, so pinning is explicit. Inside the script you add a directive string at the very beginning of the file — `"use k6 with k6/x/faker >=v0.4.0";`, or `"use k6 v2.1.0";` to constrain k6 itself. It must come before any real code: k6 stops scanning at the first token that is not a string, comment or semicolon, so a directive after the imports is never read. If you build the binary yourself, pin with `xk6 build --with github.com/grafana/[email protected]` instead. Verify either way with `k6 deps script.js`, which prints the resolved constraints and whether a custom build is required.
code
javascript · 7 lines"use k6 with k6/x/faker >=v0.4.0";
import faker from 'k6/x/faker';
export default function () {
console.log(faker.person.firstName());
}go deeper
Recall that importing a k6/x/ module does not fix its version, and that a "use k6 with ..." line at the very top of the script is how a k6 script states which version it needs.
Explain the three mechanics that decide whether a directive counts: it must precede any real code, it is read from every loaded file, and it overrides the unconstrained entry a bare import created.
Treat the pin as a pipeline artefact: assert on k6 deps --json output so a run cannot silently pick up a different extension build, and pin the build command with module@version when you produce the binary yourself.
Decide where the source of truth for extension versions lives for the whole team — in each script, in a shared build, or both — and accept that whichever you pick, someone has to own upgrading it.
## Why a bare import pins nothing When a k6 script imports a `k6/x/` module, k6 records that module as a build dependency with **no version constraint at all** — the wildcard `*`. That is fine while you are exploring and dangerous once the test matters: the protocol client you validated against last month is not necessarily the one a fresh runner gets. Pinning is therefore a separate, explicit act, and k6 v2 gives you two places to do it depending on which route to the binary you are on. ## Pinning inside the script: the `use k6 with` directive k6 reads **directive strings at the very beginning of a script file**, in the same spirit as `"use strict"`. Two forms exist: ```javascript "use k6 with k6/x/faker >=v0.4.0"; "use k6 v2.1.0"; import faker from 'k6/x/faker'; export default function () { console.log(faker.person.firstName()); } ``` - **`use k6 with <module> <constraint>`** constrains one extension. - **`use k6 <constraint>`** constrains **k6 itself**, which matters when your script relies on behaviour introduced in a particular k6 version. Three mechanics decide whether a directive is seen at all: 1. **Position.** k6 scans from the start of the file and stops at the first token that is not a string literal, a comment, whitespace or a `;`. A directive placed after the imports is simply never read. A `#!` shebang line is skipped before scanning begins. 2. **Scope.** Directives are read from every file the resolver loads, not only the entry script, so a shared helper module can carry the constraint for the extension it uses. 3. **Precedence.** A directive **overrides** the unconstrained entry that a bare import created, because k6 only replaces a dependency's constraint when the existing one is absent or wildcard. Two conflicting concrete constraints for the same module is an error, not a merge. The constraint itself is ordinary semantic-version range syntax — `>=v0.4.0`, `<v1`, an exact version — so pinning hard and pinning loosely are both available. If two files declare **different concrete constraints for the same module**, k6 reports an error rather than trying to intersect them; the resolution rule only fills in a constraint that was absent or wildcard, so a real disagreement is surfaced to you instead of being quietly resolved. ## Pinning at build time: `xk6 build --with` If you are producing the binary yourself, the pin lives in the build command instead: ```bash xk6 build latest --with github.com/grafana/[email protected] ``` The `@version` suffix is what makes the build reproducible; omit it and you get `latest`, which means the same command produces a different artifact next week. The two mechanisms are complementary rather than redundant: the build flag decides **what went into this binary**, and the directive decides **what any binary must provide** to run this script. ## Which mechanism to reach for | | `use k6 with` directive | `xk6 build --with module@version` | |---|---|---| | **lives in** | the script source | the build command | | **states** | what any binary must provide | what went into this binary | | **travels with** | the test, into code review | the artifact | | **read by** | `k6 run`, `k6 deps`, automatic resolution | nothing at run time | | **use it when** | always — it documents intent | you produce the binary yourself | They are complementary. A directive with no build is a requirement nobody has satisfied yet; a build with no directive is an artifact whose reason is undocumented. Carrying both means the script says what it needs and the pipeline can check that claim against the binary it holds. ## Verifying the pin with `k6 deps` `k6 deps script.js` resolves the script the way a run would and prints what it found: ``` Build Dependencies: k6: * k6/x/faker: >=v0.4.0 Imports: file:///path/to/script.js k6/x/faker Custom Build Required: yes ``` - **Build Dependencies** shows the constraint each module ended up with — the place to confirm a directive was actually picked up, since a mis-placed one shows as `*`. - **Imports** lists everything the resolver touched. - **Custom Build Required** answers whether *the binary you just invoked* satisfies the set. `k6 deps --json script.js` returns the same as `buildDependencies`, `imports` and `customBuildRequired`, which is the shape to assert on in a pipeline. One limitation to remember: like resolution itself, `k6 deps` follows **import and export syntax and `use` directives only** — it does not chase `require` calls, so a dynamically required extension is invisible to it.
- What does a "use k6" directive without the "with" part constrain?k6 itself. `"use k6 v2.1.0";` records a constraint on the `k6` dependency rather than on an extension, which is how a script states that it needs behaviour from a particular k6 version. It is resolved by the same machinery, so `k6 deps` lists `k6` alongside the extensions with its constraint.
- Why might a use directive appear to be ignored?Almost always position. k6 scans from the start of the file and stops at the first token that is not a string literal, comment, whitespace or semicolon, so a directive placed after the imports is never reached. Run `k6 deps` and look for the module showing a bare `*` — that is the symptom.
saying these in an interview costs you the question
- Thinks a package.json or go.mod pins a k6 extension
- Places the use directive below the import statements
- Believes an unconstrained import means a tested version
- Says k6 deps executes the script to find dependencies
- Expects k6 deps to follow dynamic require calls