Skip to content

feat(create-module): lint server-runtime-unavailable APIs in server files#710

Draft
romain-pm wants to merge 2 commits into
mainfrom
feat/lint-server-runtime-boundary
Draft

feat(create-module): lint server-runtime-unavailable APIs in server files#710
romain-pm wants to merge 2 commits into
mainfrom
feat/lint-server-runtime-boundary

Conversation

@romain-pm

Copy link
Copy Markdown

Closes #703 — part of EPIC #698 (developer experience).

Problem

The server runtime is GraalJS: microtask-only, no timers, no fetch, no Node built-ins. Today you discover that at runtime, on the first call — {"error":"setTimeout is not defined"} — and only for the one API you happened to reach first. It is the first wall a developer coming from the JS ecosystem hits.

Change

ESLint core rules (no-restricted-globals, no-restricted-imports) scoped to **/*.server.{js,jsx,ts,tsx} and **/*.action.{js,ts}, shipped in the scaffolded config and dogfooded on the samples and the test module. No new plugin, no new dependency. Client files are untouched — they run in a browser, where all of this exists.

Each message points at a new reference page, docs/3-reference/3-server-runtime-boundary, documenting what is available, what is not, and why.

How the restricted list was established

Rather than assuming, the restricted names were probed against the exact GraalJS version the engine pins (23.0.5), in a context built with the same options as GraalVMEngine.ContextPoolFactory#create, evaluating typeof <name> for ~80 names. The 40 restricted globals are those the probe reported undefined, grouped as timers/scheduling, network & async I/O, Node.js, browser, and web-platform extras. node:* and bare Node built-in imports both throw in the probe (Unsupported URI scheme node / Error reading: fs), hence the import patterns.

Deliberately not restricted:

  • console — present, used throughout the samples;
  • TextEncoder / TextDecoder — absent from raw GraalJS but polyfilled by the engine (fast-text-encoding in src/server/index.ts), so they work at runtime;
  • Promise, Intl, Atomics, WeakRef, all ECMAScript builtins — present;
  • a dozen further absent names (Worker, ReadableStream, …) documented as equally unavailable but left out of the rule, to keep it to what people actually reach for.

One real violation found

Repo-wide, exactly one: vite-plugin/fixtures/src/process.env.server.tsx reads process.env.NODE_ENV — the one legitimate use, since the Vite plugin statically replaces that expression at build time. It gets a targeted eslint-disable-next-line with the reason, and the exception is documented on the reference page. The rule was not weakened.

Verification

node --test → 4 tests: a server file using setTimeout, fetch, process.env and both Node import forms fails with both rule IDs and messages ending in the docs URL; a clean server file using console + TextEncoder passes; a client file using fetch, AbortController, window and document passes. eslint --print-config independently confirms the scoping (40 restricted globals on a server file, none on a client file). Repo yarn lint is green, and the vite-plugin snapshot test passes after regenerating the one affected map.

Not done

The build-time scan for transitive references (item 2 of the issue) is out of scope by design: the rule is syntactic and does not follow imports, which the docs page states.

Authored with Claude Code and reviewed before pushing; the test and lint output above was reproduced independently.

…iles

Server files run in GraalJS embedded in the Jahia JVM: plain ECMAScript,
no event loop, no I/O, no Node.js and no browser. Until now that was only
discovered at render time, e.g. `{"error":"setTimeout is not defined"}`
on the first call of an action using a timer.

Add a config-only guard, built on the ESLint core rules
`no-restricted-globals` and `no-restricted-imports`, scoped to
`**/*.server.{js,jsx,ts,tsx}` and `**/*.action.{js,ts}`:

- 40 globals verified `undefined` in a GraalJS 23.0.5 context configured
  like `GraalVMEngine.ContextPoolFactory#create` (timers, network, Node.js,
  browser and a few web-platform extras);
- `node:*` specifiers and bare Node.js built-in module names.

Names the runtime does provide are deliberately not restricted: `console`,
`Promise`, `Intl`, `Atomics`, and the `TextEncoder`/`TextDecoder` polyfill
the engine installs at startup. Client files are untouched.

Every message links the new reference page, which documents the
microtask-only async model, what is and isn't available, and the
`process.env.NODE_ENV` exception (statically replaced by the Vite plugin).

The same block is dogfooded by the repository root config, the hydrogen
sample and the test module. The only violation it surfaced is the
`process.env.NODE_ENV` vite-plugin fixture, opted out explicitly.

Closes #703
@github-actions

Copy link
Copy Markdown

📝 Documentation Guidelines

Thank you for contributing to our documentation! To ensure your contributions meet our standards, please review these resources:

This comment is posted automatically when changes are detected in the docs/ folder.

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

🦜 Chachalog

javascript-modules patch

Create a new entry online or run npx chachalog@0.5.2 prompt to create a new entry locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lint/build-time guard for APIs unavailable in the server runtime

1 participant