
Early binding reduces runtime universality. Dependency Injection restores control at the composition root.
title: Static Imports Are Undermining JavaScript’s Isomorphism published: true description: Early binding reduces runtime universality. Dependency Injection restores control at the composition root. tags: #javascript #webdev #architecture #designpatterns
JavaScript runs natively in both the browser and on the server. That makes true isomorphism possible.
And yet modern JavaScript architecture quietly works against it.
Consider:
import fs from "node:fs";
This line embeds a Node-only capability directly into the module. A browser cannot satisfy "node:fs" by default. The module is no longer isomorphic.
The issue is not fs.
The issue is early binding.
Static imports resolve dependencies during module evaluation. The host fixes the graph before your code runs. If a dependency is platform-specific, the module becomes platform-specific.
Instead of binding immediately, a module can declare what it needs.
// user-service.mjs
export const __deps__ = {
fs: "node:fs",
logger: "./logger.mjs",
};
export default function makeUserService({ fs, logger }) {
return {
readUserJson(path) {
const raw = fs.readFileSync(path, "utf8");
logger.log(`Read ${raw.length} bytes`);
return JSON.parse(raw);
},
};
}
The module imports nothing directly. It declares a dependency contract and receives concrete implementations from the outside.
This is Dependency Injection applied at the module level. The composition root decides what gets passed in.
// node-entry.mjs
import fs from "node:fs";
import logger from "./logger.mjs";
import makeUserService from "./user-service.mjs";
const service = makeUserService({ fs, logger });
// browser-entry.mjs
import fsAdapter from "./browser-fs-adapter.mjs";
import logger from "./logger.mjs";
import makeUserService from "./user-service.mjs";
const service = makeUserService({
fs: fsAdapter,
logger,
});
The module did not change. Only the composition root changed.
Platform decisions stay at the edge of the system — and because dependencies are injected explicitly, tests can pass fakes directly instead of mocking module imports.
Because the contract is exposed via __deps__, the composition root can be made data-driven:
// link.mjs
export async function link(entrySpecifier, overrides = {}) {
const mod = await import(entrySpecifier);
const depsSpec = mod.__deps__ ?? {};
const deps = {};
for (const [name, specifier] of Object.entries(depsSpec)) {
const finalSpecifier = overrides[specifier] ?? specifier;
const imported = await import(finalSpecifier);
deps[name] = imported.default ?? imported;
}
return mod.default(deps);
}
const service = await link("./user-service.mjs");
const service = await link("./user-service.mjs", {
"node:fs": "./browser-fs-adapter.mjs",
});
Binding becomes explicit program logic, not loader side effects.
exportspackage.json exports select entry points per environment (package-level).Import maps answer: Where is this module? Composition root answers: Which capability does this module receive?
Different layers, different concerns.
This approach is not free:
This is a tool, not a default.
Use it when:
Do not use it when:
Static imports are not wrong. They are efficient and idiomatic.
But they bind early. And early binding encodes platform assumptions.
If we care about preserving JavaScript’s isomorphism, we should be deliberate about where binding happens.
Because once a module binds to a platform capability during evaluation, it has already chosen its platform.
gemmaI ported the whole Gemma-4 family — E2B, E4B, 12B, 31B, and the 26B-A4B MoE — to run on...
communityHey DEV, I'm Tobore. Let's actually connect. I've been on here for a while now, mostly writing and...
ai(yep, kinda clickbait, just for the funsies 😊) At the beginning of the year, I relaunched my...
aiMy laptop was sitting idle with the fan at full tilt. Nothing was running that I knew of. The culprit...
githubactionsI Built a Thing! TL;DR — Google Gemini-based Pull Request reviews and Issue Triaging for...
aiI've been hearing the word "harness" thrown around a lot lately. I assumed it just meant "the IDE" or...