platform-access
Rule under the frontend-application law. Load when an edit touches the global object or the runtime — window, globalThis, PLATFORM_ID, localStorage. Names the DI tokens and the pitfalls of server-side
SKILL.md.claude/skills/platform-access/SKILL.md
GitHub<!-- rt-kit v0.35.0 · rules/platform-access.md · 28138749f60c · правится надстройкой, не здесь -->
Browser environment — how it works here
Rule under the law docs/constitution/frontend-application.md. The law says what must be true; here
— what replaces a direct call to the global object in this tree. State — angular-patterns, the
component file — component-structure, styles — styling-bem, the server access layer —
api-layer. All five under one law.
The rule is about the front end: libs/api/** and apps/api/** are NestJS, the environment there
is its own, and nothing of the listed applies.
What it is called here
| Direct call | Here |
|---|---|
globalThis.open(...), window.open() |
inject(WINDOW).open(...) |
globalThis.crypto.randomUUID() |
inject(WINDOW).crypto.randomUUID() |
isPlatformBrowser(inject(PLATFORM_ID)) |
inject(PlatformService).isPlatformBrowser |
document.defaultView |
inject(WINDOW) |
direct document |
inject(DOCUMENT) from @angular/common — as before |
| DOM initialisation after rendering | afterNextRender() |
WINDOW, PlatformService and StorageService come from @rt-tools/core.
Where it lives
In this tree — the table in implementation.md next to it. Paths live there, not here: the rule
travels between repositories, the layout does not, and a path named in the rule lies in the first
tree that keeps its code differently.
Flow
The flow of reaching the runtime: how the global object is taken, where the environment is checked and what is done in a file without dependency injection.
flowchart TD
A[Code needs the global object or the environment] --> B{Where it lies}
B -->|A class with dependency injection| C[The global object comes by a token, not taken directly]
B -->|A pure function| D[The window is taken as a parameter: there is no injection there]
C --> E{It matters where the code runs}
D --> E
E -->|Yes| F[The platform service is asked, not the presence of a name in the environment]
E -->|No| G[Work goes on]
F --> G
G --> H{The class is created at application startup}
H -->|Yes| I[Server-side page rendering is checked too: the linter does not see it]
H -->|No| J[Done]
I --> J
How the law applies here
- The global object comes by the
WINDOWtoken, not taken directly. The type is narrowed by a cast toWindow & typeof globalThis: constructors likeIntersectionObserverare declared on the global object, not on theWindowinterface. - The environment is checked through
PlatformService, not throughtypeof window. A check by the presence of a global is right by accident and breaks on the first environment where the global is stubbed. - A direct call to the global object is refused by the linter. Sixteen browser environment names are banned on the front-end trees; the three places where direct access is deliberate are taken out from under the ban by a list in the config, not by a disable on the line.
What of the law is not here
The ban acts on names, not on paths to them: the linter lets this.#document.defaultView through,
because there is no global name in such a line. Such a call is lawful — the window there already
came by injection — but a real bypass would look the same, and it can be seen only by reading.
Direct access remains in three places, and deliberately: the inline script against theme flicker in
apps/site/src/index.html (it runs before the application starts), the startup failure handler in
apps/*/src/main.ts, and the code inside page.evaluate in the end-to-end specs — it runs in the
page, outside DI. A new place is not added to this list without the owner's explicit agreement.
Patterns
platform-access-di— ready-made injects, the type cast, pure functions, the environment check.
Pitfalls
- The token factory throws
Window is not availableif the document has nodefaultView. Under server-side page renderingdefaultViewexists, and an inject as a class field in a root service is safe, but a service that must work without a DOM at all takes the window inside a method under an environment check. - After an edit that adds
WINDOWto a service created at startup, not only the build is needed but also a running page-rendering server. The site is real SSR, and the failure shows only there. - No environment check is needed around storage:
StorageServicefalls back to memory outside the browser anyway, and an extraif (!isBrowser)around a read and a write is dead code. - There is no DI in
*.logic.tsand*.util.ts: the window is taken as a parameter, and the calling component injects it. - Preparing the state of an end-to-end spec by writing to storage is not allowed: the spec walks the same steps as the user.