Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-tigers-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@typescript/vfs": patch
---

Skip the localStorage feature detect in Node so importing the package no longer emits an ExperimentalWarning on Node 26
22 changes: 17 additions & 5 deletions packages/typescript-vfs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,25 @@ interface LocalStorageLike {
declare var localStorage: LocalStorageLike | undefined;
declare var fetch: FetchLike | undefined;

const hasProcess = typeof process !== `undefined`

// Node >= 26 exposes `localStorage` as a global accessor which emits an
// ExperimentalWarning when it is read - including via `typeof` - unless the process
// was started with `--localstorage-file`. Probing it here would print that warning at
// module evaluation for every consumer, so skip the probe entirely in a bare Node
// process, where `process.env.DEBUG` below is the only reachable way to opt in anyway.
// DOM-bearing hosts that also expose `process` (Electron renderers) still get probed.
const isBareNodeProcess =
hasProcess && typeof process.versions?.node === `string` && !(`window` in globalThis)
Comment on lines +28 to +29

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, that's overkill

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot is nothing if not rigorous!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jake Bailey (@jakebailey) Want me to remove the test? Anything else I need to do?


let hasLocalStorage = false
try {
hasLocalStorage = typeof localStorage !== `undefined`
} catch (error) { }
if (!isBareNodeProcess) {
try {
hasLocalStorage = typeof localStorage !== `undefined` && typeof localStorage.getItem === `function`
} catch (error) { }
}

const hasProcess = typeof process !== `undefined`
const shouldDebug = (hasLocalStorage && typeof localStorage!.getItem === 'function' && localStorage!.getItem("DEBUG")) || (hasProcess && process.env.DEBUG)
const shouldDebug = (hasLocalStorage && localStorage!.getItem("DEBUG")) || (hasProcess && process.env.DEBUG)
const debugLog = shouldDebug ? console.log : (_message?: any, ..._optionalParams: any[]) => ""

export interface VirtualTypeScriptEnvironment {
Expand Down
54 changes: 54 additions & 0 deletions packages/typescript-vfs/test/localStorageProbe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @jest-environment node
*/

// Regression coverage for the import-time `localStorage` feature detect.
//
// Node >= 26 exposes `localStorage` as a global accessor which emits an
// ExperimentalWarning when it is read - `typeof` included - unless the process was
// started with `--localstorage-file`. The probe therefore must not touch the global
// in a bare Node process. Installing a counting accessor lets us assert that
// directly, on whichever Node version CI happens to run.

const globals = globalThis as any

/** Defines `localStorage` as an accessor and reports how many times it was read. */
const installLocalStorageAccessor = () => {
let reads = 0
Object.defineProperty(globals, "localStorage", {
configurable: true,
get() {
reads++
return { getItem: () => null, setItem: () => { }, removeItem: () => { } }
},
})
return () => reads
}

/** Evaluates the module fresh, so the import-time probe runs again. */
const importModule = () => {
jest.resetModules()
require("../src")
}

afterEach(() => {
delete globals.localStorage
delete globals.window
})

it("does not read the localStorage global when evaluated in a bare Node process", () => {
const reads = installLocalStorageAccessor()

importModule()

expect(reads()).toBe(0)
})

it("still reads the localStorage global when a DOM is present", () => {
globals.window = {}
const reads = installLocalStorageAccessor()

importModule()

expect(reads()).toBeGreaterThan(0)
})